Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@ public static enum BigSegmentsStatus {
private final ErrorKind errorKind;
private final Exception exception;
private final BigSegmentsStatus bigSegmentsStatus;
private final boolean overrideAffected;

private EvaluationReason(Kind kind, int ruleIndex, String ruleId, String prerequisiteKey, boolean inExperiment,
ErrorKind errorKind, Exception exception, BigSegmentsStatus bigSegmentsStatus) {
ErrorKind errorKind, Exception exception, BigSegmentsStatus bigSegmentsStatus, boolean overrideAffected) {
this.kind = kind;
this.ruleIndex = ruleIndex;
this.ruleId = ruleId;
Expand All @@ -153,18 +154,19 @@ private EvaluationReason(Kind kind, int ruleIndex, String ruleId, String prerequ
this.errorKind = errorKind;
this.exception = exception;
this.bigSegmentsStatus = bigSegmentsStatus;
this.overrideAffected = overrideAffected;
}

private EvaluationReason(Kind kind) {
this(kind, -1, null, null, NOT_IN_EXPERIMENT, null, null, null);
this(kind, -1, null, null, NOT_IN_EXPERIMENT, null, null, null, false);
}

private EvaluationReason(Kind kind, boolean inExperiment) {
this(kind, -1, null, null, inExperiment, null, null, null);
this(kind, -1, null, null, inExperiment, null, null, null, false);
}

private EvaluationReason(ErrorKind errorKind, Exception exception) {
this(Kind.ERROR, -1, null, null, NOT_IN_EXPERIMENT, errorKind, exception, null);
this(Kind.ERROR, -1, null, null, NOT_IN_EXPERIMENT, errorKind, exception, null, false);
}

/**
Expand Down Expand Up @@ -267,7 +269,41 @@ public BigSegmentsStatus getBigSegmentsStatus() {
*/
public EvaluationReason withBigSegmentsStatus(BigSegmentsStatus bigSegmentsStatus) {
return new EvaluationReason(kind, ruleIndex, ruleId, prerequisiteKey, inExperiment, errorKind,
exception, bigSegmentsStatus);
exception, bigSegmentsStatus, overrideAffected);
}

/**
* Whether an override affected this evaluation, directly or transitively. Returns true if the
* evaluated flag came from the SDK's override store. Also returns true if a prerequisite flag at
* any depth, or a segment read during the evaluation, came from that store. Otherwise it returns
* false.
* <p>
* In the JSON representation, the {@code overrideAffected} property appears only when this
* value is true.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @return whether an override affected the evaluation
*/
public boolean isOverrideAffected() {
return overrideAffected;
}

/**
* Returns a copy of this {@link EvaluationReason} with a specific value for
* {@link #isOverrideAffected()}. The copy keeps all other properties of the original reason.
* <p>
* Flag overrides are currently experimental and subject to change.
*
* @param overrideAffected the new property value
* @return a reason object with the given value, which is this same instance if the value is unchanged
*/
public EvaluationReason withOverrideAffected(boolean overrideAffected) {
if (this.overrideAffected == overrideAffected) {
return this;
}
return new EvaluationReason(kind, ruleIndex, ruleId, prerequisiteKey, inExperiment, errorKind,
exception, bigSegmentsStatus, overrideAffected);
}

/**
Expand Down Expand Up @@ -305,15 +341,16 @@ public boolean equals(Object other) {
inExperiment == o.inExperiment &&
Objects.equals(errorKind, o.errorKind) &&
Objects.equals(exception, o.exception) &&
Objects.equals(bigSegmentsStatus, o.bigSegmentsStatus);
Objects.equals(bigSegmentsStatus, o.bigSegmentsStatus) &&
overrideAffected == o.overrideAffected;
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(kind, ruleIndex, ruleId, prerequisiteKey, inExperiment, errorKind,
exception, bigSegmentsStatus);
exception, bigSegmentsStatus, overrideAffected);
}

/**
Expand Down Expand Up @@ -377,7 +414,7 @@ public static EvaluationReason ruleMatch(int ruleIndex, String ruleId) {
* @return a reason object
*/
public static EvaluationReason ruleMatch(int ruleIndex, String ruleId, boolean inExperiment) {
return new EvaluationReason(Kind.RULE_MATCH, ruleIndex, ruleId, null, inExperiment, null, null, null);
return new EvaluationReason(Kind.RULE_MATCH, ruleIndex, ruleId, null, inExperiment, null, null, null, false);
}

/**
Expand All @@ -387,7 +424,7 @@ public static EvaluationReason ruleMatch(int ruleIndex, String ruleId, boolean i
* @return a reason object
*/
public static EvaluationReason prerequisiteFailed(String prerequisiteKey) {
return new EvaluationReason(Kind.PREREQUISITE_FAILED, -1, null, prerequisiteKey, NOT_IN_EXPERIMENT, null, null, null);
return new EvaluationReason(Kind.PREREQUISITE_FAILED, -1, null, prerequisiteKey, NOT_IN_EXPERIMENT, null, null, null, false);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ static EvaluationReason parse(JsonReader reader) throws IOException {
boolean inExperiment = false;
EvaluationReason.ErrorKind errorKind = null;
EvaluationReason.BigSegmentsStatus bigSegmentsStatus = null;
boolean overrideAffected = false;

reader.beginObject();
while (reader.peek() != JsonToken.END_OBJECT) {
Expand All @@ -51,6 +52,9 @@ static EvaluationReason parse(JsonReader reader) throws IOException {
case "bigSegmentsStatus":
bigSegmentsStatus = readEnum(EvaluationReason.BigSegmentsStatus.class, reader);
break;
case "overrideAffected":
overrideAffected = reader.nextBoolean();
break;
default:
reader.skipValue(); // ignore any unexpected property
}
Expand Down Expand Up @@ -85,9 +89,9 @@ static EvaluationReason parse(JsonReader reader) throws IOException {
return null;
}
if (bigSegmentsStatus != null) {
return reason.withBigSegmentsStatus(bigSegmentsStatus);
reason = reason.withBigSegmentsStatus(bigSegmentsStatus);
}
return reason;
return reason.withOverrideAffected(overrideAffected);
}

@Override
Expand Down Expand Up @@ -134,6 +138,12 @@ public void write(JsonWriter writer, EvaluationReason reason) throws IOException
writer.name("bigSegmentsStatus");
writer.value(reason.getBigSegmentsStatus().name());
}

// The override marker is written only when it is true, like inExperiment.
if (reason.isOverrideAffected()) {
writer.name("overrideAffected");
writer.value(true);
}

writer.endObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
import static com.launchdarkly.sdk.EvaluationReason.Kind.RULE_MATCH;
import static com.launchdarkly.sdk.EvaluationReason.Kind.TARGET_MATCH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static java.util.Arrays.asList;

import org.junit.Test;
Expand Down Expand Up @@ -84,6 +87,67 @@ public void bigSegmentsStatus() {
assertNull(reason.getBigSegmentsStatus());
}

@Test
public void overrideAffectedIsFalseByDefault() {
assertFalse(EvaluationReason.off().isOverrideAffected());
assertFalse(EvaluationReason.fallthrough().isOverrideAffected());
assertFalse(EvaluationReason.fallthrough(true).isOverrideAffected());
assertFalse(EvaluationReason.targetMatch().isOverrideAffected());
assertFalse(EvaluationReason.ruleMatch(1, "id").isOverrideAffected());
assertFalse(EvaluationReason.prerequisiteFailed("key").isOverrideAffected());
assertFalse(EvaluationReason.error(FLAG_NOT_FOUND).isOverrideAffected());
assertFalse(EvaluationReason.exception(new Exception("sorry")).isOverrideAffected());
assertFalse(EvaluationReason.fallthrough().withBigSegmentsStatus(HEALTHY).isOverrideAffected());
}

@Test
public void withOverrideAffectedKeepsOtherProperties() {
EvaluationReason reason = EvaluationReason.ruleMatch(2, "id", true).withBigSegmentsStatus(STALE);
EvaluationReason marked = reason.withOverrideAffected(true);

assertTrue(marked.isOverrideAffected());
assertFalse(reason.isOverrideAffected());
assertEquals(RULE_MATCH, marked.getKind());
assertEquals(2, marked.getRuleIndex());
assertEquals("id", marked.getRuleId());
assertTrue(marked.isInExperiment());
assertEquals(STALE, marked.getBigSegmentsStatus());

Exception e = new Exception("sorry");
EvaluationReason markedError = EvaluationReason.exception(e).withOverrideAffected(true);
assertEquals(ERROR, markedError.getKind());
assertEquals(EvaluationReason.ErrorKind.EXCEPTION, markedError.getErrorKind());
assertEquals(e, markedError.getException());
assertTrue(markedError.isOverrideAffected());

// The marking survives a later change of another property.
assertTrue(marked.withBigSegmentsStatus(HEALTHY).isOverrideAffected());
}

@Test
public void withOverrideAffectedReturnsSameInstanceWhenUnchanged() {
EvaluationReason reason = EvaluationReason.off();
assertSame(reason, reason.withOverrideAffected(false));
EvaluationReason marked = reason.withOverrideAffected(true);
assertSame(marked, marked.withOverrideAffected(true));
assertFalse(marked.withOverrideAffected(false).isOverrideAffected());
}

@Test
public void overrideAffectedParticipatesInEquality() {
EvaluationReason plain = EvaluationReason.off();
EvaluationReason marked = plain.withOverrideAffected(true);
assertNotEquals(plain, marked);
assertEquals(marked, plain.withOverrideAffected(true));
assertEquals(marked.hashCode(), plain.withOverrideAffected(true).hashCode());
}

@Test
public void overrideAffectedDoesNotChangeStringRepresentation() {
assertEquals("OFF", EvaluationReason.off().withOverrideAffected(true).toString());
assertEquals("RULE_MATCH(1,id)", EvaluationReason.ruleMatch(1, "id").withOverrideAffected(true).toString());
}

@Test
public void simpleStringRepresentations() {
assertEquals("OFF", EvaluationReason.off().toString());
Expand Down Expand Up @@ -129,7 +193,10 @@ public void equalInstancesAreEqual() {
asList(EvaluationReason.ruleMatch(2, "id1"), EvaluationReason.ruleMatch(2, "id1")),
asList(EvaluationReason.prerequisiteFailed("a"), EvaluationReason.prerequisiteFailed("a")),
asList(EvaluationReason.error(CLIENT_NOT_READY), EvaluationReason.error(CLIENT_NOT_READY)),
asList(EvaluationReason.error(WRONG_TYPE), EvaluationReason.error(WRONG_TYPE))
asList(EvaluationReason.error(WRONG_TYPE), EvaluationReason.error(WRONG_TYPE)),
asList(EvaluationReason.off().withOverrideAffected(true), EvaluationReason.off().withOverrideAffected(true)),
asList(EvaluationReason.error(WRONG_TYPE).withOverrideAffected(true),
EvaluationReason.error(WRONG_TYPE).withOverrideAffected(true))
);
TestHelpers.doEqualityTests(testValues);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ public void reasonJsonSerializations() throws Exception {
verifyDeserializeInvalidJson(EvaluationReason.class, "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":3}");
}

@Test
public void overrideAffectedIsSerializedOnlyWhenTrue() throws Exception {
verifySerializeAndDeserialize(EvaluationReason.off().withOverrideAffected(true),
"{\"kind\":\"OFF\",\"overrideAffected\":true}");
verifySerializeAndDeserialize(EvaluationReason.fallthrough(true).withOverrideAffected(true),
"{\"kind\":\"FALLTHROUGH\",\"inExperiment\":true,\"overrideAffected\":true}");
verifySerializeAndDeserialize(EvaluationReason.ruleMatch(1, "id").withOverrideAffected(true),
"{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":\"id\",\"overrideAffected\":true}");
verifySerializeAndDeserialize(EvaluationReason.prerequisiteFailed("key").withOverrideAffected(true),
"{\"kind\":\"PREREQUISITE_FAILED\",\"prerequisiteKey\":\"key\",\"overrideAffected\":true}");
verifySerializeAndDeserialize(EvaluationReason.error(EvaluationReason.ErrorKind.MALFORMED_FLAG).withOverrideAffected(true),
"{\"kind\":\"ERROR\",\"errorKind\":\"MALFORMED_FLAG\",\"overrideAffected\":true}");
verifySerializeAndDeserialize(EvaluationReason.fallthrough().withBigSegmentsStatus(HEALTHY).withOverrideAffected(true),
"{\"kind\":\"FALLTHROUGH\",\"bigSegmentsStatus\":\"HEALTHY\",\"overrideAffected\":true}");

// A false value is never written, and an explicit false parses the same as an absent property.
verifySerialize(EvaluationReason.off().withOverrideAffected(false), "{\"kind\":\"OFF\"}");
verifyDeserialize(EvaluationReason.off(), "{\"kind\":\"OFF\",\"overrideAffected\":false}");
}

@Test
public void errorSerializationWithException() throws Exception {
// We do *not* want the JSON representation to include the exception, because that is used in events, and
Expand Down
Loading