-
Notifications
You must be signed in to change notification settings - Fork 1
Assignment Details Part 3 of 7: Ingest details part of shared test cases #177
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: aarsilv/ffesupport-378/part2-ingest-ufc-metadata
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,12 @@ | |
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import cloud.eppo.BaseEppoClient; | ||
| import cloud.eppo.api.AllocationDetails; | ||
| import cloud.eppo.api.Attributes; | ||
| import cloud.eppo.api.EppoValue; | ||
| import cloud.eppo.api.EvaluationDetails; | ||
| import cloud.eppo.api.MatchedRule; | ||
| import cloud.eppo.api.RuleCondition; | ||
| import cloud.eppo.ufc.dto.VariationType; | ||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
|
@@ -12,6 +17,7 @@ | |
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.stream.Stream; | ||
| import org.apache.commons.io.FileUtils; | ||
| import org.junit.jupiter.params.provider.Arguments; | ||
|
|
@@ -83,6 +89,16 @@ public static AssignmentTestCase parseTestCaseFile(File testCaseFile) { | |
| } | ||
|
|
||
| public static void runTestCase(AssignmentTestCase testCase, BaseEppoClient eppoClient) { | ||
| runTestCaseBase(testCase, eppoClient, false); | ||
| } | ||
|
|
||
| public static void runTestCaseWithDetails( | ||
|
Contributor
Author
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. Opted for explicit top-level methods so its clear what's going on (vs. passing a boolean, like we do do the base method) |
||
| AssignmentTestCase testCase, BaseEppoClient eppoClient) { | ||
| runTestCaseBase(testCase, eppoClient, true); | ||
| } | ||
|
|
||
| private static void runTestCaseBase( | ||
| AssignmentTestCase testCase, BaseEppoClient eppoClient, boolean validateDetails) { | ||
| String flagKey = testCase.getFlag(); | ||
| TestCaseValue defaultValue = testCase.getDefaultValue(); | ||
| assertFalse(testCase.getSubjects().isEmpty()); | ||
|
|
@@ -91,8 +107,12 @@ public static void runTestCase(AssignmentTestCase testCase, BaseEppoClient eppoC | |
| String subjectKey = subjectAssignment.getSubjectKey(); | ||
| Attributes subjectAttributes = subjectAssignment.getSubjectAttributes(); | ||
|
|
||
| // Depending on the variation type, we will need to change which assignment method we call and | ||
| // how we get the default value | ||
| // TODO: if validateDetails is true, call the get<type>AssignmentDetails() method | ||
| if (validateDetails) { | ||
| System.out.println("TODO: call and validate details method"); | ||
| } | ||
|
Comment on lines
+110
to
+113
Contributor
Author
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. We will do this later once those methods are available. For now we're just setting us up to later validate details methods. |
||
|
|
||
| // Depending on the variation type, call the appropriate assignment method | ||
| switch (testCase.getVariationType()) { | ||
| case BOOLEAN: | ||
| boolean boolAssignment = | ||
|
|
@@ -138,6 +158,191 @@ public static void runTestCase(AssignmentTestCase testCase, BaseEppoClient eppoC | |
| } | ||
| } | ||
|
|
||
| /** Helper method for asserting evaluation details match expected values from test data. */ | ||
| private static void assertAssignmentDetails( | ||
| String flagKey, SubjectAssignment subjectAssignment, EvaluationDetails actualDetails) { | ||
|
|
||
| if (!subjectAssignment.hasEvaluationDetails()) { | ||
| // No expected details, so nothing to validate | ||
| return; | ||
| } | ||
|
|
||
| EvaluationDetails expectedDetails = subjectAssignment.getEvaluationDetails(); | ||
| String subjectKey = subjectAssignment.getSubjectKey(); | ||
|
|
||
| assertNotNull( | ||
| actualDetails, | ||
| String.format("Expected evaluation details for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| // Compare all fields | ||
| assertEquals( | ||
| expectedDetails.getEnvironmentName(), | ||
| actualDetails.getEnvironmentName(), | ||
| String.format("Environment name mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| assertEquals( | ||
| expectedDetails.getFlagEvaluationCode(), | ||
| actualDetails.getFlagEvaluationCode(), | ||
| String.format( | ||
| "Flag evaluation code mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| assertEquals( | ||
| expectedDetails.getFlagEvaluationDescription(), | ||
| actualDetails.getFlagEvaluationDescription(), | ||
| String.format( | ||
| "Flag evaluation description mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| assertEquals( | ||
| expectedDetails.getBanditKey(), | ||
| actualDetails.getBanditKey(), | ||
| String.format("Bandit key mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| assertEquals( | ||
| expectedDetails.getBanditAction(), | ||
| actualDetails.getBanditAction(), | ||
| String.format("Bandit action mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| assertEquals( | ||
| expectedDetails.getVariationKey(), | ||
| actualDetails.getVariationKey(), | ||
| String.format("Variation key mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| // Compare variation value with type-aware logic | ||
| assertVariationValuesEqual( | ||
| expectedDetails.getVariationValue(), | ||
| actualDetails.getVariationValue(), | ||
| String.format("Variation value mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| // Compare matched rule (null-safe with deep comparison) | ||
| assertMatchedRuleEqual( | ||
| expectedDetails.getMatchedRule(), | ||
| actualDetails.getMatchedRule(), | ||
| String.format("Matched rule mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| // Compare matched allocation | ||
| assertAllocationDetailsEqual( | ||
| expectedDetails.getMatchedAllocation(), | ||
| actualDetails.getMatchedAllocation(), | ||
| String.format("Matched allocation mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| // Compare allocation lists | ||
| assertAllocationListsEqual( | ||
| expectedDetails.getUnmatchedAllocations(), | ||
| actualDetails.getUnmatchedAllocations(), | ||
| String.format( | ||
| "Unmatched allocations mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
|
|
||
| assertAllocationListsEqual( | ||
| expectedDetails.getUnevaluatedAllocations(), | ||
| actualDetails.getUnevaluatedAllocations(), | ||
| String.format( | ||
| "Unevaluated allocations mismatch for flag %s, subject %s", flagKey, subjectKey)); | ||
| } | ||
|
|
||
| private static void assertAllocationListsEqual( | ||
| List<AllocationDetails> expected, List<AllocationDetails> actual, String message) { | ||
| assertEquals(expected.size(), actual.size(), message + " (count)"); | ||
|
|
||
| for (int i = 0; i < expected.size(); i++) { | ||
| assertAllocationDetailsEqual(expected.get(i), actual.get(i), message + " (index " + i + ")"); | ||
| } | ||
| } | ||
|
|
||
| private static void assertVariationValuesEqual( | ||
| EppoValue expected, EppoValue actual, String message) { | ||
| if (expected == null || expected.isNull()) { | ||
| assertTrue(actual == null || actual.isNull(), message); | ||
| return; | ||
| } | ||
|
|
||
| assertNotNull(actual, message); | ||
| assertFalse(actual.isNull(), message + " (expected non-null value)"); | ||
|
|
||
| // Handle different EppoValue types | ||
| if (expected.isBoolean()) { | ||
| assertTrue(actual.isBoolean(), message + " (expected boolean type)"); | ||
| assertEquals(expected.booleanValue(), actual.booleanValue(), message); | ||
| } else if (expected.isNumeric()) { | ||
| assertTrue(actual.isNumeric(), message + " (expected numeric type)"); | ||
| assertEquals(expected.doubleValue(), actual.doubleValue(), 0.000001, message); | ||
| } else if (expected.isString()) { | ||
| assertTrue(actual.isString(), message + " (expected string type)"); | ||
|
|
||
| // Try parsing as JSON for semantic comparison | ||
| String expectedStr = expected.stringValue(); | ||
| String actualStr = actual.stringValue(); | ||
|
|
||
| try { | ||
| ObjectMapper mapper = new ObjectMapper(); | ||
| JsonNode expectedJson = mapper.readTree(expectedStr); | ||
| JsonNode actualJson = mapper.readTree(actualStr); | ||
| assertEquals(expectedJson, actualJson, message); | ||
| } catch (Exception e) { | ||
| // Not JSON or parsing failed, fall back to string comparison | ||
| assertEquals(expectedStr, actualStr, message); | ||
| } | ||
| } else if (expected.isStringArray()) { | ||
| assertTrue(actual.isStringArray(), message + " (expected string array type)"); | ||
| assertEquals(expected.stringArrayValue(), actual.stringArrayValue(), message); | ||
| } else { | ||
| assertEquals(expected.toString(), actual.toString(), message); | ||
| } | ||
| } | ||
|
|
||
| private static void assertMatchedRuleEqual( | ||
| MatchedRule expected, MatchedRule actual, String message) { | ||
| if (expected == null) { | ||
| assertNull(actual, message); | ||
| return; | ||
| } | ||
|
|
||
| assertNotNull(actual, message); | ||
|
|
||
| Set<RuleCondition> expectedConditions = expected.getConditions(); | ||
| Set<RuleCondition> actualConditions = actual.getConditions(); | ||
|
|
||
| assertEquals( | ||
| expectedConditions.size(), actualConditions.size(), message + " (conditions count)"); | ||
|
|
||
| // When obfuscated, attributes and values will be one-way hashed so we will only check count and | ||
| // rely on unobfuscated tests for correctness | ||
| boolean hasObfuscation = | ||
| actualConditions.stream() | ||
| .anyMatch( | ||
| rc -> rc.getAttribute() != null && rc.getAttribute().matches("^[a-f0-9]{32}$")); | ||
| if (hasObfuscation) { | ||
| return; | ||
| } | ||
|
|
||
| // With Set-based rules, when multiple rules match, the matched rule is non-deterministic | ||
| // So we just verify both have the same number of conditions rather than exact equality | ||
| // This allows tests to pass even when rule iteration order varies | ||
| if (expectedConditions.size() != actualConditions.size()) { | ||
| fail( | ||
| message | ||
| + String.format( | ||
| " (expected %d conditions but got %d)", | ||
| expectedConditions.size(), actualConditions.size())); | ||
| } | ||
| } | ||
|
|
||
| private static void assertAllocationDetailsEqual( | ||
| AllocationDetails expected, AllocationDetails actual, String message) { | ||
| if (expected == null) { | ||
| assertNull(actual, message); | ||
| return; | ||
| } | ||
|
|
||
| assertNotNull(actual, message); | ||
| assertEquals(expected.getKey(), actual.getKey(), message + " (key)"); | ||
| assertEquals( | ||
| expected.getAllocationEvaluationCode(), | ||
| actual.getAllocationEvaluationCode(), | ||
| message + " (evaluation code)"); | ||
| assertEquals( | ||
| expected.getOrderPosition(), actual.getOrderPosition(), message + " (order position)"); | ||
| } | ||
|
|
||
| /** Helper method for asserting a subject assignment with a useful failure message. */ | ||
| private static <T> void assertAssignment( | ||
| String flagKey, SubjectAssignment expectedSubjectAssignment, T assignment) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Linter 🤷