From f44ccb3cd825fe37a62e3a58bde0819db71d6115 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 8 Feb 2024 16:40:11 -0500 Subject: [PATCH 01/15] feat: implement in-memory provider Signed-off-by: Todd Baert Co-authored-by: Joris Goovaerts <1333336+CommCody@users.noreply.github.com> --- .github/workflows/e2e.yml | 7 +- .gitmodules | 6 +- CONTRIBUTING.md | 6 - Directory.Packages.props | 1 - spec | 1 + .../{NoOpProvider.cs => Constants.cs} | 0 src/OpenFeature/Providers/Memory/Flag.cs | 78 +++++++++++ .../Providers/Memory/InMemoryProvider.cs | 126 ++++++++++++++++++ test-harness | 1 - .../OpenFeature.E2ETests.csproj | 1 - .../Steps/EvaluationStepDefinitions.cs | 102 ++++++++++++-- .../InMemoryProviderTests.cs | 56 ++++++++ 12 files changed, 356 insertions(+), 29 deletions(-) create mode 160000 spec rename src/OpenFeature/Constant/{NoOpProvider.cs => Constants.cs} (100%) create mode 100644 src/OpenFeature/Providers/Memory/Flag.cs create mode 100644 src/OpenFeature/Providers/Memory/InMemoryProvider.cs delete mode 160000 test-harness create mode 100644 test/OpenFeature.Tests/InMemoryProviderTests.cs diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 50a0b812..4dea1592 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -13,11 +13,6 @@ on: jobs: e2e-tests: runs-on: ubuntu-latest - services: - flagd: - image: ghcr.io/open-feature/flagd-testbed:latest - ports: - - 8013:8013 steps: - uses: actions/checkout@v4 with: @@ -36,7 +31,7 @@ jobs: - name: Initialize Tests run: | git submodule update --init --recursive - cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/ + cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ - name: Run Tests run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions diff --git a/.gitmodules b/.gitmodules index 61d2eb45..160a3e90 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "test-harness"] - path = test-harness - url = https://github.com/open-feature/test-harness.git +[submodule "spec"] + path = spec + url = git@github.com:open-feature/spec.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f8cf33c..cdac14e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,12 +67,6 @@ To be able to run the e2e tests, first we need to initialize the submodule and c git submodule update --init --recursive && cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/ ``` -Afterwards, you need to start flagd locally: - -```bash -docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest -``` - Now you can run the tests using: ```bash diff --git a/Directory.Packages.props b/Directory.Packages.props index e4d708a4..ddd42e4a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,6 @@ - diff --git a/spec b/spec new file mode 160000 index 00000000..b58c3b4e --- /dev/null +++ b/spec @@ -0,0 +1 @@ +Subproject commit b58c3b4ec68b0db73e6c33ed4a30e94b1ede5e85 diff --git a/src/OpenFeature/Constant/NoOpProvider.cs b/src/OpenFeature/Constant/Constants.cs similarity index 100% rename from src/OpenFeature/Constant/NoOpProvider.cs rename to src/OpenFeature/Constant/Constants.cs diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs new file mode 100644 index 00000000..3e9ca761 --- /dev/null +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; + +namespace OpenFeature.Providers.Memory +{ + /// + /// Flag representation for the in-memory provider. + /// + public class Flag + { + + } + + /// + /// Flag representation for the in-memory provider. + /// + public class Flag : Flag + { + private Dictionary Variants; + private string DefaultVariant; + private Func ContextEvaluator; + + /// + /// Flag representation for the in-memory provider. + /// + /// dictionary of variants and their corresponding values + /// default variant (should match 1 key in variants dictionary) + /// optional context-sensitive evaluation function + public Flag(Dictionary variants, string defaultVariant, Func contextEvaluator = null) + { + this.Variants = variants; + this.DefaultVariant = defaultVariant; + this.ContextEvaluator = contextEvaluator; + } + + internal ResolutionDetails Evaluate(string flagKey, T defaultValue, EvaluationContext evaluationContext) + { + T value; + if (this.ContextEvaluator == null) + { + if (this.Variants.TryGetValue(this.DefaultVariant, out value)) + { + return new ResolutionDetails( + flagKey, + value, + variant: this.DefaultVariant, + reason: Reason.Static + ); + } + else + { + throw new GeneralException($"variant {this.DefaultVariant} not found"); + } + } + else + { + string variant = this.ContextEvaluator.Invoke(evaluationContext); + this.Variants.TryGetValue(variant, out value); + if (value == null) + { + throw new GeneralException($"variant {variant} not found"); + } + else + { + return new ResolutionDetails( + flagKey, + value, + variant: variant, + reason: Reason.TargetingMatch + ); + } + } + } + } +} diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs new file mode 100644 index 00000000..cca96ef9 --- /dev/null +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; + +namespace OpenFeature.Providers.Memory +{ + /// + /// The in memory provider. + /// Useful for testing and demonstration purposes. + /// + /// In Memory Provider specification + public class InMemoryProvider : FeatureProvider + { + + private readonly Metadata _metadata = new Metadata("InMemory"); + + private Dictionary _flags; + + /// + public override Metadata GetMetadata() + { + return this._metadata; + } + + /// + /// Construct a new InMemoryProvider. + /// + /// dictionary of Flags + public InMemoryProvider(IDictionary flags = null) + { + if (flags == null) + { + this._flags = new Dictionary(); + } + else + { + this._flags = new Dictionary(flags); // shallow copy + } + } + + /// + /// Updating provider flags configuration, replacing all flags. + /// + /// the flags to use instead of the previous flags. + public async ValueTask UpdateFlags(IDictionary flags) + { + if (flags is null) + throw new ArgumentNullException(nameof(flags)); + this._flags = new Dictionary(flags); // shallow copy + var @event = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderConfigurationChanged, + ProviderName = _metadata.Name, + FlagsChanged = flags.Keys.ToList(), // emit all + Message = "flags changed", + }; + await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false); + } + + /// + public override Task> ResolveBooleanValue( + string flagKey, + bool defaultValue, + EvaluationContext context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveStringValue( + string flagKey, + string defaultValue, + EvaluationContext context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveIntegerValue( + string flagKey, + int defaultValue, + EvaluationContext context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveDoubleValue( + string flagKey, + double defaultValue, + EvaluationContext context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + /// + public override Task> ResolveStructureValue( + string flagKey, + Value defaultValue, + EvaluationContext context = null) + { + return Task.FromResult(Resolve(flagKey, defaultValue, context)); + } + + private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext context) + { + if (!this._flags.TryGetValue(flagKey, out var flag)) + { + throw new FlagNotFoundException($"flag {flag} not found"); + } + else + { + if (typeof(Flag).Equals(flag.GetType())) { + return (flag as Flag).Evaluate(flagKey, defaultValue, context); + } else { + throw new TypeMismatchException($"flag {flag} not found"); + } + } + } + } +} diff --git a/test-harness b/test-harness deleted file mode 160000 index 01c4a433..00000000 --- a/test-harness +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 01c4a433a3bcb0df6448da8c0f8030d11ce710af diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj index e0093787..757c4e8f 100644 --- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj +++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj @@ -16,7 +16,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index d2cd483d..bb4bea5e 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -5,8 +5,9 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using OpenFeature.Constant; -using OpenFeature.Contrib.Providers.Flagd; +using OpenFeature.Extension; using OpenFeature.Model; +using OpenFeature.Providers.Memory; using TechTalk.SpecFlow; using Xunit; @@ -41,15 +42,14 @@ public class EvaluationStepDefinitions public EvaluationStepDefinitions(ScenarioContext scenarioContext) { _scenarioContext = scenarioContext; - var flagdProvider = new FlagdProvider(); - Api.Instance.SetProviderAsync(flagdProvider).Wait(); - client = Api.Instance.GetClient(); } - [Given(@"a provider is registered with cache disabled")] - public void Givenaproviderisregisteredwithcachedisabled() + [Given(@"a provider is registered")] + public void GivenAProviderIsRegistered() { - + var memProvider = new InMemoryProvider(e2eFlagConfig); + Api.Instance.SetProviderAsync(memProvider).Wait(); + client = Api.Instance.GetClient(); } [When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")] @@ -247,7 +247,7 @@ public void Thenthedefaultstringvalueshouldbereturned() public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) { Assert.Equal(Reason.Error.ToString(), notFoundDetails.Reason); - Assert.Contains(errorCode, notFoundDetails.ErrorMessage); + Assert.Equal(errorCode, notFoundDetails.ErrorType.GetDescription()); } [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] @@ -268,8 +268,88 @@ public void Thenthedefaultintegervalueshouldbereturned() public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) { Assert.Equal(Reason.Error.ToString(), typeErrorDetails.Reason); - Assert.Contains(errorCode, this.typeErrorDetails.ErrorMessage); - } - + Assert.Equal(errorCode, typeErrorDetails.ErrorType.GetDescription()); + } + + private IDictionary e2eFlagConfig = new Dictionary(){ + { + "boolean-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on" + ) + }, + { + "string-flag", new Flag( + variants: new Dictionary(){ + { "greeting", "hi" }, + { "parting", "bye" } + }, + defaultVariant: "greeting" + ) + }, + { + "integer-flag", new Flag( + variants: new Dictionary(){ + { "one", 1 }, + { "ten", 10 } + }, + defaultVariant: "ten" + ) + }, + { + "float-flag", new Flag( + variants: new Dictionary(){ + { "tenth", 0.1 }, + { "half", 0.5 } + }, + defaultVariant: "half" + ) + }, + { + "object-flag", new Flag( + variants: new Dictionary(){ + { "empty", new Value() }, + { "template", new Value(Structure.Builder() + .Set("showImages", true) + .Set("title", "Check out these pics!") + .Set("imagesPerPage", 100).Build() + ) + } + }, + defaultVariant: "template" + ) + }, + { + "context-aware", new Flag( + variants: new Dictionary(){ + { "internal", "INTERNAL" }, + { "external", "EXTERNAL" } + }, + defaultVariant: "external", + (context) => { + if (context.GetValue("fn").AsString == "Sulisław" + && context.GetValue("ln").AsString == "Świętopełk" + && context.GetValue("age").AsInteger == 29 + && context.GetValue("customer").AsBoolean == false) + { + return "internal"; + } + else return "external"; + } + ) + }, + { + "wrong-flag", new Flag( + variants: new Dictionary(){ + { "one", "uno" }, + { "two", "dos" } + }, + defaultVariant: "one" + ) + } + }; } } diff --git a/test/OpenFeature.Tests/InMemoryProviderTests.cs b/test/OpenFeature.Tests/InMemoryProviderTests.cs new file mode 100644 index 00000000..bc24c018 --- /dev/null +++ b/test/OpenFeature.Tests/InMemoryProviderTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using OpenFeature.Error; +using OpenFeature.Model; +using OpenFeature.Constant; +using OpenFeature.Providers.Memory; +using Xunit; + +namespace OpenFeature.Tests +{ + // most of the in-memory tests are handled in the e2e suite + public class InMemoryProviderTests + { + [Fact] + public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() + { + var handlerRuns = 0; + var provider = new InMemoryProvider(new Dictionary(){ + { + "boolean-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on" + ) + }}); + + // setup client and handler and run initial eval + await Api.Instance.SetProviderAsync("mem-test", provider).ConfigureAwait(false); + var client = Api.Instance.GetClient("mem-test"); + client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, (details) => { + handlerRuns++; + }); + Assert.True(await client.GetBooleanValue("boolean-flag", false).ConfigureAwait(false)); + + // update flags + await provider.UpdateFlags(new Dictionary(){ + { + "string-flag", new Flag( + variants: new Dictionary(){ + { "greeting", "hi" }, + { "parting", "bye" } + }, + defaultVariant: "greeting" + ) + }}).ConfigureAwait(false); + + // new flag should be present, old gone (defaults), handler run. + Assert.Equal("hi", await client.GetStringValue("string-flag", "nope").ConfigureAwait(false)); + Assert.False(await client.GetBooleanValue("boolean-flag", false).ConfigureAwait(false)); + Assert.Equal(1, handlerRuns); + } + } +} From 80471e41fbfc6cac21f9ce5e286501b9d505f628 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 8 Feb 2024 16:50:13 -0500 Subject: [PATCH 02/15] fixup: spec path Signed-off-by: Todd Baert --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 160a3e90..85115b56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "spec"] path = spec - url = git@github.com:open-feature/spec.git + url = https://github.com/open-feature/spec.git From f5385adaa6976b4b6d78f35e88cbff4981636e69 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 8 Feb 2024 16:53:14 -0500 Subject: [PATCH 03/15] fixup: format Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/Flag.cs | 2 +- .../Providers/Memory/InMemoryProvider.cs | 7 +++++-- .../Steps/EvaluationStepDefinitions.cs | 16 ++++++++-------- test/OpenFeature.Tests/InMemoryProviderTests.cs | 9 +++++---- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 3e9ca761..5f2b5540 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -5,7 +5,7 @@ using OpenFeature.Model; namespace OpenFeature.Providers.Memory -{ +{ /// /// Flag representation for the in-memory provider. /// diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index cca96ef9..b1977ea9 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -115,9 +115,12 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati } else { - if (typeof(Flag).Equals(flag.GetType())) { + if (typeof(Flag).Equals(flag.GetType())) + { return (flag as Flag).Evaluate(flagKey, defaultValue, context); - } else { + } + else + { throw new TypeMismatchException($"flag {flag} not found"); } } diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs index bb4bea5e..4f091ab1 100644 --- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs @@ -279,7 +279,7 @@ public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatyp { "off", false } }, defaultVariant: "on" - ) + ) }, { "string-flag", new Flag( @@ -288,7 +288,7 @@ public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatyp { "parting", "bye" } }, defaultVariant: "greeting" - ) + ) }, { "integer-flag", new Flag( @@ -297,7 +297,7 @@ public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatyp { "ten", 10 } }, defaultVariant: "ten" - ) + ) }, { "float-flag", new Flag( @@ -306,7 +306,7 @@ public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatyp { "half", 0.5 } }, defaultVariant: "half" - ) + ) }, { "object-flag", new Flag( @@ -320,7 +320,7 @@ public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatyp } }, defaultVariant: "template" - ) + ) }, { "context-aware", new Flag( @@ -338,8 +338,8 @@ public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatyp return "internal"; } else return "external"; - } - ) + } + ) }, { "wrong-flag", new Flag( @@ -348,7 +348,7 @@ public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatyp { "two", "dos" } }, defaultVariant: "one" - ) + ) } }; } diff --git a/test/OpenFeature.Tests/InMemoryProviderTests.cs b/test/OpenFeature.Tests/InMemoryProviderTests.cs index bc24c018..61587cf4 100644 --- a/test/OpenFeature.Tests/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/InMemoryProviderTests.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using OpenFeature.Constant; using OpenFeature.Error; using OpenFeature.Model; -using OpenFeature.Constant; using OpenFeature.Providers.Memory; using Xunit; @@ -24,13 +24,14 @@ public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() { "off", false } }, defaultVariant: "on" - ) + ) }}); // setup client and handler and run initial eval await Api.Instance.SetProviderAsync("mem-test", provider).ConfigureAwait(false); var client = Api.Instance.GetClient("mem-test"); - client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, (details) => { + client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, (details) => + { handlerRuns++; }); Assert.True(await client.GetBooleanValue("boolean-flag", false).ConfigureAwait(false)); @@ -44,7 +45,7 @@ await provider.UpdateFlags(new Dictionary(){ { "parting", "bye" } }, defaultVariant: "greeting" - ) + ) }}).ConfigureAwait(false); // new flag should be present, old gone (defaults), handler run. From 06281f282f51db4e2d826f12902ff21eb6be93e0 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 09:25:02 -0500 Subject: [PATCH 04/15] Update src/OpenFeature/Providers/Memory/Flag.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/Flag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 5f2b5540..ec75819b 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -57,7 +57,7 @@ internal ResolutionDetails Evaluate(string flagKey, T defaultValue, Evaluatio } else { - string variant = this.ContextEvaluator.Invoke(evaluationContext); + var variant = this.ContextEvaluator.Invoke(evaluationContext); this.Variants.TryGetValue(variant, out value); if (value == null) { From a6b4a4a0a43d3eb14edd340657b39ec33f506481 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 09:25:10 -0500 Subject: [PATCH 05/15] Update src/OpenFeature/Providers/Memory/Flag.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/Flag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index ec75819b..7b4d6c53 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -17,7 +17,7 @@ public class Flag /// /// Flag representation for the in-memory provider. /// - public class Flag : Flag + public sealed class Flag : Flag { private Dictionary Variants; private string DefaultVariant; From 4fa6f78dc1513e32198f90b4e9f8f8fd610702b8 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 09:27:28 -0500 Subject: [PATCH 06/15] Update src/OpenFeature/Providers/Memory/Flag.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/Flag.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 7b4d6c53..af51b120 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -4,6 +4,7 @@ using OpenFeature.Error; using OpenFeature.Model; +#nullable enable namespace OpenFeature.Providers.Memory { /// From dc004222fecdf042e75e41b2d66fd4017ccdfbf4 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 09:27:36 -0500 Subject: [PATCH 07/15] Update src/OpenFeature/Providers/Memory/InMemoryProvider.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/InMemoryProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index b1977ea9..9aafd133 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -7,6 +7,7 @@ using OpenFeature.Error; using OpenFeature.Model; +#nullable enable namespace OpenFeature.Providers.Memory { /// From b47b64bd639f4cf82d9cdfb9dfd3d0f7a90d362b Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 10:28:39 -0500 Subject: [PATCH 08/15] fixup: pr feedback and tests Signed-off-by: Todd Baert --- .github/workflows/e2e.yml | 2 +- src/OpenFeature/Providers/Memory/Flag.cs | 10 +- .../Providers/Memory/InMemoryProvider.cs | 16 +- .../InMemoryProviderTests.cs | 147 ++++++++++++++++-- 4 files changed, 146 insertions(+), 29 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4dea1592..a9794050 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -34,4 +34,4 @@ jobs: cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ - name: Run Tests - run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions + run: dotnet test test/OpenFeature.E2ETests/ --logger GitHubActions diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index af51b120..e1a88287 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -10,7 +10,7 @@ namespace OpenFeature.Providers.Memory /// /// Flag representation for the in-memory provider. /// - public class Flag + public interface Flag { } @@ -22,7 +22,7 @@ public sealed class Flag : Flag { private Dictionary Variants; private string DefaultVariant; - private Func ContextEvaluator; + private Func? ContextEvaluator; /// /// Flag representation for the in-memory provider. @@ -30,14 +30,14 @@ public sealed class Flag : Flag /// dictionary of variants and their corresponding values /// default variant (should match 1 key in variants dictionary) /// optional context-sensitive evaluation function - public Flag(Dictionary variants, string defaultVariant, Func contextEvaluator = null) + public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null) { this.Variants = variants; this.DefaultVariant = defaultVariant; this.ContextEvaluator = contextEvaluator; } - internal ResolutionDetails Evaluate(string flagKey, T defaultValue, EvaluationContext evaluationContext) + internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) { T value; if (this.ContextEvaluator == null) @@ -58,7 +58,7 @@ internal ResolutionDetails Evaluate(string flagKey, T defaultValue, Evaluatio } else { - var variant = this.ContextEvaluator.Invoke(evaluationContext); + var variant = this.ContextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); this.Variants.TryGetValue(variant, out value); if (value == null) { diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 9aafd133..d7a9ffaf 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -32,7 +32,7 @@ public override Metadata GetMetadata() /// Construct a new InMemoryProvider. /// /// dictionary of Flags - public InMemoryProvider(IDictionary flags = null) + public InMemoryProvider(IDictionary? flags = null) { if (flags == null) { @@ -67,7 +67,7 @@ public async ValueTask UpdateFlags(IDictionary flags) public override Task> ResolveBooleanValue( string flagKey, bool defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } @@ -76,7 +76,7 @@ public override Task> ResolveBooleanValue( public override Task> ResolveStringValue( string flagKey, string defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } @@ -85,7 +85,7 @@ public override Task> ResolveStringValue( public override Task> ResolveIntegerValue( string flagKey, int defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } @@ -94,7 +94,7 @@ public override Task> ResolveIntegerValue( public override Task> ResolveDoubleValue( string flagKey, double defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } @@ -103,12 +103,12 @@ public override Task> ResolveDoubleValue( public override Task> ResolveStructureValue( string flagKey, Value defaultValue, - EvaluationContext context = null) + EvaluationContext? context = null) { return Task.FromResult(Resolve(flagKey, defaultValue, context)); } - private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext context) + private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext? context) { if (!this._flags.TryGetValue(flagKey, out var flag)) { @@ -118,7 +118,7 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati { if (typeof(Flag).Equals(flag.GetType())) { - return (flag as Flag).Evaluate(flagKey, defaultValue, context); + return ((Flag)flag).Evaluate(flagKey, defaultValue, context); } else { diff --git a/test/OpenFeature.Tests/InMemoryProviderTests.cs b/test/OpenFeature.Tests/InMemoryProviderTests.cs index 61587cf4..799fd9db 100644 --- a/test/OpenFeature.Tests/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/InMemoryProviderTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Threading; using OpenFeature.Constant; using OpenFeature.Error; using OpenFeature.Model; @@ -9,16 +10,133 @@ namespace OpenFeature.Tests { - // most of the in-memory tests are handled in the e2e suite public class InMemoryProviderTests { + private FeatureProvider commonProvider; + + public InMemoryProviderTests() + { + var provider = new InMemoryProvider(new Dictionary(){ + { + "boolean-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on" + ) + }, + { + "string-flag", new Flag( + variants: new Dictionary(){ + { "greeting", "hi" }, + { "parting", "bye" } + }, + defaultVariant: "greeting" + ) + }, + { + "integer-flag", new Flag( + variants: new Dictionary(){ + { "one", 1 }, + { "ten", 10 } + }, + defaultVariant: "ten" + ) + }, + { + "float-flag", new Flag( + variants: new Dictionary(){ + { "tenth", 0.1 }, + { "half", 0.5 } + }, + defaultVariant: "half" + ) + }, + { + "object-flag", new Flag( + variants: new Dictionary(){ + { "empty", new Value() }, + { "template", new Value(Structure.Builder() + .Set("showImages", true) + .Set("title", "Check out these pics!") + .Set("imagesPerPage", 100).Build() + ) + } + }, + defaultVariant: "template" + ) + } + }); + + this.commonProvider = provider; + } + + [Fact] + public async void GetBoolean_ShouldEvaluate() + { + ResolutionDetails details = await this.commonProvider.ResolveBooleanValue("boolean-flag", false, EvaluationContext.Empty).ConfigureAwait(false); + Assert.True(details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("on", details.Variant); + } + + [Fact] + public async void GetString_ShouldEvaluate() + { + ResolutionDetails details = await this.commonProvider.ResolveStringValue("string-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal("hi", details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("greeting", details.Variant); + } + + [Fact] + public async void GetInt_ShouldEvaluate() + { + ResolutionDetails details = await this.commonProvider.ResolveIntegerValue("integer-flag", 13, EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal(10, details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("ten", details.Variant); + } + + [Fact] + public async void GetDouble_ShouldEvaluate() + { + ResolutionDetails details = await this.commonProvider.ResolveDoubleValue("float-flag", 13, EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal(0.5, details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("half", details.Variant); + } + + [Fact] + public async void GetStruct_ShouldEvaluate() + { + ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty).ConfigureAwait(false); + Assert.Equal(true, details.Value.AsStructure["showImages"].AsBoolean); + Assert.Equal("Check out these pics!", details.Value.AsStructure["title"].AsString); + Assert.Equal(100, details.Value.AsStructure["imagesPerPage"].AsInteger); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("template", details.Variant); + } + + [Fact] + public async void MissingFlag_ShouldThrow() + { + await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("missing-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + } + + [Fact] + public async void MismatchedFlag_ShouldThrow() + { + await Assert.ThrowsAsync(() => commonProvider.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)).ConfigureAwait(false); + } + [Fact] public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() { - var handlerRuns = 0; var provider = new InMemoryProvider(new Dictionary(){ { - "boolean-flag", new Flag( + "old-flag", new Flag( variants: new Dictionary(){ { "on", true }, { "off", false } @@ -27,19 +145,13 @@ public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() ) }}); - // setup client and handler and run initial eval - await Api.Instance.SetProviderAsync("mem-test", provider).ConfigureAwait(false); - var client = Api.Instance.GetClient("mem-test"); - client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, (details) => - { - handlerRuns++; - }); - Assert.True(await client.GetBooleanValue("boolean-flag", false).ConfigureAwait(false)); + ResolutionDetails details = await provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty).ConfigureAwait(false); + Assert.True(details.Value); // update flags await provider.UpdateFlags(new Dictionary(){ { - "string-flag", new Flag( + "new-flag", new Flag( variants: new Dictionary(){ { "greeting", "hi" }, { "parting", "bye" } @@ -48,10 +160,15 @@ await provider.UpdateFlags(new Dictionary(){ ) }}).ConfigureAwait(false); + var res = await provider.GetEventChannel().Reader.ReadAsync().ConfigureAwait(false) as ProviderEventPayload; + Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res.Type); + + await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + // new flag should be present, old gone (defaults), handler run. - Assert.Equal("hi", await client.GetStringValue("string-flag", "nope").ConfigureAwait(false)); - Assert.False(await client.GetBooleanValue("boolean-flag", false).ConfigureAwait(false)); - Assert.Equal(1, handlerRuns); + ResolutionDetails detailsAfter = await provider.ResolveStringValue("new-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false); + Assert.True(details.Value); + Assert.Equal("hi", detailsAfter.Value); } } } From 377eb1ba1456c4f7fbdb537226f80d4bdf4da562 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 10:54:08 -0500 Subject: [PATCH 09/15] fixup: more tests Signed-off-by: Todd Baert --- .github/workflows/e2e.yml | 2 +- src/OpenFeature/Providers/Memory/Flag.cs | 2 +- .../Memory}/InMemoryProviderTests.cs | 51 +++++++++++++++++-- 3 files changed, 48 insertions(+), 7 deletions(-) rename test/OpenFeature.Tests/{ => Providers/Memory}/InMemoryProviderTests.cs (75%) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a9794050..4dea1592 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -34,4 +34,4 @@ jobs: cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/ - name: Run Tests - run: dotnet test test/OpenFeature.E2ETests/ --logger GitHubActions + run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index e1a88287..2a9a315c 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -39,7 +39,7 @@ public Flag(Dictionary variants, string defaultVariant, Func Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) { - T value; + T? value = default; if (this.ContextEvaluator == null) { if (this.Variants.TryGetValue(this.DefaultVariant, out value)) diff --git a/test/OpenFeature.Tests/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs similarity index 75% rename from test/OpenFeature.Tests/InMemoryProviderTests.cs rename to test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 799fd9db..6180bb0f 100644 --- a/test/OpenFeature.Tests/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -53,6 +53,22 @@ public InMemoryProviderTests() defaultVariant: "half" ) }, + { + "context-aware", new Flag( + variants: new Dictionary(){ + { "internal", "INTERNAL" }, + { "external", "EXTERNAL" } + }, + defaultVariant: "external", + (context) => { + if (context.GetValue("email").AsString.Contains("@faas.com")) + { + return "internal"; + } + else return "external"; + } + ) + }, { "object-flag", new Flag( variants: new Dictionary(){ @@ -66,6 +82,15 @@ public InMemoryProviderTests() }, defaultVariant: "template" ) + }, + { + "invalid-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "missing" + ) } }); @@ -73,7 +98,7 @@ public InMemoryProviderTests() } [Fact] - public async void GetBoolean_ShouldEvaluate() + public async void GetBoolean_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveBooleanValue("boolean-flag", false, EvaluationContext.Empty).ConfigureAwait(false); Assert.True(details.Value); @@ -82,7 +107,7 @@ public async void GetBoolean_ShouldEvaluate() } [Fact] - public async void GetString_ShouldEvaluate() + public async void GetString_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveStringValue("string-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false); Assert.Equal("hi", details.Value); @@ -91,7 +116,7 @@ public async void GetString_ShouldEvaluate() } [Fact] - public async void GetInt_ShouldEvaluate() + public async void GetInt_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveIntegerValue("integer-flag", 13, EvaluationContext.Empty).ConfigureAwait(false); Assert.Equal(10, details.Value); @@ -100,7 +125,7 @@ public async void GetInt_ShouldEvaluate() } [Fact] - public async void GetDouble_ShouldEvaluate() + public async void GetDouble_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveDoubleValue("float-flag", 13, EvaluationContext.Empty).ConfigureAwait(false); Assert.Equal(0.5, details.Value); @@ -109,7 +134,7 @@ public async void GetDouble_ShouldEvaluate() } [Fact] - public async void GetStruct_ShouldEvaluate() + public async void GetStruct_ShouldEvaluateWithReasonAndVariant() { ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty).ConfigureAwait(false); Assert.Equal(true, details.Value.AsStructure["showImages"].AsBoolean); @@ -119,6 +144,16 @@ public async void GetStruct_ShouldEvaluate() Assert.Equal("template", details.Variant); } + [Fact] + public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() + { + EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build(); + ResolutionDetails details = await this.commonProvider.ResolveStringValue("context-aware", "nope", context).ConfigureAwait(false); + Assert.Equal("INTERNAL", details.Value); + Assert.Equal(Reason.TargetingMatch, details.Reason); + Assert.Equal("internal", details.Variant); + } + [Fact] public async void MissingFlag_ShouldThrow() { @@ -131,6 +166,12 @@ public async void MismatchedFlag_ShouldThrow() await Assert.ThrowsAsync(() => commonProvider.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)).ConfigureAwait(false); } + [Fact] + public async void MissingVariant_ShouldThrow() + { + await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + } + [Fact] public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() { From 976bcfb0962a73b1edf0f2e24f651ed2d911fc6f Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 11:05:35 -0500 Subject: [PATCH 10/15] fixup: more tests Signed-off-by: Todd Baert --- .../Providers/Memory/InMemoryProvider.cs | 17 ++++++++++++----- .../Providers/Memory/InMemoryProviderTests.cs | 6 ++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index d7a9ffaf..5d584bc0 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -48,16 +48,23 @@ public InMemoryProvider(IDictionary? flags = null) /// Updating provider flags configuration, replacing all flags. /// /// the flags to use instead of the previous flags. - public async ValueTask UpdateFlags(IDictionary flags) + public async ValueTask UpdateFlags(IDictionary? flags = null) { - if (flags is null) - throw new ArgumentNullException(nameof(flags)); - this._flags = new Dictionary(flags); // shallow copy + var changed = this._flags.Keys.ToList(); + if (flags == null) + { + this._flags = new Dictionary(); + } + else + { + this._flags = new Dictionary(flags); // shallow copy + } + changed.AddRange(this._flags.Keys.ToList()); var @event = new ProviderEventPayload { Type = ProviderEventTypes.ProviderConfigurationChanged, ProviderName = _metadata.Name, - FlagsChanged = flags.Keys.ToList(), // emit all + FlagsChanged = changed, // emit all Message = "flags changed", }; await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false); diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 6180bb0f..de0c73bc 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -154,6 +154,12 @@ public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( Assert.Equal("internal", details.Variant); } + [Fact] + public async void EmptyFlags_ShouldWork() + { + var provider = new InMemoryProvider(); + } + [Fact] public async void MissingFlag_ShouldThrow() { From 85494356702ed42c77e81a4dd65ac6e27d41d46d Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 11:09:25 -0500 Subject: [PATCH 11/15] fixup: more tests Signed-off-by: Todd Baert --- test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index de0c73bc..1b7e876b 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -158,6 +158,7 @@ public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( public async void EmptyFlags_ShouldWork() { var provider = new InMemoryProvider(); + await provider.UpdateFlags().ConfigureAwait(false); } [Fact] From 02b6e2bb0b6eb4fddd332457aab6b2328205276f Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 12:15:34 -0500 Subject: [PATCH 12/15] fixup: more tests Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/Flag.cs | 3 +-- .../Providers/Memory/InMemoryProviderTests.cs | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index 2a9a315c..99975de3 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -59,8 +59,7 @@ internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? e else { var variant = this.ContextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); - this.Variants.TryGetValue(variant, out value); - if (value == null) + if (!this.Variants.TryGetValue(variant, out value)) { throw new GeneralException($"variant {variant} not found"); } diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 1b7e876b..e5ade795 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -91,6 +91,18 @@ public InMemoryProviderTests() }, defaultVariant: "missing" ) + }, + { + "invalid-evaluator-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on", + (context) => { + return "missing"; + } + ) } }); @@ -157,8 +169,9 @@ public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( [Fact] public async void EmptyFlags_ShouldWork() { - var provider = new InMemoryProvider(); + var provider = new InMemoryProvider(); await provider.UpdateFlags().ConfigureAwait(false); + Assert.Equal("InMemory", provider.GetMetadata().Name); } [Fact] @@ -174,11 +187,17 @@ public async void MismatchedFlag_ShouldThrow() } [Fact] - public async void MissingVariant_ShouldThrow() + public async void MissingDefaultVariant_ShouldThrow() { await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); } + [Fact] + public async void MissingEvaluatedVariant_ShouldThrow() + { + await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-evaluator-flag", false, EvaluationContext.Empty)).ConfigureAwait(false); + } + [Fact] public async void PutConfiguration_shouldUpdateConfigAndRunHandlers() { From 2ee2a82d9a0a70a9278c10036bd33b1cd17d303e Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 12:36:20 -0500 Subject: [PATCH 13/15] fixup: lint Signed-off-by: Todd Baert --- .../OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index e5ade795..3df038ab 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -169,7 +169,7 @@ public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( [Fact] public async void EmptyFlags_ShouldWork() { - var provider = new InMemoryProvider(); + var provider = new InMemoryProvider(); await provider.UpdateFlags().ConfigureAwait(false); Assert.Equal("InMemory", provider.GetMetadata().Name); } From 30a313456966a4aeafbaa1d45ca61718faa09b90 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 12:45:47 -0500 Subject: [PATCH 14/15] fixup: error message Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/InMemoryProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 5d584bc0..5e4a57ac 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -119,7 +119,7 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati { if (!this._flags.TryGetValue(flagKey, out var flag)) { - throw new FlagNotFoundException($"flag {flag} not found"); + throw new FlagNotFoundException($"flag {flagKey} not found"); } else { @@ -129,7 +129,7 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati } else { - throw new TypeMismatchException($"flag {flag} not found"); + throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}"); } } } From 6c70f15a85a343df11cc67497ca73bc4436d625b Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 9 Feb 2024 13:09:15 -0500 Subject: [PATCH 15/15] fixup: add comment about conversion Signed-off-by: Todd Baert --- src/OpenFeature/Providers/Memory/InMemoryProvider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs index 5e4a57ac..ddd1e270 100644 --- a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs +++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs @@ -123,6 +123,8 @@ private ResolutionDetails Resolve(string flagKey, T defaultValue, Evaluati } else { + // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa. + // In a production provider, such behavior is probably not desirable; consider supporting conversion. if (typeof(Flag).Equals(flag.GetType())) { return ((Flag)flag).Evaluate(flagKey, defaultValue, context);