Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/main/java/cloud/eppo/api/EvaluationDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public class EvaluationDetails {
private final List<AllocationDetails> unmatchedAllocations;
private final List<AllocationDetails> unevaluatedAllocations;


Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linter 🤷

public EvaluationDetails(
String environmentName,
Date configFetchedAt,
Expand Down
209 changes: 207 additions & 2 deletions src/test/java/cloud/eppo/helpers/AssignmentTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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());
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 =
Expand Down Expand Up @@ -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) {
Expand Down
Loading