diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/SOURCE.md b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/SOURCE.md index 1360501bfd61..50cafec9f4be 100644 --- a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/SOURCE.md +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/SOURCE.md @@ -3,7 +3,7 @@ These files are copied from the canonical FFE fixture repository. Canonical source: https://github.com/DataDog/ffe-system-test-data -Source commit: ea8b5cc5ce335109f11f3efbc5fd608f98a3ca54 +Source commit: d9b05ae23d861228e210b9f4b7b1e5a926f88878 Do not edit these fixtures directly in dd-trace-dotnet. Add or update shared FFE behavior in ffe-system-test-data first, then refresh this snapshot. diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-regex-flag.json b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-regex-flag.json index 13ea0c3ce3bf..b01f6f1e5ca8 100644 --- a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-regex-flag.json +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-regex-flag.json @@ -1,4 +1,43 @@ [ + { + "attributes": { + "comma_regex_case": "xxaaay" + }, + "defaultValue": "none", + "flag": "regex-flag", + "result": { + "reason": "TARGETING_MATCH", + "value": "comma-quantifier" + }, + "targetingKey": "comma-quantifier", + "variationType": "STRING" + }, + { + "attributes": { + "whitespace_regex_case": "prefix exact suffix" + }, + "defaultValue": "none", + "flag": "regex-flag", + "result": { + "reason": "TARGETING_MATCH", + "value": "literal-whitespace" + }, + "targetingKey": "literal-whitespace", + "variationType": "STRING" + }, + { + "attributes": { + "whitespace_regex_case": "exact" + }, + "defaultValue": "none", + "flag": "regex-flag", + "result": { + "reason": "DEFAULT", + "value": "none" + }, + "targetingKey": "trimmed-whitespace-does-not-match", + "variationType": "STRING" + }, { "attributes": { "email": "user.name+tag@capture.example" diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-invalid-comparand-categories.json b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-invalid-comparand-categories.json new file mode 100644 index 000000000000..4f00961d9f96 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-invalid-comparand-categories.json @@ -0,0 +1,120 @@ +[ + { + "description": "A non-parseable SemVer comparand (\"not-a-version\") rejects the flag at parse time with a PARSE_ERROR rather than matching or coercing.", + "attributes": { + "app_version": "1.2.3" + }, + "defaultValue": "unknown", + "flag": "semver-invalid-comparand-syntax-test", + "result": { + "errorCode": "PARSE_ERROR", + "reason": "ERROR", + "value": "unknown" + }, + "targetingKey": "invalid-syntax", + "variationType": "STRING" + }, + { + "description": "A SemVer comparand with repeated hyphens inside its prerelease identifiers (\"1.2.3----R-S.12.9.1--.12+meta\") is valid and matches normally.", + "attributes": { + "app_version": "1.2.3----R-S.12.9.1--.12+meta" + }, + "defaultValue": "unknown", + "flag": "semver-valid-comparand-hyphenated-prerelease-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "matched" + }, + "targetingKey": "valid-hyphenated-prerelease", + "variationType": "STRING" + }, + { + "description": "A SemVer comparand with consecutive dots in its prerelease identifiers (\"1.0.0-alpha..1\") rejects the flag at parse time with a PARSE_ERROR.", + "attributes": { + "app_version": "1.0.0-alpha.1" + }, + "defaultValue": "unknown", + "flag": "semver-invalid-comparand-consecutive-dots-test", + "result": { + "errorCode": "PARSE_ERROR", + "reason": "ERROR", + "value": "unknown" + }, + "targetingKey": "invalid-consecutive-dots", + "variationType": "STRING" + }, + { + "description": "A Ruby-style dot-suffixed SemVer comparand (\"1.2.3.DEV\") rejects the flag at parse time with a PARSE_ERROR.", + "attributes": { + "app_version": "1.2.3" + }, + "defaultValue": "unknown", + "flag": "semver-invalid-comparand-dot-suffix-test", + "result": { + "errorCode": "PARSE_ERROR", + "reason": "ERROR", + "value": "unknown" + }, + "targetingKey": "invalid-dot-suffix", + "variationType": "STRING" + }, + { + "description": "A v-prefixed SemVer comparand (\"v1.2.3\") rejects the flag at parse time with a PARSE_ERROR.", + "attributes": { + "app_version": "1.2.3" + }, + "defaultValue": "unknown", + "flag": "semver-invalid-comparand-vprefix-test", + "result": { + "errorCode": "PARSE_ERROR", + "reason": "ERROR", + "value": "unknown" + }, + "targetingKey": "invalid-vprefix", + "variationType": "STRING" + }, + { + "description": "A leading-zero core component in the SemVer comparand (\"01.2.3\") rejects the flag at parse time with a PARSE_ERROR.", + "attributes": { + "app_version": "1.2.3" + }, + "defaultValue": "unknown", + "flag": "semver-invalid-comparand-leading-zero-test", + "result": { + "errorCode": "PARSE_ERROR", + "reason": "ERROR", + "value": "unknown" + }, + "targetingKey": "invalid-leading-zero", + "variationType": "STRING" + }, + { + "description": "A non-string (JSON number) SemVer comparand rejects the flag at parse time with a PARSE_ERROR rather than coercing the number to a string.", + "attributes": { + "app_version": "1.2.3" + }, + "defaultValue": "unknown", + "flag": "semver-invalid-comparand-non-string-test", + "result": { + "errorCode": "PARSE_ERROR", + "reason": "ERROR", + "value": "unknown" + }, + "targetingKey": "invalid-non-string", + "variationType": "STRING" + }, + { + "description": "The rejected invalid-comparand flags must not poison the rest of the configuration: a neighboring valid SemVer flag still evaluates normally.", + "attributes": { + "app_version": "2.3.4" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "stable" + }, + "targetingKey": "after-invalid-categories", + "variationType": "STRING" + } +] diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-invalid-comparand-waterfall.json b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-invalid-comparand-waterfall.json new file mode 100644 index 000000000000..cbddbb87d033 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-invalid-comparand-waterfall.json @@ -0,0 +1,17 @@ +[ + { + "description": "An invalid configured SemVer comparand in a non-last allocation must reject the whole flag at parse time, even though a valid default allocation follows it in the waterfall. An implementation that skips the invalid allocation and falls through to the default would return \"fallback\" with a STATIC reason instead of an error.", + "attributes": { + "app_version": "1.2.3" + }, + "defaultValue": "unknown", + "flag": "semver-invalid-comparand-waterfall-test", + "result": { + "errorCode": "PARSE_ERROR", + "reason": "ERROR", + "value": "unknown" + }, + "targetingKey": "waterfall", + "variationType": "STRING" + } +] diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-precedence-flag.json b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-precedence-flag.json new file mode 100644 index 000000000000..ef027a066607 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-precedence-flag.json @@ -0,0 +1,170 @@ +[ + { + "description": "Multi-digit numeric prerelease identifiers compare by numeric value, not lexicographically: 1.0.0-beta.2 must precede 1.0.0-beta.11. An implementation that string-compares identifiers would treat \"2\" as greater than \"11\" and fail to match.", + "attributes": { + "app_version": "10.0.0-beta.2" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "numeric-prerelease" + }, + "targetingKey": "numeric-prerelease", + "variationType": "STRING" + }, + { + "description": "Build metadata is ignored for equality: 1.0.0+linux and 1.0.0+darwin have equal precedence, so SEMVER_EQ matches.", + "attributes": { + "app_version": "20.0.0+linux" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "build-equal-precedence" + }, + "targetingKey": "build-equal-precedence", + "variationType": "STRING" + }, + { + "description": "Dotted/multi-segment build metadata is ignored for equality: 4.0.0+exp.sha.5114f85 equals 4.0.0.", + "attributes": { + "app_version": "30.0.0+exp.sha.5114f85" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "build-dotted-eq" + }, + "targetingKey": "build-dotted-eq", + "variationType": "STRING" + }, + { + "description": "Build metadata is ignored for SEMVER_LTE: 4.0.0+build.42 <= 4.0.0 is true (match).", + "attributes": { + "app_version": "40.0.0+build.42" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "build-lte" + }, + "targetingKey": "build-lte", + "variationType": "STRING" + }, + { + "description": "Build metadata is ignored for SEMVER_GTE: 4.0.0+build.42 >= 4.0.0 is true (match).", + "attributes": { + "app_version": "50.0.0+build.42" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "build-gte" + }, + "targetingKey": "build-gte", + "variationType": "STRING" + }, + { + "description": "Build metadata is ignored for SEMVER_NEQ: 4.0.0+build.42 != 4.0.0 is false (no match), so the evaluation falls through to the default.", + "attributes": { + "app_version": "60.0.0+build.42" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "build-neq", + "variationType": "STRING" + }, + { + "description": "Build metadata is ignored for SEMVER_LT: 4.0.0+build.42 < 4.0.0 is false (no match), so the evaluation falls through to the default.", + "attributes": { + "app_version": "70.0.0+build.42" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "build-lt", + "variationType": "STRING" + }, + { + "description": "Build metadata is ignored for SEMVER_GT: 4.0.0+build.42 > 4.0.0 is false (no match), so the evaluation falls through to the default.", + "attributes": { + "app_version": "80.0.0+build.42" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "build-gt", + "variationType": "STRING" + }, + { + "description": "Alphanumeric prerelease ordering: 1.0.0-alpha precedes 1.0.0-alpha.1 (fewer identifiers first), so SEMVER_LT matches.", + "attributes": { + "app_version": "90.0.0-alpha" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "alpha-before-alpha-1" + }, + "targetingKey": "alpha-before-alpha-1", + "variationType": "STRING" + }, + { + "description": "Alphanumeric prerelease ordering: numeric identifiers have lower precedence than alphanumeric (1.0.0-alpha.1 precedes 1.0.0-alpha.beta), so SEMVER_LT matches.", + "attributes": { + "app_version": "91.0.0-alpha.1" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "alpha-1-before-alpha-beta" + }, + "targetingKey": "alpha-1-before-alpha-beta", + "variationType": "STRING" + }, + { + "description": "Alphanumeric prerelease ordering: identifiers are compared lexicographically (1.0.0-alpha.beta precedes 1.0.0-beta), so SEMVER_LT matches.", + "attributes": { + "app_version": "92.0.0-alpha.beta" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "alpha-beta-before-beta" + }, + "targetingKey": "alpha-beta-before-beta", + "variationType": "STRING" + }, + { + "description": "Alphanumeric prerelease ordering: a prerelease version precedes the corresponding release (1.0.0-beta precedes 1.0.0), so SEMVER_LT matches.", + "attributes": { + "app_version": "93.0.0-beta" + }, + "defaultValue": "unknown", + "flag": "semver-precedence-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "beta-before-release" + }, + "targetingKey": "beta-before-release", + "variationType": "STRING" + } +] diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-validation-flag.json b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-validation-flag.json index 9fa90622c0c0..9b4bfea37102 100644 --- a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-validation-flag.json +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-validation-flag.json @@ -13,20 +13,6 @@ "targetingKey": "max-core", "variationType": "STRING" }, - { - "description": "Semver requires all three core components.", - "attributes": { - "app_version": "1.2" - }, - "defaultValue": "unknown", - "flag": "semver-comparison-test", - "result": { - "reason": "DEFAULT", - "value": "unknown" - }, - "targetingKey": "short-version", - "variationType": "STRING" - }, { "description": "Semver does not accept a v prefix.", "attributes": { @@ -111,6 +97,146 @@ "targetingKey": "non-ascii-prerelease", "variationType": "STRING" }, + { + "description": "An empty string is not a valid SemVer version.", + "attributes": { + "app_version": "" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "empty-string", + "variationType": "STRING" + }, + { + "description": "A leading zero in the minor core component is rejected.", + "attributes": { + "app_version": "1.02.3" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "minor-leading-zero", + "variationType": "STRING" + }, + { + "description": "A leading zero in the patch core component is rejected.", + "attributes": { + "app_version": "1.2.03" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "patch-leading-zero", + "variationType": "STRING" + }, + { + "description": "A trailing prerelease delimiter with no identifiers is rejected.", + "attributes": { + "app_version": "1.2.3-" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "empty-prerelease", + "variationType": "STRING" + }, + { + "description": "A trailing build delimiter with no identifiers is rejected.", + "attributes": { + "app_version": "1.2.3+" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "empty-build", + "variationType": "STRING" + }, + { + "description": "Underscores are not permitted in prerelease identifiers.", + "attributes": { + "app_version": "1.2.3-alpha_1" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "underscore-prerelease", + "variationType": "STRING" + }, + { + "description": "More than one build metadata delimiter is rejected.", + "attributes": { + "app_version": "1.2.3-alpha+build+other" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "multiple-build-delimiters", + "variationType": "STRING" + }, + { + "description": "Leading whitespace is not accepted by the SemVer parser.", + "attributes": { + "app_version": " 1.2.3" + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "leading-whitespace", + "variationType": "STRING" + }, + { + "description": "Trailing whitespace is not accepted by the SemVer parser.", + "attributes": { + "app_version": "1.2.3 " + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "trailing-whitespace", + "variationType": "STRING" + }, + { + "description": "A non-string attribute (JSON number) does not match any SemVer condition and falls through to the default rather than erroring.", + "attributes": { + "app_version": 1.2 + }, + "defaultValue": "unknown", + "flag": "semver-comparison-test", + "result": { + "reason": "DEFAULT", + "value": "unknown" + }, + "targetingKey": "non-string-attribute", + "variationType": "STRING" + }, { "description": "A flag with an invalid configured SemVer comparand returns a parse error while the rest of the configuration remains usable.", "attributes": { diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-version-parts.json b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-version-parts.json new file mode 100644 index 000000000000..bac7fdade7bb --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/evaluation-cases/test-case-semver-version-parts.json @@ -0,0 +1,118 @@ +[ + { + "description": "A one-part version is normalized with missing minor and patch parts: \"18\" compares equal to \"18.0.0\".", + "attributes": { + "scenario": "one-part-input", + "app_version": "18" + }, + "defaultValue": "unknown", + "flag": "semver-version-parts-input-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "one-part-input" + }, + "targetingKey": "one-part-input", + "variationType": "STRING" + }, + { + "description": "A one-part configured comparand is valid and normalized: \"18.0.0\" compares equal to configured \"18\".", + "attributes": { + "app_version": "18.0.0" + }, + "defaultValue": "unknown", + "flag": "semver-version-parts-one-part-comparand-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "one-part-comparand" + }, + "targetingKey": "one-part-comparand", + "variationType": "STRING" + }, + { + "description": "A two-part version is normalized with a missing patch part: \"18.0\" compares equal to \"18.0.0\".", + "attributes": { + "scenario": "two-part-input", + "app_version": "18.0" + }, + "defaultValue": "unknown", + "flag": "semver-version-parts-input-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "two-part-input" + }, + "targetingKey": "two-part-input", + "variationType": "STRING" + }, + { + "description": "A two-part configured comparand is valid and normalized: \"18.0.0\" compares equal to configured \"18.0\".", + "attributes": { + "app_version": "18.0.0" + }, + "defaultValue": "unknown", + "flag": "semver-version-parts-two-part-comparand-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "two-part-comparand" + }, + "targetingKey": "two-part-comparand", + "variationType": "STRING" + }, + { + "description": "A four-part version is accepted as an attribute and compares above a canonical three-part lower version.", + "attributes": { + "scenario": "four-part-input", + "app_version": "18.0.0.0" + }, + "defaultValue": "unknown", + "flag": "semver-version-parts-input-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "four-part-input" + }, + "targetingKey": "four-part-input", + "variationType": "STRING" + }, + { + "description": "A four-part configured comparand is accepted and compares below a canonical three-part higher version.", + "attributes": { + "app_version": "19.0.0" + }, + "defaultValue": "unknown", + "flag": "semver-version-parts-four-part-comparand-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "four-part-comparand" + }, + "targetingKey": "four-part-comparand", + "variationType": "STRING" + }, + { + "description": "A five-part version is accepted as an attribute and compares above a canonical three-part lower version.", + "attributes": { + "scenario": "five-part-input", + "app_version": "18.0.0.0.0" + }, + "defaultValue": "unknown", + "flag": "semver-version-parts-input-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "five-part-input" + }, + "targetingKey": "five-part-input", + "variationType": "STRING" + }, + { + "description": "A five-part configured comparand is accepted and compares below a canonical three-part higher version.", + "attributes": { + "app_version": "19.0.0" + }, + "defaultValue": "unknown", + "flag": "semver-version-parts-five-part-comparand-test", + "result": { + "reason": "TARGETING_MATCH", + "value": "five-part-comparand" + }, + "targetingKey": "five-part-comparand", + "variationType": "STRING" + } +] diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/targeting-regex-conformance.json b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/targeting-regex-conformance.json new file mode 100644 index 000000000000..64cd10bcc569 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/targeting-regex-conformance.json @@ -0,0 +1,1065 @@ +{ + "schema": "datadog.ffe.targeting-regex-conformance/v1", + "schemaVersion": 1, + "contractVersion": "targeting-regex-v2", + "semantics": { + "acceptedSyntax": "The portable intersection exercised by accepted cases, not every construct accepted by an individual engine.", + "engines": [ + "RE2JS 1.2.2 using normalizedPattern", + "Go regexp using normalizedPattern", + "Rust regex 1.12 using rawPattern", + "Rust regex-syntax and regex-automata using rawPattern" + ], + "matchMode": "unanchored search unless the pattern contains anchors", + "nativeCompileFailureEvaluationResult": false, + "normalization": "For the inline flag groups exercised below, remove only the u flag and preserve the tested i, m, and s flags. Remove an empty global group and replace an empty scoped group with a non-capturing group.", + "contract": "accepted or rejected is the portable authoring decision. It is separate from native engine compilation.", + "expectedCompile": "The common native compile result. It is null when engines disagree; engineExpectations then records each result.", + "expectedMatch": "The common unanchored evaluation result. It is null when engines disagree or there is no portable match result.", + "engineExpectations": "When present, records native Go, RE2JS, Rust rules-based, and Rust rkyv compile and match observations. Match is null when that engine cannot compile the pattern." + }, + "portableSyntax": { + "accepted": [ + "literals and escaped literals", + "dot", + "start and end anchors", + "alternation", + "capturing and non-capturing groups", + "positive and negated character classes without a leading literal closing bracket", + "star, plus, optional, and counted quantifiers", + "Unicode literals", + "case-insensitive, multiline, dot-all, and standalone normalized Unicode inline flags" + ], + "rejected": [ + "malformed expressions", + "positive and negative lookahead", + "positive and negative lookbehind", + "numeric and named backreferences", + "octal escapes and quoted-literal escapes that are not accepted consistently", + "Perl shorthand classes and word boundaries with ASCII versus Unicode semantic differences", + "unsupported byte escapes", + "Rust-only character class set operators and nested non-POSIX character classes", + "inline Unicode flag forms whose raw Rust meaning differs from normalized Go and browser syntax", + "named capture groups", + "POSIX character classes", + "Unicode property character classes", + "combined or scoped Unicode inline flags", + "character classes with a leading literal closing bracket", + "atomic groups", + "possessive quantifiers" + ] + }, + "cases": [ + { + "id": "accepted-literal-unanchored-match", + "description": "A literal searches within a larger string.", + "category": "literal", + "contract": "accepted", + "rawPattern": "datadog", + "normalizedPattern": "datadog", + "expectedCompile": true, + "input": "visit datadoghq.com", + "expectedMatch": true + }, + { + "id": "accepted-literal-unanchored-no-match", + "description": "A valid literal can compile without matching the input.", + "category": "literal", + "contract": "accepted", + "rawPattern": "datadog", + "normalizedPattern": "datadog", + "expectedCompile": true, + "input": "example.com", + "expectedMatch": false + }, + { + "id": "accepted-escaped-literal-dot-no-match", + "description": "An escaped dot is a literal dot, not any character.", + "category": "escape", + "contract": "accepted", + "rawPattern": "datadoghq\\.com", + "normalizedPattern": "datadoghq\\.com", + "expectedCompile": true, + "input": "datadoghqXcom", + "expectedMatch": false + }, + { + "id": "accepted-dot-match", + "description": "Dot matches one non-newline character.", + "category": "dot", + "contract": "accepted", + "rawPattern": "d.g", + "normalizedPattern": "d.g", + "expectedCompile": true, + "input": "dog", + "expectedMatch": true + }, + { + "id": "accepted-anchors-full-match", + "description": "Start and end anchors require a full-string match.", + "category": "anchors", + "contract": "accepted", + "rawPattern": "^foo$", + "normalizedPattern": "^foo$", + "expectedCompile": true, + "input": "foo", + "expectedMatch": true + }, + { + "id": "accepted-anchors-reject-substring", + "description": "Anchors prevent a substring-only match.", + "category": "anchors", + "contract": "accepted", + "rawPattern": "^foo$", + "normalizedPattern": "^foo$", + "expectedCompile": true, + "input": "prefix foo suffix", + "expectedMatch": false + }, + { + "id": "accepted-alternation-capturing-group", + "description": "Alternation inside a capturing group is portable.", + "category": "alternation", + "contract": "accepted", + "rawPattern": "^(cat|dog)$", + "normalizedPattern": "^(cat|dog)$", + "expectedCompile": true, + "input": "dog", + "expectedMatch": true + }, + { + "id": "accepted-non-capturing-group", + "description": "A non-capturing group is portable.", + "category": "group", + "contract": "accepted", + "rawPattern": "^(?:cat|dog)$", + "normalizedPattern": "^(?:cat|dog)$", + "expectedCompile": true, + "input": "cat", + "expectedMatch": true + }, + { + "id": "rejected-python-style-named-group", + "description": "Python-style named captures are not accepted consistently by shipped SDK evaluators.", + "category": "named-group", + "contract": "rejected", + "rawPattern": "^(?P[[:alpha:]]+)$", + "normalizedPattern": "^(?P[[:alpha:]]+)$", + "expectedCompile": true, + "input": "Dog", + "expectedMatch": true + }, + { + "id": "rejected-js-style-named-group", + "description": "JS-style named captures are not evaluated consistently by shipped SDK evaluators.", + "category": "named-group", + "contract": "rejected", + "rawPattern": "^(?[[:alpha:]]+)$", + "normalizedPattern": "^(?[[:alpha:]]+)$", + "expectedCompile": true, + "input": "Dog", + "expectedMatch": true + }, + { + "id": "accepted-escaped-octal-like-literal", + "description": "A doubled backslash followed by digits is literal text, not an octal escape.", + "category": "literal-escape", + "contract": "accepted", + "rawPattern": "^\\\\123$", + "normalizedPattern": "^\\\\123$", + "expectedCompile": true, + "input": "\\123", + "expectedMatch": true + }, + { + "id": "accepted-escaped-shorthand-like-literal", + "description": "A doubled backslash followed by d is literal text, not a digit shorthand.", + "category": "literal-escape", + "contract": "accepted", + "rawPattern": "^\\\\d$", + "normalizedPattern": "^\\\\d$", + "expectedCompile": true, + "input": "\\d", + "expectedMatch": true + }, + { + "id": "accepted-escaped-quoted-literal-like-text", + "description": "A doubled backslash before Q is literal text, not the start of a quoted-literal escape.", + "category": "literal-escape", + "contract": "accepted", + "rawPattern": "^\\\\Qfoo$", + "normalizedPattern": "^\\\\Qfoo$", + "expectedCompile": true, + "input": "\\Qfoo", + "expectedMatch": true + }, + { + "id": "accepted-escaped-named-group-punctuation", + "description": "Escaped punctuation that spells (?< is literal text, not a named capture.", + "category": "literal-escape", + "contract": "accepted", + "rawPattern": "^\\(\\?<$", + "normalizedPattern": "^\\(\\?<$", + "expectedCompile": true, + "input": "(?<", + "expectedMatch": true + }, + { + "id": "accepted-named-group-punctuation-in-character-class", + "description": "The characters (?< inside a character class are literals.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[(?<]+$", + "normalizedPattern": "^[(?<]+$", + "expectedCompile": true, + "input": "(?<", + "expectedMatch": true + }, + { + "id": "accepted-escaped-shorthand-in-character-class", + "description": "A doubled backslash before d inside a character class is literal syntax, not a digit shorthand.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[\\\\d]$", + "normalizedPattern": "^[\\\\d]$", + "expectedCompile": true, + "input": "d", + "expectedMatch": true + }, + { + "id": "accepted-escaped-boundary-in-character-class", + "description": "A doubled backslash before b inside a character class is literal syntax, not a word boundary.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[\\\\b]$", + "normalizedPattern": "^[\\\\b]$", + "expectedCompile": true, + "input": "b", + "expectedMatch": true + }, + { + "id": "accepted-positive-character-class", + "description": "A positive character class is portable.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[A-Z][0-9]$", + "normalizedPattern": "^[A-Z][0-9]$", + "expectedCompile": true, + "input": "Q7", + "expectedMatch": true + }, + { + "id": "accepted-negated-character-class", + "description": "A negated character class is portable.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[^0-9]+$", + "normalizedPattern": "^[^0-9]+$", + "expectedCompile": true, + "input": "alpha", + "expectedMatch": true + }, + { + "id": "accepted-star-quantifier", + "description": "The star quantifier accepts zero repetitions.", + "category": "quantifier", + "contract": "accepted", + "rawPattern": "^ab*c$", + "normalizedPattern": "^ab*c$", + "expectedCompile": true, + "input": "ac", + "expectedMatch": true + }, + { + "id": "accepted-plus-quantifier-no-match", + "description": "The plus quantifier requires one or more repetitions.", + "category": "quantifier", + "contract": "accepted", + "rawPattern": "^ab+c$", + "normalizedPattern": "^ab+c$", + "expectedCompile": true, + "input": "ac", + "expectedMatch": false + }, + { + "id": "accepted-optional-quantifier", + "description": "The optional quantifier accepts a missing character.", + "category": "quantifier", + "contract": "accepted", + "rawPattern": "^colou?r$", + "normalizedPattern": "^colou?r$", + "expectedCompile": true, + "input": "color", + "expectedMatch": true + }, + { + "id": "accepted-comma-counted-quantifier", + "description": "A comma inside a counted quantifier remains part of one regex value.", + "category": "quantifier", + "contract": "accepted", + "rawPattern": "a{1,3}", + "normalizedPattern": "a{1,3}", + "expectedCompile": true, + "input": "caaad", + "expectedMatch": true + }, + { + "id": "rejected-posix-alpha-class", + "description": "POSIX character classes are not evaluated consistently by shipped SDK evaluators.", + "category": "posix-class", + "contract": "rejected", + "rawPattern": "^[[:alpha:]]+$", + "normalizedPattern": "^[[:alpha:]]+$", + "expectedCompile": true, + "input": "Alpha", + "expectedMatch": true + }, + { + "id": "rejected-unicode-letter-class", + "description": "Unicode property character classes are not evaluated consistently by shipped SDK evaluators.", + "category": "unicode-class", + "contract": "rejected", + "rawPattern": "^\\p{L}+$", + "normalizedPattern": "^\\p{L}+$", + "expectedCompile": true, + "input": "mañana", + "expectedMatch": true + }, + { + "id": "accepted-unicode-literal-unanchored", + "description": "Unicode literal text is portable with unanchored search.", + "category": "unicode-literal", + "contract": "accepted", + "rawPattern": "mañana", + "normalizedPattern": "mañana", + "expectedCompile": true, + "input": "hasta mañana!", + "expectedMatch": true + }, + { + "id": "accepted-inline-case-insensitive", + "description": "The case-insensitive inline flag is portable.", + "category": "inline-flag", + "contract": "accepted", + "rawPattern": "(?i)^alice$", + "normalizedPattern": "(?i)^alice$", + "expectedCompile": true, + "input": "ALICE", + "expectedMatch": true + }, + { + "id": "accepted-inline-multiline", + "description": "The multiline inline flag changes anchors to line boundaries.", + "category": "inline-flag", + "contract": "accepted", + "rawPattern": "(?m)^second$", + "normalizedPattern": "(?m)^second$", + "expectedCompile": true, + "input": "first\nsecond\nthird", + "expectedMatch": true + }, + { + "id": "accepted-inline-dot-all", + "description": "The dot-all inline flag lets dot match a newline.", + "category": "inline-flag", + "contract": "accepted", + "rawPattern": "(?s)^a.b$", + "normalizedPattern": "(?s)^a.b$", + "expectedCompile": true, + "input": "a\nb", + "expectedMatch": true + }, + { + "id": "accepted-inline-unicode-global-normalized", + "description": "A global Unicode flag is redundant and removed for Go and RE2JS.", + "category": "normalization", + "contract": "accepted", + "rawPattern": "(?u)foo", + "normalizedPattern": "foo", + "expectedCompile": true, + "input": "seafood", + "expectedMatch": true + }, + { + "id": "rejected-inline-unicode-combined-normalized", + "description": "Combined Unicode inline flags are not normalized consistently by shipped SDK evaluators.", + "category": "normalization", + "contract": "rejected", + "rawPattern": "(?iu)^alice$", + "normalizedPattern": "(?i)^alice$", + "expectedCompile": true, + "input": "ALICE", + "expectedMatch": true + }, + { + "id": "rejected-inline-unicode-scoped-normalized", + "description": "Scoped Unicode inline flags are not normalized consistently by shipped SDK evaluators.", + "category": "normalization", + "contract": "rejected", + "rawPattern": "(?u:^mañana$)", + "normalizedPattern": "(?:^mañana$)", + "expectedCompile": true, + "input": "mañana", + "expectedMatch": true + }, + { + "id": "rejected-inline-unicode-scoped-combined-normalized", + "description": "Combined scoped Unicode inline flags are not normalized consistently by shipped SDK evaluators.", + "category": "normalization", + "contract": "rejected", + "rawPattern": "(?iu:^alice$)", + "normalizedPattern": "(?i:^alice$)", + "expectedCompile": true, + "input": "ALICE", + "expectedMatch": true + }, + { + "id": "accepted-single-ampersand-in-character-class", + "description": "A single ampersand in an ordinary character class is literal text, not a Rust set-intersection operator.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[a&]+$", + "normalizedPattern": "^[a&]+$", + "expectedCompile": true, + "input": "&", + "expectedMatch": true + }, + { + "id": "accepted-single-tilde-in-character-class", + "description": "A single tilde in an ordinary character class is literal text, not a Rust set-symmetric-difference operator.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[a~]+$", + "normalizedPattern": "^[a~]+$", + "expectedCompile": true, + "input": "~", + "expectedMatch": true + }, + { + "id": "accepted-single-hyphen-in-character-class", + "description": "A trailing single hyphen in a character class is literal text, not a Rust set-difference operator.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[a-]+$", + "normalizedPattern": "^[a-]+$", + "expectedCompile": true, + "input": "-", + "expectedMatch": true + }, + { + "id": "accepted-range-in-character-class", + "description": "An ordinary character range remains part of the portable contract.", + "category": "character-class", + "contract": "accepted", + "rawPattern": "^[a-z]+$", + "normalizedPattern": "^[a-z]+$", + "expectedCompile": true, + "input": "range", + "expectedMatch": true + }, + { + "id": "rejected-leading-closing-bracket-in-character-class", + "description": "A leading literal closing bracket in a character class is not evaluated consistently by shipped SDK evaluators.", + "category": "character-class", + "contract": "rejected", + "rawPattern": "^[]a]+$", + "normalizedPattern": "^[]a]+$", + "expectedCompile": true, + "input": "]", + "expectedMatch": true + }, + { + "id": "rejected-leading-closing-bracket-in-negated-character-class", + "description": "A leading literal closing bracket in a negated character class is not evaluated consistently by shipped SDK evaluators.", + "category": "character-class", + "contract": "rejected", + "rawPattern": "^[^]a]+$", + "normalizedPattern": "^[^]a]+$", + "expectedCompile": true, + "input": "b", + "expectedMatch": true + }, + { + "id": "rejected-rust-character-class-intersection", + "description": "Double ampersand is a set-intersection operator in Rust but literal class text in Go and RE2JS.", + "category": "character-class-dialect", + "contract": "rejected", + "rawPattern": "^[a-z&&b-z]+$", + "normalizedPattern": "^[a-z&&b-z]+$", + "expectedCompile": true, + "input": "&", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": true, "match": false }, + "rustRkyv": { "compile": true, "match": false } + } + }, + { + "id": "rejected-rust-character-class-intersection-after-leading-closing-bracket", + "description": "A leading closing bracket stays inside the class; the later double ampersand is a Rust intersection operator but literal class text in Go and RE2JS.", + "category": "character-class-dialect", + "contract": "rejected", + "rawPattern": "^[]a&&b]+$", + "normalizedPattern": "^[]a&&b]+$", + "expectedCompile": true, + "input": "&", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": true, "match": false }, + "rustRkyv": { "compile": true, "match": false } + } + }, + { + "id": "rejected-rust-character-class-intersection-after-leading-closing-bracket-in-negated-class", + "description": "A leading closing bracket after negation stays inside the class; Rust complements an empty intersection while Go and RE2JS treat ampersands as excluded literals.", + "category": "character-class-dialect", + "contract": "rejected", + "rawPattern": "^[^]a&&b]+$", + "normalizedPattern": "^[^]a&&b]+$", + "expectedCompile": true, + "input": "&", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": false }, + "re2js": { "compile": true, "match": false }, + "rustRulesBased": { "compile": true, "match": true }, + "rustRkyv": { "compile": true, "match": true } + } + }, + { + "id": "rejected-rust-character-class-symmetric-difference", + "description": "Double tilde is a set-symmetric-difference operator in Rust but literal class text in Go and RE2JS.", + "category": "character-class-dialect", + "contract": "rejected", + "rawPattern": "^[a-z~~b-z]+$", + "normalizedPattern": "^[a-z~~b-z]+$", + "expectedCompile": true, + "input": "~", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": true, "match": false }, + "rustRkyv": { "compile": true, "match": false } + } + }, + { + "id": "rejected-rust-character-class-difference", + "description": "Double hyphen is a set-difference operator in Rust but literal class text in Go and RE2JS.", + "category": "character-class-dialect", + "contract": "rejected", + "rawPattern": "^[a-z--b-z]+$", + "normalizedPattern": "^[a-z--b-z]+$", + "expectedCompile": true, + "input": "-", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": true, "match": false }, + "rustRkyv": { "compile": true, "match": false } + } + }, + { + "id": "rejected-rust-nested-character-class", + "description": "Rust treats a nested class as a union, while Go and RE2JS parse the same bytes with different match behavior.", + "category": "character-class-dialect", + "contract": "rejected", + "rawPattern": "^[a-z[0-9]]+$", + "normalizedPattern": "^[a-z[0-9]]+$", + "expectedCompile": true, + "input": "5", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": false }, + "re2js": { "compile": true, "match": false }, + "rustRulesBased": { "compile": true, "match": true }, + "rustRkyv": { "compile": true, "match": true } + } + }, + { + "id": "rejected-inline-flag-token-in-character-class", + "description": "The raw class contains a literal u, but current Go and browser normalization removes it as if it were an inline flag.", + "category": "normalization-dialect", + "contract": "rejected", + "rawPattern": "^[(?iu)]+$", + "normalizedPattern": "^[(?i)]+$", + "expectedCompile": true, + "input": "u", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": false }, + "re2js": { "compile": true, "match": false }, + "rustRulesBased": { "compile": true, "match": true }, + "rustRkyv": { "compile": true, "match": true } + } + }, + { + "id": "rejected-malformed-inline-unicode-flags", + "description": "The raw Rust flag group is malformed, while current Go and browser normalization removes it and accepts the remaining literal.", + "category": "normalization-dialect", + "contract": "rejected", + "rawPattern": "(?u-)foo", + "normalizedPattern": "foo", + "expectedCompile": null, + "input": "foo", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": false, "match": null }, + "rustRkyv": { "compile": false, "match": null } + } + }, + { + "id": "rejected-disabled-unicode-scoped-class", + "description": "Rust rejects a Unicode class inside a Unicode-disabled scope, while normalization removes the disabling flag and accepts it.", + "category": "normalization-dialect", + "contract": "rejected", + "rawPattern": "(?-u:\\p{L}+)", + "normalizedPattern": "(?:\\p{L}+)", + "expectedCompile": null, + "input": "ñ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": false, "match": null }, + "rustRkyv": { "compile": false, "match": null } + } + }, + { + "id": "rejected-disabled-unicode-global-class", + "description": "Rust rejects a Unicode class after Unicode is disabled globally, while normalization removes the disabling flag and accepts it.", + "category": "normalization-dialect", + "contract": "rejected", + "rawPattern": "(?-u)\\p{L}+", + "normalizedPattern": "\\p{L}+", + "expectedCompile": null, + "input": "ñ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": false, "match": null }, + "rustRkyv": { "compile": false, "match": null } + } + }, + { + "id": "rejected-disabled-unicode-negated-class", + "description": "Rust rejects a broad negated class inside a Unicode-disabled scope, while normalization removes the disabling flag and accepts it.", + "category": "normalization-dialect", + "contract": "rejected", + "rawPattern": "(?-u:[^a])", + "normalizedPattern": "(?:[^a])", + "expectedCompile": null, + "input": "ñ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": false, "match": null }, + "rustRkyv": { "compile": false, "match": null } + } + }, + { + "id": "rejected-octal-escape", + "description": "Octal escapes are accepted by Go and RE2JS but disabled by default in Rust, so they are outside the portable contract.", + "category": "dialect-escape", + "contract": "rejected", + "rawPattern": "\\123", + "normalizedPattern": "\\123", + "expectedCompile": null, + "input": "S", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": false, "match": null }, + "rustRkyv": { "compile": false, "match": null } + } + }, + { + "id": "rejected-quoted-literal-escape", + "description": "Quoted-literal escapes are accepted by Go and RE2JS but rejected by Rust, so they are outside the portable contract.", + "category": "dialect-escape", + "contract": "rejected", + "rawPattern": "\\Qfoo.bar\\E", + "normalizedPattern": "\\Qfoo.bar\\E", + "expectedCompile": null, + "input": "foo.bar", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": false, "match": null }, + "rustRkyv": { "compile": false, "match": null } + } + }, + { + "id": "rejected-byte-escape", + "description": "The byte escape is unsupported by the targeting regex engines and falls through to native compiler rejection.", + "category": "dialect-escape", + "contract": "rejected", + "rawPattern": "\\C", + "normalizedPattern": "\\C", + "expectedCompile": false, + "input": "A", + "expectedMatch": false + }, + { + "id": "rejected-digit-shorthand-unicode-divergence", + "description": "Digit shorthand is ASCII-oriented in Go and RE2JS but Unicode-aware in Rust.", + "category": "semantic-shorthand", + "contract": "rejected", + "rawPattern": "^\\d+$", + "normalizedPattern": "^\\d+$", + "expectedCompile": true, + "input": "٣", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": false }, + "re2js": { "compile": true, "match": false }, + "rustRulesBased": { "compile": true, "match": true }, + "rustRkyv": { "compile": true, "match": true } + } + }, + { + "id": "rejected-negated-digit-shorthand-unicode-divergence", + "description": "Negated digit shorthand is ASCII-oriented in Go and RE2JS but Unicode-aware in Rust.", + "category": "semantic-shorthand", + "contract": "rejected", + "rawPattern": "^\\D+$", + "normalizedPattern": "^\\D+$", + "expectedCompile": true, + "input": "٣", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": true, "match": false }, + "rustRkyv": { "compile": true, "match": false } + } + }, + { + "id": "rejected-space-shorthand-unicode-divergence", + "description": "Space shorthand excludes non-breaking space in Go and RE2JS but includes it in Rust.", + "category": "semantic-shorthand", + "contract": "rejected", + "rawPattern": "^\\s+$", + "normalizedPattern": "^\\s+$", + "expectedCompile": true, + "input": " ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": false }, + "re2js": { "compile": true, "match": false }, + "rustRulesBased": { "compile": true, "match": true }, + "rustRkyv": { "compile": true, "match": true } + } + }, + { + "id": "rejected-negated-space-shorthand-unicode-divergence", + "description": "Negated space shorthand includes non-breaking space in Go and RE2JS but excludes it in Rust.", + "category": "semantic-shorthand", + "contract": "rejected", + "rawPattern": "^\\S+$", + "normalizedPattern": "^\\S+$", + "expectedCompile": true, + "input": " ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": true, "match": false }, + "rustRkyv": { "compile": true, "match": false } + } + }, + { + "id": "rejected-word-shorthand-unicode-divergence", + "description": "Word shorthand excludes a non-ASCII letter in Go and RE2JS but includes it in Rust.", + "category": "semantic-shorthand", + "contract": "rejected", + "rawPattern": "^\\w+$", + "normalizedPattern": "^\\w+$", + "expectedCompile": true, + "input": "ñ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": false }, + "re2js": { "compile": true, "match": false }, + "rustRulesBased": { "compile": true, "match": true }, + "rustRkyv": { "compile": true, "match": true } + } + }, + { + "id": "rejected-negated-word-shorthand-unicode-divergence", + "description": "Negated word shorthand includes a non-ASCII letter in Go and RE2JS but excludes it in Rust.", + "category": "semantic-shorthand", + "contract": "rejected", + "rawPattern": "^\\W+$", + "normalizedPattern": "^\\W+$", + "expectedCompile": true, + "input": "ñ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": true, "match": false }, + "rustRkyv": { "compile": true, "match": false } + } + }, + { + "id": "rejected-word-boundary-unicode-divergence", + "description": "Word boundary is ASCII-oriented in Go and RE2JS but Unicode-aware in Rust.", + "category": "semantic-boundary", + "contract": "rejected", + "rawPattern": "\\bñ\\b", + "normalizedPattern": "\\bñ\\b", + "expectedCompile": true, + "input": "ñ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": false }, + "re2js": { "compile": true, "match": false }, + "rustRulesBased": { "compile": true, "match": true }, + "rustRkyv": { "compile": true, "match": true } + } + }, + { + "id": "rejected-negated-word-boundary-unicode-divergence", + "description": "Negated word boundary is ASCII-oriented in Go and RE2JS but Unicode-aware in Rust.", + "category": "semantic-boundary", + "contract": "rejected", + "rawPattern": "^\\Bñ\\B$", + "normalizedPattern": "^\\Bñ\\B$", + "expectedCompile": true, + "input": "ñ", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": true }, + "re2js": { "compile": true, "match": true }, + "rustRulesBased": { "compile": true, "match": false }, + "rustRkyv": { "compile": true, "match": false } + } + }, + { + "id": "rejected-digit-shorthand-in-character-class", + "description": "Digit shorthand keeps the same ASCII versus Unicode semantic difference inside a character class.", + "category": "semantic-shorthand", + "contract": "rejected", + "rawPattern": "^[\\d]$", + "normalizedPattern": "^[\\d]$", + "expectedCompile": true, + "input": "٣", + "expectedMatch": null, + "engineExpectations": { + "go": { "compile": true, "match": false }, + "re2js": { "compile": true, "match": false }, + "rustRulesBased": { "compile": true, "match": true }, + "rustRkyv": { "compile": true, "match": true } + } + }, + { + "id": "rejected-boundary-escape-in-character-class", + "description": "The boundary escape is not accepted consistently inside a character class.", + "category": "semantic-boundary", + "contract": "rejected", + "rawPattern": "^[\\b]$", + "normalizedPattern": "^[\\b]$", + "expectedCompile": false, + "input": "b", + "expectedMatch": false + }, + { + "id": "rejected-unclosed-character-class", + "description": "Malformed character classes are not authorable and fail closed.", + "category": "malformed", + "contract": "rejected", + "rawPattern": "[", + "normalizedPattern": "[", + "expectedCompile": false, + "input": "anything", + "expectedMatch": false + }, + { + "id": "rejected-dangling-quantifier", + "description": "A quantifier without an expression is malformed and fails closed.", + "category": "malformed", + "contract": "rejected", + "rawPattern": "*foo", + "normalizedPattern": "*foo", + "expectedCompile": false, + "input": "foo", + "expectedMatch": false + }, + { + "id": "rejected-positive-lookahead", + "description": "Positive lookahead is outside the portable syntax contract.", + "category": "lookaround", + "contract": "rejected", + "rawPattern": "foo(?=bar)", + "normalizedPattern": "foo(?=bar)", + "expectedCompile": false, + "input": "foobar", + "expectedMatch": false + }, + { + "id": "rejected-negative-lookahead", + "description": "Negative lookahead is outside the portable syntax contract.", + "category": "lookaround", + "contract": "rejected", + "rawPattern": "foo(?!bar)", + "normalizedPattern": "foo(?!bar)", + "expectedCompile": false, + "input": "foobaz", + "expectedMatch": false + }, + { + "id": "rejected-positive-lookbehind", + "description": "Positive lookbehind is outside the portable syntax contract.", + "category": "lookaround", + "contract": "rejected", + "rawPattern": "(?<=foo)bar", + "normalizedPattern": "(?<=foo)bar", + "expectedCompile": false, + "input": "foobar", + "expectedMatch": false + }, + { + "id": "rejected-negative-lookbehind", + "description": "Negative lookbehind is outside the portable syntax contract.", + "category": "lookaround", + "contract": "rejected", + "rawPattern": "(?a)$", + "normalizedPattern": "^(?P<1>a)$", + "expectedCompile": null, + "input": "a", + "expectedMatch": null, + "engineExpectations": { + "go": { + "compile": true, + "match": true + }, + "re2js": { + "compile": true, + "match": true + }, + "rustRulesBased": { + "compile": false, + "match": null + }, + "rustRkyv": { + "compile": false, + "match": null + } + } + }, + { + "id": "rejected-duplicate-named-group", + "description": "Go accepts a duplicate capture name, but RE2JS and both Rust evaluators reject it.", + "category": "named-group-dialect", + "contract": "rejected", + "rawPattern": "^(?Pa)(?b)$", + "normalizedPattern": "^(?Pa)(?b)$", + "expectedCompile": null, + "input": "ab", + "expectedMatch": null, + "engineExpectations": { + "go": { + "compile": true, + "match": true + }, + "re2js": { + "compile": false, + "match": null + }, + "rustRulesBased": { + "compile": false, + "match": null + }, + "rustRkyv": { + "compile": false, + "match": null + } + } + }, + { + "id": "rejected-named-backreference", + "description": "Named backreferences and named capture groups are outside the portable syntax contract.", + "category": "backreference", + "contract": "rejected", + "rawPattern": "(?Pa)\\k", + "normalizedPattern": "(?Pa)\\k", + "expectedCompile": false, + "input": "aa", + "expectedMatch": false + }, + { + "id": "rejected-atomic-group", + "description": "Atomic groups are outside the portable syntax contract.", + "category": "group", + "contract": "rejected", + "rawPattern": "(?>foo)", + "normalizedPattern": "(?>foo)", + "expectedCompile": false, + "input": "foo", + "expectedMatch": false + }, + { + "id": "rejected-possessive-quantifier", + "description": "Possessive quantifiers are outside the portable syntax contract.", + "category": "quantifier", + "contract": "rejected", + "rawPattern": "a++", + "normalizedPattern": "a++", + "expectedCompile": null, + "input": "aaa", + "expectedMatch": null, + "engineExpectations": { + "go": { + "compile": false, + "match": null + }, + "re2js": { + "compile": false, + "match": null + }, + "rustRulesBased": { + "compile": true, + "match": true + }, + "rustRkyv": { + "compile": true, + "match": true + } + } + } + ] +} diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/targeting-regex-conformance.sha256 b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/targeting-regex-conformance.sha256 new file mode 100644 index 000000000000..52f81f64c2f2 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/targeting-regex-conformance.sha256 @@ -0,0 +1 @@ +4f8ef95faec9faa3c2b0756ac61afe862e35c0646cba5375532a85335f209e8f targeting-regex-conformance.json diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/test-validate-targeting-regex-conformance.sh b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/test-validate-targeting-regex-conformance.sh new file mode 100755 index 000000000000..d1480b59b730 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/test-validate-targeting-regex-conformance.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +fixture="$script_dir/targeting-regex-conformance.json" +validator="$script_dir/validate-targeting-regex-conformance.jq" +temporary_file=$(mktemp) + +cleanup() { + rm -f "$temporary_file" +} + +handle_signal() { + signal=$1 + trap - "$signal" + cleanup + kill -s "$signal" "$$" +} + +trap cleanup 0 +trap 'handle_signal HUP' HUP +trap 'handle_signal INT' INT +trap 'handle_signal TERM' TERM + +expect_invalid() { + description=$1 + mutation=$2 + + jq "$mutation" "$fixture" >"$temporary_file" + if jq -e -f "$validator" "$temporary_file" >/dev/null; then + echo "validator accepted invalid fixture: $description" >&2 + exit 1 + fi +} + +jq -e -f "$validator" "$fixture" >/dev/null + +expect_invalid \ + "compile failure with a true match result" \ + '.cases |= map(if .id == "rejected-byte-escape" then .expectedMatch = true else . end)' + +expect_invalid \ + "empty semantics" \ + '.semantics = {}' + +expect_invalid \ + "empty portable syntax contract" \ + '.portableSyntax.accepted = [] | .portableSyntax.rejected = []' + +echo "targeting regex conformance validator tests passed" diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/validate-targeting-regex-conformance.jq b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/validate-targeting-regex-conformance.jq new file mode 100644 index 000000000000..78a24e3c6f92 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/regex-conformance/validate-targeting-regex-conformance.jq @@ -0,0 +1,98 @@ +def nonempty_string: + type == "string" and length > 0; + +def nullable_boolean: + type == "boolean" or . == null; + +def nonempty_string_array: + type == "array" and + length > 0 and + all(.[]; nonempty_string) and + length == (unique | length); + +def valid_semantics: + type == "object" and + (.acceptedSyntax | nonempty_string) and + (.engines | nonempty_string_array) and + (.matchMode | nonempty_string) and + .nativeCompileFailureEvaluationResult == false and + (.normalization | nonempty_string) and + (.contract | nonempty_string) and + (.expectedCompile | nonempty_string) and + (.expectedMatch | nonempty_string) and + (.engineExpectations | nonempty_string); + +def valid_portable_syntax: + type == "object" and + (.accepted | nonempty_string_array) and + (.rejected | nonempty_string_array) and + ([.accepted[], .rejected[]] | length) == + ([.accepted[], .rejected[]] | unique | length); + +def expected_engine_keys: + ["go", "re2js", "rustRkyv", "rustRulesBased"]; + +def valid_engine_expectation: + has("compile") and + has("match") and + (.compile | type) == "boolean" and + (.match | nullable_boolean) and + (if .compile then (.match | type) == "boolean" else .match == null end); + +def valid_engine_expectations: + (.engineExpectations | keys | sort) == expected_engine_keys and + all(.engineExpectations[]; valid_engine_expectation) and + ([.engineExpectations[].compile] | unique) as $compile_values | + ([.engineExpectations[].match] | unique) as $match_values | + (if ($compile_values | length) == 1 + then .expectedCompile == $compile_values[0] + else .expectedCompile == null + end) and + (if ($match_values | length) == 1 + then .expectedMatch == $match_values[0] + else .expectedMatch == null + end); + +def valid_case: + . as $case | + has("id") and + has("description") and + has("category") and + has("contract") and + has("rawPattern") and + has("normalizedPattern") and + has("expectedCompile") and + has("input") and + has("expectedMatch") and + (.id | nonempty_string) and + (.description | nonempty_string) and + (.category | nonempty_string) and + (.contract == "accepted" or .contract == "rejected") and + (.id | startswith($case.contract + "-")) and + (.rawPattern | type) == "string" and + (.normalizedPattern | type) == "string" and + (.expectedCompile | nullable_boolean) and + (.input | type) == "string" and + (.expectedMatch | nullable_boolean) and + (if .contract == "accepted" + then .expectedCompile == true and + (.expectedMatch | type) == "boolean" and + has("engineExpectations") == false + else true + end) and + (if has("engineExpectations") + then valid_engine_expectations + else .expectedCompile != null and + .expectedMatch != null and + (if .expectedCompile == false then .expectedMatch == false else true end) + end); + +.schema == "datadog.ffe.targeting-regex-conformance/v1" and +.schemaVersion == 1 and +(.contractVersion | nonempty_string) and +(.semantics | valid_semantics) and +(.portableSyntax | valid_portable_syntax) and +(.cases | type) == "array" and +(.cases | length) > 0 and +([.cases[].id] | length) == ([.cases[].id] | unique | length) and +all(.cases[]; valid_case) diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/ufc-config.json b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/ufc-config.json index 52fd31d5ff1a..ece892bc0e6f 100644 --- a/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/ufc-config.json +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/ffe-system-test-data/ufc-config.json @@ -89,9 +89,59 @@ "unicode-word-boundary": { "key": "unicode-word-boundary", "value": "unicode-word-boundary" + }, + "comma-quantifier": { + "key": "comma-quantifier", + "value": "comma-quantifier" + }, + "literal-whitespace": { + "key": "literal-whitespace", + "value": "literal-whitespace" } }, "allocations": [ + { + "key": "comma-quantifier", + "rules": [ + { + "conditions": [ + { + "attribute": "comma_regex_case", + "operator": "MATCHES", + "value": "a{1,3}" + } + ] + } + ], + "splits": [ + { + "variationKey": "comma-quantifier", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "literal-whitespace", + "rules": [ + { + "conditions": [ + { + "attribute": "whitespace_regex_case", + "operator": "MATCHES", + "value": " exact " + } + ] + } + ], + "splits": [ + { + "variationKey": "literal-whitespace", + "shards": [] + } + ], + "doLog": true + }, { "key": "capturing-groups", "rules": [ @@ -888,6 +938,56 @@ } ] }, + "semver-invalid-comparand-waterfall-test": { + "key": "semver-invalid-comparand-waterfall-test", + "enabled": true, + "variationType": "STRING", + "comment": "This flag deliberately places an invalid configured SemVer comparand in a non-last allocation of a waterfall, followed by a valid default allocation. SDKs must reject the whole flag during parsing rather than skipping the invalid allocation and falling through to the default.", + "variations": { + "matched": { + "key": "matched", + "value": "matched" + }, + "fallback": { + "key": "fallback", + "value": "fallback" + } + }, + "allocations": [ + { + "key": "invalid-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "18446744073709551616.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "default", + "rules": [], + "splits": [ + { + "variationKey": "fallback", + "shards": [] + } + ], + "doLog": true + } + ] + }, "start-and-end-date-test": { "key": "start-and-end-date-test", "enabled": true, @@ -4141,6 +4241,891 @@ "doLog": true } ] + }, + "semver-precedence-test": { + "key": "semver-precedence-test", + "enabled": true, + "variationType": "STRING", + "comment": "Isolates SemVer precedence rules that are easy to get wrong: multi-digit numeric prerelease ordering (beta.2 < beta.11), build-metadata-ignored across every operator (EQ/NEQ/LT/LTE/GT/GTE), and alphanumeric prerelease ordering (alpha < alpha.1 < alpha.beta < beta < release). Each allocation gates on a unique SEMVER_EQ attribute version so test attributes cannot cross-match other allocations; negative build-metadata cases (NEQ/LT/GT) fall through to DEFAULT.", + "variations": { + "numeric-prerelease": { + "key": "numeric-prerelease", + "value": "numeric-prerelease" + }, + "build-equal-precedence": { + "key": "build-equal-precedence", + "value": "build-equal-precedence" + }, + "build-dotted-eq": { + "key": "build-dotted-eq", + "value": "build-dotted-eq" + }, + "build-lte": { + "key": "build-lte", + "value": "build-lte" + }, + "build-gte": { + "key": "build-gte", + "value": "build-gte" + }, + "alpha-before-alpha-1": { + "key": "alpha-before-alpha-1", + "value": "alpha-before-alpha-1" + }, + "alpha-1-before-alpha-beta": { + "key": "alpha-1-before-alpha-beta", + "value": "alpha-1-before-alpha-beta" + }, + "alpha-beta-before-beta": { + "key": "alpha-beta-before-beta", + "value": "alpha-beta-before-beta" + }, + "beta-before-release": { + "key": "beta-before-release", + "value": "beta-before-release" + }, + "matched": { + "key": "matched", + "value": "matched" + } + }, + "allocations": [ + { + "key": "numeric-prerelease", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "10.0.0-beta.2" + }, + { + "attribute": "app_version", + "operator": "SEMVER_LT", + "value": "10.0.0-beta.11" + } + ] + } + ], + "splits": [ + { + "variationKey": "numeric-prerelease", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "build-equal-precedence", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "20.0.0+linux" + }, + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "20.0.0+darwin" + } + ] + } + ], + "splits": [ + { + "variationKey": "build-equal-precedence", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "build-dotted-eq", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "30.0.0+exp.sha.5114f85" + }, + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "30.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "build-dotted-eq", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "build-lte", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "40.0.0+build.42" + }, + { + "attribute": "app_version", + "operator": "SEMVER_LTE", + "value": "40.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "build-lte", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "build-gte", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "50.0.0+build.42" + }, + { + "attribute": "app_version", + "operator": "SEMVER_GTE", + "value": "50.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "build-gte", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "build-neq", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "60.0.0+build.42" + }, + { + "attribute": "app_version", + "operator": "SEMVER_NEQ", + "value": "60.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "build-lt", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "70.0.0+build.42" + }, + { + "attribute": "app_version", + "operator": "SEMVER_LT", + "value": "70.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "build-gt", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "80.0.0+build.42" + }, + { + "attribute": "app_version", + "operator": "SEMVER_GT", + "value": "80.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "alpha-before-alpha-1", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "90.0.0-alpha" + }, + { + "attribute": "app_version", + "operator": "SEMVER_LT", + "value": "90.0.0-alpha.1" + } + ] + } + ], + "splits": [ + { + "variationKey": "alpha-before-alpha-1", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "alpha-1-before-alpha-beta", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "91.0.0-alpha.1" + }, + { + "attribute": "app_version", + "operator": "SEMVER_LT", + "value": "91.0.0-alpha.beta" + } + ] + } + ], + "splits": [ + { + "variationKey": "alpha-1-before-alpha-beta", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "alpha-beta-before-beta", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "92.0.0-alpha.beta" + }, + { + "attribute": "app_version", + "operator": "SEMVER_LT", + "value": "92.0.0-beta" + } + ] + } + ], + "splits": [ + { + "variationKey": "alpha-beta-before-beta", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "beta-before-release", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "93.0.0-beta" + }, + { + "attribute": "app_version", + "operator": "SEMVER_LT", + "value": "93.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "beta-before-release", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-version-parts-input-test": { + "key": "semver-version-parts-input-test", + "enabled": true, + "variationType": "STRING", + "comment": "Exercises one- through five-part runtime attributes using only canonical three-part configured comparands, so comparand ingestion cannot prevent an input case from running. Scenario gates isolate each runtime comparison.", + "variations": { + "one-part-input": { + "key": "one-part-input", + "value": "one-part-input" + }, + "two-part-input": { + "key": "two-part-input", + "value": "two-part-input" + }, + "four-part-input": { + "key": "four-part-input", + "value": "four-part-input" + }, + "five-part-input": { + "key": "five-part-input", + "value": "five-part-input" + } + }, + "allocations": [ + { + "key": "one-part-input", + "rules": [ + { + "conditions": [ + { + "attribute": "scenario", + "operator": "ONE_OF", + "value": [ + "one-part-input" + ] + }, + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "18.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "one-part-input", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "two-part-input", + "rules": [ + { + "conditions": [ + { + "attribute": "scenario", + "operator": "ONE_OF", + "value": [ + "two-part-input" + ] + }, + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "18.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "two-part-input", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "four-part-input", + "rules": [ + { + "conditions": [ + { + "attribute": "scenario", + "operator": "ONE_OF", + "value": [ + "four-part-input" + ] + }, + { + "attribute": "app_version", + "operator": "SEMVER_GT", + "value": "17.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "four-part-input", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "five-part-input", + "rules": [ + { + "conditions": [ + { + "attribute": "scenario", + "operator": "ONE_OF", + "value": [ + "five-part-input" + ] + }, + { + "attribute": "app_version", + "operator": "SEMVER_GT", + "value": "17.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "five-part-input", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-version-parts-one-part-comparand-test": { + "key": "semver-version-parts-one-part-comparand-test", + "enabled": true, + "variationType": "STRING", + "comment": "Isolates a one-part configured SemVer comparand from every other extended comparand shape.", + "variations": { + "one-part-comparand": { + "key": "one-part-comparand", + "value": "one-part-comparand" + } + }, + "allocations": [ + { + "key": "one-part-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "18" + } + ] + } + ], + "splits": [ + { + "variationKey": "one-part-comparand", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-version-parts-two-part-comparand-test": { + "key": "semver-version-parts-two-part-comparand-test", + "enabled": true, + "variationType": "STRING", + "comment": "Isolates a two-part configured SemVer comparand from every other extended comparand shape.", + "variations": { + "two-part-comparand": { + "key": "two-part-comparand", + "value": "two-part-comparand" + } + }, + "allocations": [ + { + "key": "two-part-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "18.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "two-part-comparand", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-version-parts-four-part-comparand-test": { + "key": "semver-version-parts-four-part-comparand-test", + "enabled": true, + "variationType": "STRING", + "comment": "Isolates a four-part configured SemVer comparand while the runtime attribute remains a canonical three-part version.", + "variations": { + "four-part-comparand": { + "key": "four-part-comparand", + "value": "four-part-comparand" + } + }, + "allocations": [ + { + "key": "four-part-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_GT", + "value": "18.0.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "four-part-comparand", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-version-parts-five-part-comparand-test": { + "key": "semver-version-parts-five-part-comparand-test", + "enabled": true, + "variationType": "STRING", + "comment": "Isolates a five-part configured SemVer comparand while the runtime attribute remains a canonical three-part version.", + "variations": { + "five-part-comparand": { + "key": "five-part-comparand", + "value": "five-part-comparand" + } + }, + "allocations": [ + { + "key": "five-part-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_GT", + "value": "18.0.0.0.0" + } + ] + } + ], + "splits": [ + { + "variationKey": "five-part-comparand", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-invalid-comparand-syntax-test": { + "key": "semver-invalid-comparand-syntax-test", + "enabled": true, + "variationType": "STRING", + "comment": "This flag deliberately uses a non-parseable SemVer comparand. SDKs must remove this flag without rejecting the full configuration.", + "variations": { + "matched": { + "key": "matched", + "value": "matched" + } + }, + "allocations": [ + { + "key": "invalid-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "not-a-version" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-valid-comparand-hyphenated-prerelease-test": { + "key": "semver-valid-comparand-hyphenated-prerelease-test", + "enabled": true, + "variationType": "STRING", + "comment": "This flag uses a valid SemVer comparand with repeated hyphens inside its prerelease identifiers.", + "variations": { + "matched": { + "key": "matched", + "value": "matched" + } + }, + "allocations": [ + { + "key": "valid-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "1.2.3----R-S.12.9.1--.12+meta" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-invalid-comparand-consecutive-dots-test": { + "key": "semver-invalid-comparand-consecutive-dots-test", + "enabled": true, + "variationType": "STRING", + "comment": "This flag deliberately uses consecutive dots in its SemVer prerelease comparand. SDKs must remove this flag without rejecting the full configuration.", + "variations": { + "matched": { + "key": "matched", + "value": "matched" + } + }, + "allocations": [ + { + "key": "invalid-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "1.0.0-alpha..1" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-invalid-comparand-dot-suffix-test": { + "key": "semver-invalid-comparand-dot-suffix-test", + "enabled": true, + "variationType": "STRING", + "comment": "This flag deliberately uses a Ruby-style dot-suffixed SemVer comparand. SDKs must remove this flag without rejecting the full configuration.", + "variations": { + "matched": { + "key": "matched", + "value": "matched" + } + }, + "allocations": [ + { + "key": "invalid-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "1.2.3.DEV" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-invalid-comparand-vprefix-test": { + "key": "semver-invalid-comparand-vprefix-test", + "enabled": true, + "variationType": "STRING", + "comment": "This flag deliberately uses a v-prefixed SemVer comparand, which is not valid SemVer. SDKs must remove this flag without rejecting the full configuration.", + "variations": { + "matched": { + "key": "matched", + "value": "matched" + } + }, + "allocations": [ + { + "key": "invalid-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "v1.2.3" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-invalid-comparand-leading-zero-test": { + "key": "semver-invalid-comparand-leading-zero-test", + "enabled": true, + "variationType": "STRING", + "comment": "This flag deliberately uses a leading-zero core component in its SemVer comparand. SDKs must remove this flag without rejecting the full configuration.", + "variations": { + "matched": { + "key": "matched", + "value": "matched" + } + }, + "allocations": [ + { + "key": "invalid-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": "01.2.3" + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "semver-invalid-comparand-non-string-test": { + "key": "semver-invalid-comparand-non-string-test", + "enabled": true, + "variationType": "STRING", + "comment": "This flag deliberately uses a non-string (JSON number) SemVer comparand. SDKs must reject this as an invalid comparand without rejecting the full configuration.", + "variations": { + "matched": { + "key": "matched", + "value": "matched" + } + }, + "allocations": [ + { + "key": "invalid-comparand", + "rules": [ + { + "conditions": [ + { + "attribute": "app_version", + "operator": "SEMVER_EQ", + "value": 1.2 + } + ] + } + ], + "splits": [ + { + "variationKey": "matched", + "shards": [] + } + ], + "doLog": true + } + ] } } }