-
Notifications
You must be signed in to change notification settings - Fork 11
fix!: flagd, behavioral tests conformance (evaluation and config) #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
67e804f
608c65b
2184d6b
6a4fc07
1b26140
6f43120
d55d62e
f5c6a3b
97f7ab6
9b604a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| +2 −2 | .github/workflows/ci.yml | |
| +7 −14 | .github/workflows/release-please.yml | |
| +94 −0 | .release-please-config.json | |
| +2 −2 | .release-please-manifest.json | |
| +175 −0 | CHANGELOG.md | |
| +6 −0 | CODEOWNERS | |
| +69 −25 | README.md | |
| +176 −0 | docker-compose.yaml | |
| +1 −1 | flagd/Dockerfile | |
| +15 −0 | flags/metadata-flags.json | |
| +38 −0 | flags/selector-flag-combined-metadata.json | |
| +56 −0 | flags/testing-flags.json | |
| +64 −0 | flags/zero-flags.json | |
| +31 −0 | gherkin/config.feature | |
| +59 −18 | gherkin/connection.feature | |
| +52 −0 | gherkin/evaluation.feature | |
| +55 −0 | gherkin/metadata.feature | |
| +1 −1 | gherkin/rpc-caching.feature | |
| +28 −0 | gherkin/sync-payload.feature | |
| +7 −6 | gherkin/targeting.feature | |
| +8 −0 | launchpad/configs/metadata.json | |
| +16 −0 | launchpad/configs/sync-payload.json | |
| +1 −1 | launchpad/go.mod | |
| +2 −0 | launchpad/go.sum | |
| +82 −3 | launchpad/handlers/http.go | |
| +50 −1 | launchpad/pkg/filewatcher.go | |
| +49 −9 | launchpad/pkg/flagd.go | |
| +2 −1 | launchpad/pkg/json.go | |
| +0 −65 | release-please-config.json | |
| +11 −4 | renovate.json | |
| +1 −1 | version.txt |
| +2 −2 | .release-please-manifest.json | |
| +1 −1 | CODEOWNERS | |
| +4 −2 | Makefile | |
| +3 −6 | go.mod | |
| +6 −13 | go.sum | |
| +22 −0 | json/CHANGELOG.md | |
| +5 −0 | json/flagd.json | |
| +3 −0 | json/flagd.yaml | |
| +3 −0 | json/flagd_definitions.go | |
| +85 −33 | json/flagd_definitions_test.go | |
| +142 −63 | json/flags.json | |
| +102 −71 | json/flags.yaml | |
| +1 −1 | json/targeting.json | |
| +1 −1 | json/targeting.yaml | |
| +0 −0 | json/test/flagd/negative/.gitkeep | |
| +153 −0 | json/test/flagd/positive/with-array-flags.json | |
| +127 −0 | json/test/flagd/positive/with-object-flags.json | |
| +14 −0 | json/test/flags/negative/with-array-flags.json | |
| +153 −0 | json/test/flags/positive/example.flagdimport.array.json | |
| +127 −0 | json/test/flags/positive/example.flagdimport.map.json | |
| +0 −0 | json/test/flags/positive/no-default-variant.json | |
| +13 −0 | json/test/flags/positive/null-default-variant.json | |
| +12 −0 | json/test/flags/positive/undefined-default-variant.json | |
| +1 −1 | json/version.txt | |
| +25 −244 | package-lock.json | |
| +12 −0 | protobuf/CHANGELOG.md | |
| +10 −2 | protobuf/flagd/sync/v1/sync.proto |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,17 +24,24 @@ impl ConfigWorld { | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| fn clear(&mut self) { | ||||||||||||||||||||||||||||||||||||
| // SAFETY: Removing environment variables is safe here because: | ||||||||||||||||||||||||||||||||||||
| // 1. We're only removing variables that were set during this specific test scenario | ||||||||||||||||||||||||||||||||||||
| // 2. The test is protected by #[serial_test::serial] | ||||||||||||||||||||||||||||||||||||
| // 3. This prevents test pollution between scenarios | ||||||||||||||||||||||||||||||||||||
| // 4. All variables being removed are tracked in world.env_vars | ||||||||||||||||||||||||||||||||||||
| // 1. The test is protected by #[serial_test::serial] | ||||||||||||||||||||||||||||||||||||
| // 2. This prevents test pollution between scenarios | ||||||||||||||||||||||||||||||||||||
| // 3. We clear env vars that were set in previous scenarios | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Clear env vars that were set in the previous scenario | ||||||||||||||||||||||||||||||||||||
| for key in self.env_vars.keys() { | ||||||||||||||||||||||||||||||||||||
| unsafe { | ||||||||||||||||||||||||||||||||||||
| std::env::remove_var(key); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| self.env_vars.clear(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Also explicitly clear FLAGD_OFFLINE_FLAG_SOURCE_PATH because it can affect resolver type | ||||||||||||||||||||||||||||||||||||
| // via FlagdOptions::default() logic, even if not tracked in world.env_vars | ||||||||||||||||||||||||||||||||||||
| unsafe { | ||||||||||||||||||||||||||||||||||||
| std::env::remove_var("FLAGD_OFFLINE_FLAG_SOURCE_PATH"); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| self.env_vars.clear(); | ||||||||||||||||||||||||||||||||||||
| self.options = FlagdOptions::default(); | ||||||||||||||||||||||||||||||||||||
| self.provider = None; | ||||||||||||||||||||||||||||||||||||
| self.option_values.clear(); | ||||||||||||||||||||||||||||||||||||
|
|
@@ -106,43 +113,60 @@ async fn env_with_value(world: &mut ConfigWorld, env: String, value: String) { | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| #[when(expr = "a config was initialized")] | ||||||||||||||||||||||||||||||||||||
| async fn initialize_config(world: &mut ConfigWorld) { | ||||||||||||||||||||||||||||||||||||
| // Start with defaults (which reads from environment variables) | ||||||||||||||||||||||||||||||||||||
| let mut options = FlagdOptions::default(); | ||||||||||||||||||||||||||||||||||||
| let mut resolver_explicitly_set = false; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Handle resolver type first | ||||||||||||||||||||||||||||||||||||
| if let Some(resolver) = world.option_values.get("resolver") { | ||||||||||||||||||||||||||||||||||||
| // Apply env vars from world.env_vars to ensure they take precedence | ||||||||||||||||||||||||||||||||||||
| // This handles cases where env vars were set in test steps but timing issues | ||||||||||||||||||||||||||||||||||||
| // prevent FlagdOptions::default() from reading them correctly | ||||||||||||||||||||||||||||||||||||
| if let Some(resolver) = world.env_vars.get("FLAGD_RESOLVER") { | ||||||||||||||||||||||||||||||||||||
| options.resolver_type = match resolver.to_uppercase().as_str() { | ||||||||||||||||||||||||||||||||||||
| "RPC" => ResolverType::Rpc, | ||||||||||||||||||||||||||||||||||||
| "REST" => ResolverType::Rest, | ||||||||||||||||||||||||||||||||||||
| "IN-PROCESS" | "INPROCESS" => ResolverType::InProcess, | ||||||||||||||||||||||||||||||||||||
| "FILE" | "OFFLINE" => ResolverType::File, | ||||||||||||||||||||||||||||||||||||
| _ => ResolverType::Rpc, | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
| } else if let Ok(resolver) = std::env::var("FLAGD_RESOLVER") { | ||||||||||||||||||||||||||||||||||||
| resolver_explicitly_set = true; | ||||||||||||||||||||||||||||||||||||
| // Update port based on resolver type when set via env var | ||||||||||||||||||||||||||||||||||||
| options.port = match options.resolver_type { | ||||||||||||||||||||||||||||||||||||
| ResolverType::Rpc => 8013, | ||||||||||||||||||||||||||||||||||||
| ResolverType::InProcess => 8015, | ||||||||||||||||||||||||||||||||||||
| _ => options.port, | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Handle explicit options - these override env vars | ||||||||||||||||||||||||||||||||||||
| if let Some(resolver) = world.option_values.get("resolver") { | ||||||||||||||||||||||||||||||||||||
| options.resolver_type = match resolver.to_uppercase().as_str() { | ||||||||||||||||||||||||||||||||||||
| "RPC" => ResolverType::Rpc, | ||||||||||||||||||||||||||||||||||||
| "REST" => ResolverType::Rest, | ||||||||||||||||||||||||||||||||||||
| "IN-PROCESS" | "INPROCESS" => ResolverType::InProcess, | ||||||||||||||||||||||||||||||||||||
| "FILE" | "OFFLINE" => ResolverType::File, | ||||||||||||||||||||||||||||||||||||
| _ => ResolverType::Rpc, | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
| resolver_explicitly_set = true; | ||||||||||||||||||||||||||||||||||||
| // Update port based on resolver type when explicitly set | ||||||||||||||||||||||||||||||||||||
| options.port = match options.resolver_type { | ||||||||||||||||||||||||||||||||||||
| ResolverType::Rpc => 8013, | ||||||||||||||||||||||||||||||||||||
| ResolverType::InProcess => 8015, | ||||||||||||||||||||||||||||||||||||
| _ => options.port, | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Set default port based on resolver type | ||||||||||||||||||||||||||||||||||||
| options.port = match options.resolver_type { | ||||||||||||||||||||||||||||||||||||
| ResolverType::Rpc => 8013, | ||||||||||||||||||||||||||||||||||||
| ResolverType::InProcess => 8015, | ||||||||||||||||||||||||||||||||||||
| _ => options.port, | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Handle source configuration after resolver type | ||||||||||||||||||||||||||||||||||||
| // Handle source configuration - may override resolver type for backwards compatibility | ||||||||||||||||||||||||||||||||||||
| // BUT only if resolver wasn't explicitly set to "rpc" | ||||||||||||||||||||||||||||||||||||
| if let Some(source) = world.option_values.get("offlineFlagSourcePath") { | ||||||||||||||||||||||||||||||||||||
| options.source_configuration = Some(source.clone()); | ||||||||||||||||||||||||||||||||||||
| if options.resolver_type != ResolverType::Rpc { | ||||||||||||||||||||||||||||||||||||
| // For backwards compatibility: if offline path is set, switch to File resolver | ||||||||||||||||||||||||||||||||||||
| // UNLESS resolver was explicitly set to "rpc" (in which case keep it as "rpc") | ||||||||||||||||||||||||||||||||||||
| if !resolver_explicitly_set || options.resolver_type != ResolverType::Rpc { | ||||||||||||||||||||||||||||||||||||
| options.resolver_type = ResolverType::File; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
160
to
167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic here for determining the resolver type seems to contradict the stated goal of the pull request and the implementation in The PR description states: " The logic in If the goal is to give any explicitly set resolver precedence, the condition should be simplified. If the current logic is required to pass the behavioral tests, it might be worth adding a more detailed comment explaining this specific backward-compatibility rule.
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Handle remaining explicit options | ||||||||||||||||||||||||||||||||||||
| // Handle remaining explicit options (these override env vars) | ||||||||||||||||||||||||||||||||||||
| if let Some(host) = world.option_values.get("host") { | ||||||||||||||||||||||||||||||||||||
| options.host = host.clone(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
@@ -217,6 +241,9 @@ async fn initialize_config(world: &mut ConfigWorld) { | |||||||||||||||||||||||||||||||||||
| if let Some(selector) = world.option_values.get("selector") { | ||||||||||||||||||||||||||||||||||||
| options.selector = Some(selector.clone()); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| if let Some(provider_id) = world.option_values.get("providerId") { | ||||||||||||||||||||||||||||||||||||
| options.provider_id = Some(provider_id.clone()); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| if let Some(max_size) = world | ||||||||||||||||||||||||||||||||||||
| .option_values | ||||||||||||||||||||||||||||||||||||
| .get("maxCacheSize") | ||||||||||||||||||||||||||||||||||||
|
|
@@ -269,12 +296,30 @@ async fn check_option_value( | |||||||||||||||||||||||||||||||||||
| "retryBackoffMaxMs" => Some(world.options.retry_backoff_max_ms.to_string()), | ||||||||||||||||||||||||||||||||||||
| "retryGracePeriod" => Some(world.options.retry_grace_period.to_string()), | ||||||||||||||||||||||||||||||||||||
| "selector" => world.options.selector.clone(), | ||||||||||||||||||||||||||||||||||||
| "providerId" => world.options.provider_id.clone(), | ||||||||||||||||||||||||||||||||||||
| "socketPath" => world.options.socket_path.clone(), | ||||||||||||||||||||||||||||||||||||
| "streamDeadlineMs" => Some(world.options.stream_deadline_ms.to_string()), | ||||||||||||||||||||||||||||||||||||
| _ => None, | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
| let expected = convert_type(&option_type, &value); | ||||||||||||||||||||||||||||||||||||
| assert_eq!(actual, expected, "Option '{}' value mismatch", option); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // For resolver type, do case-insensitive comparison since enum normalizes to lowercase | ||||||||||||||||||||||||||||||||||||
| let actual_normalized = if option == "resolver" { | ||||||||||||||||||||||||||||||||||||
| actual.as_ref().map(|v| v.to_lowercase()) | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| actual.clone() | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
| let expected_normalized = if option == "resolver" { | ||||||||||||||||||||||||||||||||||||
| expected.as_ref().map(|v| v.to_lowercase()) | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| expected.clone() | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||
| actual_normalized, expected_normalized, | ||||||||||||||||||||||||||||||||||||
| "Option '{}' value mismatch", | ||||||||||||||||||||||||||||||||||||
| option | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| #[test(tokio::test)] | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.