From 76d783a9f35156613fb01e3cecdf3b915669502f Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 2 Sep 2026 10:56:35 -0400 Subject: [PATCH 1/3] chore: use testcontainers for e2e tests Signed-off-by: Todd Baert --- .../openfeature-flagd-provider/Gemfile.lock | 11 +++++ .../features/support/env.rb | 43 ++++++++++++------- .../openfeature-flagd-provider.gemspec | 2 + 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/providers/openfeature-flagd-provider/Gemfile.lock b/providers/openfeature-flagd-provider/Gemfile.lock index b541f0c..b20623b 100644 --- a/providers/openfeature-flagd-provider/Gemfile.lock +++ b/providers/openfeature-flagd-provider/Gemfile.lock @@ -9,6 +9,7 @@ GEM remote: https://rubygems.org/ specs: ast (2.4.3) + base64 (0.3.0) bigdecimal (4.0.1) builder (3.3.0) cucumber (9.2.1) @@ -38,6 +39,11 @@ GEM cucumber-tag-expressions (6.1.2) diff-lcs (1.5.1) docile (1.4.1) + docker-api (2.4.0) + excon (>= 0.64.0) + multi_json + excon (1.7.1) + logger ffi (1.17.4) ffi (1.17.4-arm64-darwin) ffi (1.17.4-x86_64-linux-gnu) @@ -67,6 +73,7 @@ GEM logger (1.7.0) memoist3 (1.0.0) mini_mime (1.1.5) + multi_json (1.21.1) multi_test (1.1.0) openfeature-sdk (0.3.1) parallel (1.27.0) @@ -131,6 +138,8 @@ GEM sys-uname (1.5.1) ffi (~> 1.1) memoist3 (~> 1.0.0) + testcontainers-core (0.2.0) + docker-api (~> 2.2) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) @@ -142,6 +151,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + base64 cucumber (~> 9.2) logger openfeature-flagd-provider! @@ -150,6 +160,7 @@ DEPENDENCIES rubocop simplecov (~> 0.22) standard + testcontainers-core (~> 0.2) BUNDLED WITH 2.3.25 diff --git a/providers/openfeature-flagd-provider/features/support/env.rb b/providers/openfeature-flagd-provider/features/support/env.rb index eb33302..e1da6c9 100644 --- a/providers/openfeature-flagd-provider/features/support/env.rb +++ b/providers/openfeature-flagd-provider/features/support/env.rb @@ -3,46 +3,57 @@ require "net/http" require "uri" require "rspec/expectations" +require "testcontainers" require "open_feature/sdk" require "openfeature/flagd/provider" World(RSpec::Matchers) -# Boots the shared flagd testbed container and drives its launchpad so flagd is serving the -# standard flag set before the suite runs. Uses the Docker CLI directly (reliable on CI). +# Boots the shared flagd testbed container via testcontainers and drives its launchpad so flagd is +# serving the standard flag set before the suite runs. The image version is pinned to the +# flagd-testbed submodule (test-harness/version.txt), matching the other flagd providers. module Testbed VERSION = File.read(File.expand_path("../../test-harness/version.txt", __dir__)).strip IMAGE = "ghcr.io/open-feature/flagd-testbed:v#{VERSION}" - NAME = "flagd-e2e-ruby-#{Process.pid}" + RPC_PORT = 8013 + HEALTH_PORT = 8014 + LAUNCHPAD_PORT = 8080 module_function def start - sh("docker rm -f #{NAME}") - raise "failed to start #{IMAGE} (is docker available?)" unless - sh("docker run -d --name #{NAME} -p 8013:8013 -p 8014:8014 -p 8080:8080 #{IMAGE}") + @container = Testcontainers::DockerContainer + .new(IMAGE) + .with_exposed_ports(RPC_PORT, HEALTH_PORT, LAUNCHPAD_PORT) + .with_wait_for(:tcp_port, LAUNCHPAD_PORT) + @container.start # launchpad starts flagd with the default flag set - wait_for { http_code(:post, "http://localhost:8080/start") } - wait_for { http_code(:get, "http://localhost:8014/healthz") == "200" } || + Net::HTTP.post(URI("http://#{host}:#{mapped(LAUNCHPAD_PORT)}/start"), "") + wait_for { http_code(:get, "http://#{host}:#{mapped(HEALTH_PORT)}/healthz") == "200" } || raise("flagd testbed did not become healthy in time") + + # point the provider at the mapped RPC port for this run + ENV["FLAGD_HOST"] = host + ENV["FLAGD_PORT"] = mapped(RPC_PORT).to_s end def stop - sh("docker rm -f #{NAME}") + @container&.stop + @container&.remove + end + + def host + @container.host end - def sh(cmd) - system(cmd, out: File::NULL, err: File::NULL) + def mapped(port) + @container.mapped_port(port) end def http_code(verb, url) uri = URI(url) - res = if verb == :post - Net::HTTP.post(uri, "") - else - Net::HTTP.get_response(uri) - end + res = (verb == :post) ? Net::HTTP.post(uri, "") : Net::HTTP.get_response(uri) res.code rescue nil diff --git a/providers/openfeature-flagd-provider/openfeature-flagd-provider.gemspec b/providers/openfeature-flagd-provider/openfeature-flagd-provider.gemspec index 2409c56..0e28325 100644 --- a/providers/openfeature-flagd-provider/openfeature-flagd-provider.gemspec +++ b/providers/openfeature-flagd-provider/openfeature-flagd-provider.gemspec @@ -36,6 +36,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "rspec", "~> 3.12.0" spec.add_development_dependency "cucumber", "~> 9.2" + spec.add_development_dependency "testcontainers-core", "~> 0.2" + spec.add_development_dependency "base64" spec.add_development_dependency "logger" spec.add_development_dependency "standard" spec.add_development_dependency "rubocop" From 446d7f6cf24423c1b04b843b6d521d3d1d45fa37 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 2 Sep 2026 11:11:36 -0400 Subject: [PATCH 2/3] fixup: start wait Signed-off-by: Todd Baert --- .../openfeature-flagd-provider/features/support/env.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/openfeature-flagd-provider/features/support/env.rb b/providers/openfeature-flagd-provider/features/support/env.rb index e1da6c9..aae57c9 100644 --- a/providers/openfeature-flagd-provider/features/support/env.rb +++ b/providers/openfeature-flagd-provider/features/support/env.rb @@ -9,9 +9,7 @@ World(RSpec::Matchers) -# Boots the shared flagd testbed container via testcontainers and drives its launchpad so flagd is -# serving the standard flag set before the suite runs. The image version is pinned to the -# flagd-testbed submodule (test-harness/version.txt), matching the other flagd providers. +# Boots the flagd testbed container via testcontainers and drives its launchpad; version pinned to test-harness/version.txt module Testbed VERSION = File.read(File.expand_path("../../test-harness/version.txt", __dir__)).strip IMAGE = "ghcr.io/open-feature/flagd-testbed:v#{VERSION}" @@ -28,8 +26,10 @@ def start .with_wait_for(:tcp_port, LAUNCHPAD_PORT) @container.start - # launchpad starts flagd with the default flag set - Net::HTTP.post(URI("http://#{host}:#{mapped(LAUNCHPAD_PORT)}/start"), "") + # start flagd via launchpad; retry since the mapped port opens before the app serves + launchpad = "http://#{host}:#{mapped(LAUNCHPAD_PORT)}/start" + wait_for { http_code(:post, launchpad) == "200" } || + raise("launchpad did not start flagd in time") wait_for { http_code(:get, "http://#{host}:#{mapped(HEALTH_PORT)}/healthz") == "200" } || raise("flagd testbed did not become healthy in time") From 6eb311d605d6b216ad8c47e08f85085e53d051e8 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 2 Sep 2026 11:34:43 -0400 Subject: [PATCH 3/3] fixup: clean up container Signed-off-by: Todd Baert --- .../openfeature-flagd-provider/features/support/env.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/providers/openfeature-flagd-provider/features/support/env.rb b/providers/openfeature-flagd-provider/features/support/env.rb index aae57c9..c84cc8b 100644 --- a/providers/openfeature-flagd-provider/features/support/env.rb +++ b/providers/openfeature-flagd-provider/features/support/env.rb @@ -36,11 +36,18 @@ def start # point the provider at the mapped RPC port for this run ENV["FLAGD_HOST"] = host ENV["FLAGD_PORT"] = mapped(RPC_PORT).to_s + rescue + stop + raise end def stop @container&.stop @container&.remove + rescue + nil + ensure + @container = nil end def host