Skip to content

Commit

Permalink
More v6 test (#47)
Browse files Browse the repository at this point in the history
* Small fixes and extra v6 tests
- segment and prerequisite evaluation tests
- comparison attribute conversion to canonical String test
- optimize getKeyAndValue logic
- fix error message
- fix cache deserialization

* Update version to 10.0.1

* Add test case for skipped sdkKey validation in case of LOCAL_ONLY OverrideBehavior.

* Update double format.
Add missing required variation ids to json overrides.

* Run code format

* Fixes based on code review.

* Move configSalt validation from deserialization.
Added platform based line ending.
Moved LogHelper into EvaluateLogger.

* Fix configSalt checks
  • Loading branch information
novalisdenahi authored Feb 13, 2024
1 parent 778e6c0 commit cf68865
Show file tree
Hide file tree
Showing 27 changed files with 1,283 additions and 269 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version=10.0.0
version=10.0.1

org.gradle.jvmargs=-Xmx2g
4 changes: 2 additions & 2 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.configcat">
package="com.configcat">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
14 changes: 8 additions & 6 deletions src/main/java/com/configcat/ConfigCatClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,12 @@ private <T> Map.Entry<String, T> getKeyAndValueFromSettingsMap(Class<T> classOfT
}

for (TargetingRule targetingRule : setting.getTargetingRules()) {
if (targetingRule.getSimpleValue() != null && variationId.equals(targetingRule.getSimpleValue().getVariationId())) {
return new AbstractMap.SimpleEntry<>(settingKey, (T) this.parseObject(classOfT, targetingRule.getSimpleValue().getValue(), setting.getType()));
}
if (targetingRule.getPercentageOptions() != null) {
if (targetingRule.getSimpleValue() != null) {
if (variationId.equals(targetingRule.getSimpleValue().getVariationId())) {
return new AbstractMap.SimpleEntry<>(settingKey, (T) this.parseObject(classOfT, targetingRule.getSimpleValue().getValue(), setting.getType()));

}
} else if (targetingRule.getPercentageOptions() != null) {
for (PercentageOption percentageRule : targetingRule.getPercentageOptions()) {
if (variationId.equals(percentageRule.getVariationId())) {
return new AbstractMap.SimpleEntry<>(settingKey, (T) this.parseObject(classOfT, percentageRule.getValue(), setting.getType()));
Expand Down Expand Up @@ -522,10 +524,10 @@ else if ((classOfT == Double.class || classOfT == double.class) && settingsValue
else if ((classOfT == Boolean.class || classOfT == boolean.class) && settingsValue.getBooleanValue() != null && SettingType.BOOLEAN.equals(settingType))
return settingsValue.getBooleanValue();

throw new IllegalArgumentException("The type of a setting must match the type of the setting's default value. "
throw new IllegalArgumentException("The type of a setting must match the type of the specified default value. "
+ "Setting's type was {" + settingType + "} but the default value's type was {" + classOfT + "}. "
+ "Please use a default value which corresponds to the setting type {" + settingType + "}."
+ "Learn more: https://configcat.com/docs/sdk-reference/dotnet/#setting-type-mapping");
+ "Learn more: https://configcat.com/docs/sdk-reference/android/#setting-type-mapping");
}

private Class<?> classBySettingType(SettingType settingType) {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/configcat/ConfigCatLogMessages.java
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public static String getUserObjectMissing(final String key) {
* @return The formatted warn message.
*/
public static String getUserAttributeMissing(final String key, final UserCondition userCondition, final String attributeName) {
return "Cannot evaluate condition (" + LogHelper.formatUserCondition(userCondition) + ") for setting '" + key + "' (the User." + attributeName + " attribute is missing). You should set the User." + attributeName + " attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/";
return "Cannot evaluate condition (" + EvaluateLogger.formatUserCondition(userCondition) + ") for setting '" + key + "' (the User." + attributeName + " attribute is missing). You should set the User." + attributeName + " attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/";
}

/**
Expand All @@ -224,7 +224,7 @@ public static String getUserAttributeMissing(final String key, final String attr
* @return The formatted warn message.
*/
public static String getUserAttributeInvalid(final String key, final UserCondition userCondition, final String reason, final String attributeName) {
return "Cannot evaluate condition (" + LogHelper.formatUserCondition(userCondition) + ") for setting '" + key + "' (" + reason + "). Please check the User." + attributeName + " attribute and make sure that its value corresponds to the comparison operator.";
return "Cannot evaluate condition (" + EvaluateLogger.formatUserCondition(userCondition) + ") for setting '" + key + "' (" + reason + "). Please check the User." + attributeName + " attribute and make sure that its value corresponds to the comparison operator.";
}

/**
Expand All @@ -237,7 +237,7 @@ public static String getUserAttributeInvalid(final String key, final UserConditi
* @return The formatted warn message.
*/
public static String getUserObjectAttributeIsAutoConverted(String key, UserCondition userCondition, String attributeName, String attributeValue) {
return "Evaluation of condition (" + LogHelper.formatUserCondition(userCondition) + ") for setting '" + key + "' may not produce the expected result (the User." + attributeName + " attribute is not a string value, thus it was automatically converted to the string value '" + attributeValue + "'). Please make sure that using a non-string value was intended.";
return "Evaluation of condition (" + EvaluateLogger.formatUserCondition(userCondition) + ") for setting '" + key + "' may not produce the expected result (the User." + attributeName + " attribute is not a string value, thus it was automatically converted to the string value '" + attributeValue + "'). Please make sure that using a non-string value was intended.";
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/configcat/ConfigService.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class ConfigService implements Closeable {
private String cachedEntryString = "";
private Entry cachedEntry = Entry.EMPTY;
private CompletableFuture<Result<Entry>> runningTask;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final AtomicBoolean offline;
private final AtomicBoolean closed = new AtomicBoolean(false);
private final String cacheKey;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/configcat/Entry.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public static Entry fromString(String cacheValue) throws IllegalArgumentExceptio
throw new IllegalArgumentException("Empty config jsom value.");
}
try {
Config config = Utils.gson.fromJson(configJson, Config.class);
Config config = Utils.deserializeConfig(configJson);
return new Entry(config, eTag, configJson, fetchTimeUnixMillis);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid config JSON content: " + configJson);
Expand Down
175 changes: 169 additions & 6 deletions src/main/java/com/configcat/EvaluateLogger.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
package com.configcat;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public class EvaluateLogger {

private static final String HASHED_VALUE = "<hashed value>";
public static final String INVALID_VALUE = "<invalid value>";
public static final String INVALID_NAME = "<invalid name>";
public static final String INVALID_REFERENCE = "<invalid reference>";

private static final int MAX_LIST_ELEMENT = 10;
private final StringBuilder stringBuilder = new StringBuilder();

public EvaluateLogger(LogLevel logLevel) {
Expand Down Expand Up @@ -90,7 +103,7 @@ public final void newLine() {
if (!isLoggable) {
return;
}
stringBuilder.append("\n");
stringBuilder.append(System.lineSeparator());
for (int i = 0; i < indentLevel; i++) {
stringBuilder.append(" ");
}
Expand Down Expand Up @@ -170,7 +183,7 @@ public void logPercentageEvaluationReturnValue(int hashValue, int i, int percent
if (!isLoggable) {
return;
}
String percentageOptionValue = settingsValue != null ? settingsValue.toString() : LogHelper.INVALID_VALUE;
String percentageOptionValue = settingsValue != null ? settingsValue.toString() : INVALID_VALUE;
newLine();
append("- Hash value " + hashValue + " selects % option " + (i + 1) + " (" + percentage + "%), '" + percentageOptionValue + "'.");
}
Expand All @@ -194,7 +207,7 @@ public void logSegmentEvaluationResult(SegmentCondition segmentCondition, Segmen
String segmentResultComparator = segmentResult ? SegmentComparator.IS_IN_SEGMENT.getName() : SegmentComparator.IS_NOT_IN_SEGMENT.getName();
append("Segment evaluation result: User " + segmentResultComparator + ".");
newLine();
append("Condition (" + LogHelper.formatSegmentFlagCondition(segmentCondition, segment) + ") evaluates to " + result + ".");
append("Condition (" + formatSegmentFlagCondition(segmentCondition, segment) + ") evaluates to " + result + ".");
decreaseIndentLevel();
newLine();
append(")");
Expand All @@ -208,7 +221,7 @@ public void logSegmentEvaluationError(SegmentCondition segmentCondition, Segment

append("Segment evaluation result: " + error + ".");
newLine();
append("Condition (" + LogHelper.formatSegmentFlagCondition(segmentCondition, segment) + ") failed to evaluate.");
append("Condition (" + formatSegmentFlagCondition(segmentCondition, segment) + ") failed to evaluate.");
decreaseIndentLevel();
newLine();
append(")");
Expand All @@ -230,12 +243,162 @@ public void logPrerequisiteFlagEvaluationResult(PrerequisiteFlagCondition prereq
return;
}
newLine();
String prerequisiteFlagValueFormat = prerequisiteFlagValue != null ? prerequisiteFlagValue.toString() : LogHelper.INVALID_VALUE;
String prerequisiteFlagValueFormat = prerequisiteFlagValue != null ? prerequisiteFlagValue.toString() : INVALID_VALUE;
append("Prerequisite flag evaluation result: '" + prerequisiteFlagValueFormat + "'.");
newLine();
append("Condition (" + LogHelper.formatPrerequisiteFlagCondition(prerequisiteFlagCondition) + ") evaluates to " + result + ".");
append("Condition (" + formatPrerequisiteFlagCondition(prerequisiteFlagCondition) + ") evaluates to " + result + ".");
decreaseIndentLevel();
newLine();
append(")");
}

private static String formatStringListComparisonValue(String[] comparisonValue, boolean isSensitive) {
if (comparisonValue == null) {
return INVALID_VALUE;
}
List<String> comparisonValues = new ArrayList<>(Arrays.asList(comparisonValue));
if (comparisonValues.isEmpty()) {
return INVALID_VALUE;
}
String formattedList;
if (isSensitive) {
String sensitivePostFix = comparisonValues.size() == 1 ? "value" : "values";
formattedList = "<" + comparisonValues.size() + " hashed " + sensitivePostFix + ">";
} else {
String listPostFix = "";
if (comparisonValues.size() > MAX_LIST_ELEMENT) {
int count = comparisonValues.size() - MAX_LIST_ELEMENT;
String countPostFix = count == 1 ? "value" : "values";
listPostFix = ", ... <" + count + " more " + countPostFix + ">";
}
List<String> subList = comparisonValues.subList(0, Math.min(MAX_LIST_ELEMENT, comparisonValues.size()));
StringBuilder formatListBuilder = new StringBuilder();
int subListSize = subList.size();
for (int i = 0; i < subListSize; i++) {
formatListBuilder.append("'").append(subList.get(i)).append("'");
if (i != subListSize - 1) {
formatListBuilder.append(", ");
}
}
formatListBuilder.append(listPostFix);
formattedList = formatListBuilder.toString();
}

return "[" + formattedList + "]";
}

private static String formatStringComparisonValue(String comparisonValue, boolean isSensitive) {
return "'" + (isSensitive ? HASHED_VALUE : comparisonValue) + "'";
}

private static String formatDoubleComparisonValue(Double comparisonValue, boolean isDate) {
if (comparisonValue == null) {
return INVALID_VALUE;
}
DecimalFormat decimalFormat = new DecimalFormat("0.#####");
decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.UK));
if (isDate) {
return "'" + decimalFormat.format(comparisonValue) + "' (" + DateTimeUtils.doubleToFormattedUTC(comparisonValue) + " UTC)";
}
return "'" + decimalFormat.format(comparisonValue) + "'";
}
public static String formatUserCondition(UserCondition userCondition) {
UserComparator userComparator = UserComparator.fromId(userCondition.getComparator());
if (userComparator == null) {
throw new IllegalArgumentException("Comparison operator is invalid.");
}
String comparisonValue;
switch (userComparator) {
case IS_ONE_OF:
case IS_NOT_ONE_OF:
case CONTAINS_ANY_OF:
case NOT_CONTAINS_ANY_OF:
case SEMVER_IS_ONE_OF:
case SEMVER_IS_NOT_ONE_OF:
case TEXT_STARTS_WITH:
case TEXT_NOT_STARTS_WITH:
case TEXT_ENDS_WITH:
case TEXT_NOT_ENDS_WITH:
case TEXT_ARRAY_CONTAINS:
case TEXT_ARRAY_NOT_CONTAINS:
comparisonValue = formatStringListComparisonValue(userCondition.getStringArrayValue(), false);
break;
case SEMVER_LESS:
case SEMVER_LESS_EQUALS:
case SEMVER_GREATER:
case SEMVER_GREATER_EQUALS:
case TEXT_EQUALS:
case TEXT_NOT_EQUALS:
comparisonValue = formatStringComparisonValue(userCondition.getStringValue(), false);
break;
case NUMBER_EQUALS:
case NUMBER_NOT_EQUALS:
case NUMBER_LESS:
case NUMBER_LESS_EQUALS:
case NUMBER_GREATER:
case NUMBER_GREATER_EQUALS:
comparisonValue = formatDoubleComparisonValue(userCondition.getDoubleValue(), false);
break;
case SENSITIVE_IS_ONE_OF:
case SENSITIVE_IS_NOT_ONE_OF:
case HASHED_STARTS_WITH:
case HASHED_NOT_STARTS_WITH:
case HASHED_ENDS_WITH:
case HASHED_NOT_ENDS_WITH:
case HASHED_ARRAY_CONTAINS:
case HASHED_ARRAY_NOT_CONTAINS:
comparisonValue = formatStringListComparisonValue(userCondition.getStringArrayValue(), true);
break;
case DATE_BEFORE:
case DATE_AFTER:
comparisonValue = formatDoubleComparisonValue(userCondition.getDoubleValue(), true);
break;
case HASHED_EQUALS:
case HASHED_NOT_EQUALS:
comparisonValue = formatStringComparisonValue(userCondition.getStringValue(), true);
break;
default:
comparisonValue = INVALID_VALUE;
}

return "User." + userCondition.getComparisonAttribute() + " " + userComparator.getName() + " " + comparisonValue;
}

public static String formatSegmentFlagCondition(SegmentCondition segmentCondition, Segment segment) {
String segmentName;
if (segment != null) {
segmentName = segment.getName();
if (segmentName == null || segmentName.isEmpty()) {
segmentName = INVALID_NAME;
}
} else {
segmentName = INVALID_REFERENCE;
}
SegmentComparator segmentComparator = SegmentComparator.fromId(segmentCondition.getSegmentComparator());
if (segmentComparator == null) {
throw new IllegalArgumentException("Segment comparison operator is invalid.");
}
return "User " + segmentComparator.getName() + " '" + segmentName + "'";
}

public static String formatPrerequisiteFlagCondition(PrerequisiteFlagCondition prerequisiteFlagCondition) {
String prerequisiteFlagKey = prerequisiteFlagCondition.getPrerequisiteFlagKey();
PrerequisiteComparator prerequisiteComparator = PrerequisiteComparator.fromId(prerequisiteFlagCondition.getPrerequisiteComparator());
if (prerequisiteComparator == null) {
throw new IllegalArgumentException("Prerequisite Flag comparison operator is invalid.");
}
SettingsValue prerequisiteValue = prerequisiteFlagCondition.getValue();
String comparisonValue = prerequisiteValue == null ? INVALID_VALUE : prerequisiteValue.toString();
return "Flag '" + prerequisiteFlagKey + "' " + prerequisiteComparator.getName() + " '" + comparisonValue + "'";
}


public static String formatCircularDependencyList(List<String> visitedKeys, String key) {
StringBuilder builder = new StringBuilder();
for (String visitedKey : visitedKeys) {
builder.append("'").append(visitedKey).append("' -> ");
}
builder.append("'").append(key).append("'");
return builder.toString();
}
}
Loading

0 comments on commit cf68865

Please sign in to comment.