From aae0dfa29338e436e36a03034e390484fd91ac59 Mon Sep 17 00:00:00 2001 From: vishalup29 Date: Sat, 29 Nov 2025 03:08:41 -0600 Subject: [PATCH] Issue #1754 Implement equals/hashCode for EvaluationContext and add tests. Signed-off-by: vishalup29 --- .../dev/openfeature/sdk/ImmutableContext.java | 32 +++++++++++++++++++ .../sdk/LayeredEvaluationContext.java | 18 +++++++++++ .../openfeature/sdk/ImmutableContextTest.java | 16 ++++++++++ .../sdk/LayeredEvaluationContextTest.java | 27 ++++++++++++++++ 4 files changed, 93 insertions(+) diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java index 35f28d4f4..9d2bb67a6 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java @@ -96,6 +96,38 @@ public EvaluationContext merge(EvaluationContext overridingContext) { return new ImmutableContext(attributes); } + /** + * Equality for EvaluationContext implementations is defined in terms of their resolved + * attribute maps. Two contexts are considered equal if their {@link #asMap()} representations + * contain the same key/value pairs, regardless of how the context was constructed or layered. + * + * @param o the object to compare with this context + * @return true if the other object is an EvaluationContext whose resolved attributes match + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EvaluationContext)) { + return false; + } + EvaluationContext that = (EvaluationContext) o; + return this.asMap().equals(that.asMap()); + } + + /** + * Computes a hash code consistent with {@link #equals(Object)} by delegating to the + * hash code of this context's resolved attribute map. This ensures that contexts with + * identical effective attributes will have the same hash code. + * + * @return the hash code derived from this context's attribute map + */ + @Override + public int hashCode() { + return asMap().hashCode(); + } + @SuppressWarnings("all") private static class DelegateExclusions { @ExcludeFromGeneratedCoverageReport diff --git a/src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java b/src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java index bdd81f8c3..70fbfc503 100644 --- a/src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java +++ b/src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java @@ -251,6 +251,24 @@ public Map asObjectMap() { return map; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EvaluationContext)) { + return false; + } + + EvaluationContext that = (EvaluationContext) o; + return this.asMap().equals(that.asMap()); + } + + @Override + public int hashCode() { + return asMap().hashCode(); + } + void putHookContext(EvaluationContext context) { if (context == null || context.isEmpty()) { return; diff --git a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java index 0b8a44d0d..e77911cc0 100644 --- a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java +++ b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java @@ -148,6 +148,22 @@ void mergeShouldObtainKeysFromOverridingContextWhenExistingContextIsEmpty() { assertEquals(new java.util.HashSet<>(java.util.Arrays.asList("key1", "key2")), merge.keySet()); } + @DisplayName("Two ImmutableContext objects with identical attributes are considered equal") + @Test + void testImmutableContextEquality() { + Map map1 = new HashMap<>(); + map1.put("key", new Value("value")); + + Map map2 = new HashMap<>(); + map2.put("key", new Value("value")); + + ImmutableContext a = new ImmutableContext(null, map1); + ImmutableContext b = new ImmutableContext(null, map2); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } + @DisplayName("Two different MutableContext objects with the different contents are not considered equal") @Test void unequalImmutableContextsAreNotEqual() { diff --git a/src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java b/src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java index 7eecd9abd..21faf25f0 100644 --- a/src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java +++ b/src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java @@ -397,5 +397,32 @@ void mergesCorrectlyWhenOtherHasNoTargetingKey() { merged.asMap()); assertEquals(invocationContext.getTargetingKey(), merged.getTargetingKey()); } + + @Test + void testLayeredContextEquality() { + Map baseMap = Map.of("k", new Value("v")); + Map layerMap = Map.of("x", new Value("y")); + + EvaluationContext base = new MutableContext(null, baseMap); + EvaluationContext layer = new MutableContext(null, layerMap); + + LayeredEvaluationContext l1 = new LayeredEvaluationContext(base, layer, null, null); + LayeredEvaluationContext l2 = new LayeredEvaluationContext(base, layer, null, null); + + assertEquals(l1, l2); + assertEquals(l1.hashCode(), l2.hashCode()); + } + + @Test + void testMixedContextEquality() { + Map map = Map.of("foo", new Value("bar")); + + EvaluationContext base = new MutableContext(null, map); + LayeredEvaluationContext layered = new LayeredEvaluationContext(null, null, null, base); + + assertEquals(base, layered); + assertEquals(layered, base); + assertEquals(base.hashCode(), layered.hashCode()); + } } }