Skip to content

Commit

Permalink
More config v6 tests (#102)
Browse files Browse the repository at this point in the history
* Add test for another special case of segment condition logging

* Add test for multi-level flag dependency

* Add tests for comparison attribute and comparison value trimming

* Use valid SDK keys in cache key generation tests

* Add tests for comparison attribute conversion to canonical string representation

* Simplify EvaluateContext + improve perf. of user attribute retrieval during evaluation + reduce allocations

* Set eslint as default formatter for VSCode workspace

* Minor corrections

* Move isStringArray into Utils

* Make line break char sequence configurable in log messages

* Align config json error handling of EvaluateLogBuilder with error reporting of RolloutEvaluator

* Improve naming

* Improve User Object tests

* Add a few more test cases

* Correct visibility of appendTargetingRuleThenPart

* Minor corrections

* Correct grammar mistake

* Make naming of UserComparator member consistent

* Add tests for some number parsing edge cases

* Adjust terminology to docs (eliminate the usage of term 'match' in the context of conditions)

* Bump version
  • Loading branch information
adams85 authored Mar 21, 2024
1 parent 68e2306 commit 547bc42
Show file tree
Hide file tree
Showing 22 changed files with 2,982 additions and 168 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"editor.rulers": [160],
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"eslint.format.enable": true,
"eslint.validate": [
"typescript"
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "configcat-common",
"version": "9.2.0",
"version": "9.3.0",
"description": "ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
11 changes: 9 additions & 2 deletions src/ConfigCatLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export interface IConfigCatLogger {
/** Gets the log level (the minimum level to use for filtering log events). */
readonly level?: LogLevel;

/** Gets the character sequence to use for line breaks in log messages. Defaults to "\n". */
readonly eol?: string;

/**
* Writes an event into the log.
* @param level Event severity level.
Expand All @@ -79,6 +82,10 @@ export class LoggerWrapper implements IConfigCatLogger {
return this.logger.level ?? LogLevel.Warn;
}

get eol(): string {
return this.logger.eol ?? "\n";
}

constructor(
private readonly logger: IConfigCatLogger,
private readonly hooks?: SafeHooksWrapper) {
Expand Down Expand Up @@ -369,7 +376,7 @@ export class ConfigCatConsoleLogger implements IConfigCatLogger {
/**
* Create an instance of ConfigCatConsoleLogger
*/
constructor(public level = LogLevel.Warn) {
constructor(public level = LogLevel.Warn, readonly eol = "\n") {
}

/** @inheritdoc */
Expand All @@ -381,7 +388,7 @@ export class ConfigCatConsoleLogger implements IConfigCatLogger {
level === LogLevel.Error ? [console.error, "ERROR"] :
[console.log, LogLevel[level].toUpperCase()];

const exceptionString = exception !== void 0 ? "\n" + errorToString(exception, true) : "";
const exceptionString = exception !== void 0 ? this.eol + errorToString(exception, true) : "";

logMethod(`${this.SOURCE} - ${levelString} - [${eventId}] ${message}${exceptionString}`);
}
Expand Down
104 changes: 52 additions & 52 deletions src/ConfigJson.ts

Large diffs are not rendered by default.

77 changes: 46 additions & 31 deletions src/EvaluateLogBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PrerequisiteFlagComparator, SegmentComparator, UserComparator } from "./ConfigJson";
import type { PrerequisiteFlagCondition, SegmentCondition, SettingValue, TargetingRule, UserCondition, UserConditionUnion } from "./ProjectConfig";
import type { PrerequisiteFlagCondition, SegmentCondition, Setting, SettingValue, TargetingRule, UserCondition, UserConditionUnion } from "./ProjectConfig";
import { isAllowedValue } from "./RolloutEvaluator";
import { formatStringList, isArray } from "./Utils";
import { formatStringList, isArray, isStringArray } from "./Utils";

const invalidValuePlaceholder = "<invalid value>";
const invalidNamePlaceholder = "<invalid name>";
Expand All @@ -14,6 +14,9 @@ export class EvaluateLogBuilder {
private log = "";
private indent = "";

constructor(private readonly eol: string) {
}

resetIndent(): this {
this.indent = "";
return this;
Expand All @@ -30,7 +33,7 @@ export class EvaluateLogBuilder {
}

newLine(text?: string): this {
this.log += "\n" + this.indent + (text ?? "");
this.log += this.eol + this.indent + (text ?? "");
return this;
}

Expand All @@ -43,20 +46,20 @@ export class EvaluateLogBuilder {
return this.log;
}

appendEvaluationResult(isMatch: boolean): this {
return this.append(`${isMatch}`);
}

private appendUserConditionCore(comparisonAttribute: string, comparator: UserComparator, comparisonValue?: unknown) {
return this.append(`User.${comparisonAttribute} ${formatUserComparator(comparator)} '${comparisonValue ?? invalidValuePlaceholder}'`);
}

private appendUserConditionString(comparisonAttribute: string, comparator: UserComparator, comparisonValue: string, isSensitive: boolean) {
if (typeof comparisonValue !== "string") {
return this.appendUserConditionCore(comparisonAttribute, comparator);
}

return this.appendUserConditionCore(comparisonAttribute, comparator, !isSensitive ? comparisonValue : "<hashed value>");
}

private appendUserConditionStringList(comparisonAttribute: string, comparator: UserComparator, comparisonValue: ReadonlyArray<string>, isSensitive: boolean): this {
if (comparisonValue == null) {
if (!isStringArray(comparisonValue)) {
return this.appendUserConditionCore(comparisonAttribute, comparator);
}

Expand All @@ -74,7 +77,7 @@ export class EvaluateLogBuilder {
}

private appendUserConditionNumber(comparisonAttribute: string, comparator: UserComparator, comparisonValue: number, isDateTime?: boolean) {
if (comparisonValue == null) {
if (typeof comparisonValue !== "number") {
return this.appendUserConditionCore(comparisonAttribute, comparator);
}

Expand All @@ -86,12 +89,14 @@ export class EvaluateLogBuilder {
}

appendUserCondition(condition: UserConditionUnion): this {
const { comparisonAttribute, comparator } = condition;
const comparisonAttribute = typeof condition.comparisonAttribute === "string" ? condition.comparisonAttribute : invalidNamePlaceholder;
const comparator = condition.comparator;

switch (condition.comparator) {
case UserComparator.IsOneOf:
case UserComparator.IsNotOneOf:
case UserComparator.ContainsAnyOf:
case UserComparator.NotContainsAnyOf:
case UserComparator.TextIsOneOf:
case UserComparator.TextIsNotOneOf:
case UserComparator.TextContainsAnyOf:
case UserComparator.TextNotContainsAnyOf:
case UserComparator.SemVerIsOneOf:
case UserComparator.SemVerIsNotOneOf:
case UserComparator.TextStartsWithAnyOf:
Expand All @@ -118,8 +123,8 @@ export class EvaluateLogBuilder {
case UserComparator.NumberGreaterOrEquals:
return this.appendUserConditionNumber(comparisonAttribute, comparator, condition.comparisonValue);

case UserComparator.SensitiveIsOneOf:
case UserComparator.SensitiveIsNotOneOf:
case UserComparator.SensitiveTextIsOneOf:
case UserComparator.SensitiveTextIsNotOneOf:
case UserComparator.SensitiveTextStartsWithAnyOf:
case UserComparator.SensitiveTextNotStartsWithAnyOf:
case UserComparator.SensitiveTextEndsWithAnyOf:
Expand All @@ -141,8 +146,12 @@ export class EvaluateLogBuilder {
}
}

appendPrerequisiteFlagCondition(condition: PrerequisiteFlagCondition): this {
const prerequisiteFlagKey = condition.prerequisiteFlagKey;
appendPrerequisiteFlagCondition(condition: PrerequisiteFlagCondition, settings: Readonly<{ [name: string]: Setting }>): this {
const prerequisiteFlagKey =
typeof condition.prerequisiteFlagKey !== "string" ? invalidNamePlaceholder :
!(condition.prerequisiteFlagKey in settings) ? invalidReferencePlaceholder :
condition.prerequisiteFlagKey;

const comparator = condition.comparator;
const comparisonValue = condition.comparisonValue;

Expand All @@ -153,18 +162,24 @@ export class EvaluateLogBuilder {
const segment = condition.segment;
const comparator = condition.comparator;

const segmentName = segment?.name ??
(segment == null ? invalidReferencePlaceholder : invalidNamePlaceholder);
const segmentName =
segment == null ? invalidReferencePlaceholder :
typeof segment.name !== "string" || !segment.name ? invalidNamePlaceholder :
segment.name;

return this.append(`User ${formatSegmentComparator(comparator)} '${segmentName}'`);
}

appendConditionConsequence(isMatch: boolean): this {
this.append(" => ").appendEvaluationResult(isMatch);
return isMatch ? this : this.append(", skipping the remaining AND conditions");
appendConditionResult(result: boolean): this {
return this.append(`${result}`);
}

appendConditionConsequence(result: boolean): this {
this.append(" => ").appendConditionResult(result);
return result ? this : this.append(", skipping the remaining AND conditions");
}

appendTargetingRuleThenPart(targetingRule: TargetingRule, newLine: boolean): this {
private appendTargetingRuleThenPart(targetingRule: TargetingRule, newLine: boolean): this {
(newLine ? this.newLine() : this.append(" "))
.append("THEN");

Expand All @@ -184,14 +199,14 @@ export class EvaluateLogBuilder {

export function formatUserComparator(comparator: UserComparator): string {
switch (comparator) {
case UserComparator.IsOneOf:
case UserComparator.SensitiveIsOneOf:
case UserComparator.TextIsOneOf:
case UserComparator.SensitiveTextIsOneOf:
case UserComparator.SemVerIsOneOf: return "IS ONE OF";
case UserComparator.IsNotOneOf:
case UserComparator.SensitiveIsNotOneOf:
case UserComparator.TextIsNotOneOf:
case UserComparator.SensitiveTextIsNotOneOf:
case UserComparator.SemVerIsNotOneOf: return "IS NOT ONE OF";
case UserComparator.ContainsAnyOf: return "CONTAINS ANY OF";
case UserComparator.NotContainsAnyOf: return "NOT CONTAINS ANY OF";
case UserComparator.TextContainsAnyOf: return "CONTAINS ANY OF";
case UserComparator.TextNotContainsAnyOf: return "NOT CONTAINS ANY OF";
case UserComparator.SemVerLess:
case UserComparator.NumberLess: return "<";
case UserComparator.SemVerLessOrEquals:
Expand Down Expand Up @@ -225,7 +240,7 @@ export function formatUserComparator(comparator: UserComparator): string {
}

export function formatUserCondition(condition: UserConditionUnion): string {
return new EvaluateLogBuilder().appendUserCondition(condition).toString();
return new EvaluateLogBuilder("").appendUserCondition(condition).toString();
}

export function formatPrerequisiteFlagComparator(comparator: PrerequisiteFlagComparator): string {
Expand Down
12 changes: 6 additions & 6 deletions src/ProjectConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,10 @@ export interface ICondition<TCondition extends keyof ConditionTypeMap = keyof Co
export type ConditionUnion = UserConditionUnion | PrerequisiteFlagCondition | SegmentCondition;

export type UserConditionComparisonValueTypeMap = {
[UserComparator.IsOneOf]: Readonly<string[]>;
[UserComparator.IsNotOneOf]: Readonly<string[]>;
[UserComparator.ContainsAnyOf]: Readonly<string[]>;
[UserComparator.NotContainsAnyOf]: Readonly<string[]>;
[UserComparator.TextIsOneOf]: Readonly<string[]>;
[UserComparator.TextIsNotOneOf]: Readonly<string[]>;
[UserComparator.TextContainsAnyOf]: Readonly<string[]>;
[UserComparator.TextNotContainsAnyOf]: Readonly<string[]>;
[UserComparator.SemVerIsOneOf]: Readonly<string[]>;
[UserComparator.SemVerIsNotOneOf]: Readonly<string[]>;
[UserComparator.SemVerLess]: string;
Expand All @@ -288,8 +288,8 @@ export type UserConditionComparisonValueTypeMap = {
[UserComparator.NumberLessOrEquals]: number;
[UserComparator.NumberGreater]: number;
[UserComparator.NumberGreaterOrEquals]: number;
[UserComparator.SensitiveIsOneOf]: Readonly<string[]>;
[UserComparator.SensitiveIsNotOneOf]: Readonly<string[]>;
[UserComparator.SensitiveTextIsOneOf]: Readonly<string[]>;
[UserComparator.SensitiveTextIsNotOneOf]: Readonly<string[]>;
[UserComparator.DateTimeBefore]: number;
[UserComparator.DateTimeAfter]: number;
[UserComparator.SensitiveTextEquals]: string;
Expand Down
Loading

0 comments on commit 547bc42

Please sign in to comment.