diff --git a/.github/workflows/validate_tests.yml b/.github/workflows/validate_tests.yml new file mode 100644 index 0000000000..5782ccda34 --- /dev/null +++ b/.github/workflows/validate_tests.yml @@ -0,0 +1,20 @@ +name: Validate test data + +on: + push: + branches: + - main + pull_request: + branches: '**' + +jobs: + run_all: + name: Validate tests for specific schema version + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Validate tests for schema v0.0.1 + run: | + cd test + python3 validate.py --schema-dir ./schemas/v0-0-1/ diff --git a/test/.vscode/settings.json b/test/.vscode/settings.json new file mode 100644 index 0000000000..92532167bf --- /dev/null +++ b/test/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "json.schemas": [ + { + "fileMatch": [ + "tests/**/*.json" + ], + "url": "./schemas/v0-0-1/tests.schema.json" + } + ] +} diff --git a/test/README.md b/test/README.md index ac0aecf94e..1317c00120 100644 --- a/test/README.md +++ b/test/README.md @@ -1,37 +1,25 @@ -The files in this directory were originally copied from the [messageformat project](https://github.com/messageformat/messageformat/tree/11c95dab2b25db8454e49ff4daadb817e1d5b770/packages/mf2-messageformat/src/__fixtures) +The tests in the `./tests/` directory were originally copied from the [messageformat project](https://github.com/messageformat/messageformat/tree/11c95dab2b25db8454e49ff4daadb817e1d5b770/packages/mf2-messageformat/src/__fixtures) and are here relicensed by their original author (Eemeli Aro) under the Unicode License. These test files are intended to be useful for testing multiple different message processors in different ways: -- `syntax-errors.json` — An array of strings that should produce a Syntax Error when parsed. - -- `data-model-errors.json` - An object with string keys and arrays of strings as values, - where each key is the name of an error and its value is an array of strings that - should produce `error` when processed. - Error names are defined in ["MessageFormat 2.0 Errors"](../spec/errors.md) in the spec. - -- `test-core.json` — An array of test cases that do not depend on any registry definitions. - Each test may include some of the following fields: - - `src: string` (required) — The MF2 syntax source. - - `exp: string` (required) — The expected result of formatting the message to a string. - - `locale: string` — The locale to use for formatting. Defaults to 'en-US'. - - `params: Record` — Parameters to pass in to the formatter for resolving external variables. - - `parts: object[]` — The expected result of formatting the message to parts. - - `cleanSrc: string` — A normalixed form of `src`, for testing stringifiers. - - `errors: { type: string }[]` — The runtime errors expected to be emitted when formatting the message. - If `errors` is either absent or empty, the message must be formatted without errors. - - `only: boolean` — Normally not set. A flag to use during development to only run one or more specific tests. - -- `test-function.json` — An object with string keys and arrays of test cases as values, - using the same definition as for `test-core.json`. - The keys each correspond to a function that is used in the tests. - Since the behavior of built-in formatters is implementation-specific, - the `exp` field should generally be omitted, - except for error cases. - -TypeScript `.d.ts` files are included for `test-core.json` and `test-function.json` with the above definition. +- `syntax.json` — Test cases that do not depend on any registry definitions. + +- `syntax-errors.json` — Strings that should produce a Syntax Error when parsed. + +- `data-model-errors.json` - Strings that should produce Data Model Error when processed. + Error names are defined in ["MessageFormat 2.0 Errors"](../spec/errors.md) in the spec. + +- `functions/` — Test cases that correspond to built-in functions. + The behaviour of the built-in formatters is implementation-specific so the `exp` field is often + omitted and assertions are made on error cases. Some examples of test harnesses using these tests, from the source repository: + - [CST parse/stringify tests](https://github.com/messageformat/messageformat/blob/11c95dab2b25db8454e49ff4daadb817e1d5b770/packages/mf2-messageformat/src/cst/cst.test.ts) - [Data model stringify tests](https://github.com/messageformat/messageformat/blob/11c95dab2b25db8454e49ff4daadb817e1d5b770/packages/mf2-messageformat/src/data-model/stringify.test.ts) - [Formatting tests](https://github.com/messageformat/messageformat/blob/11c95dab2b25db8454e49ff4daadb817e1d5b770/packages/mf2-messageformat/src/messageformat.test.ts) + +A JSON schema is included for the test files in this repository. [Semantic-versioned](https://semver.org/) schema files can be found in the [schemas folder](./schemas/). + +For users of Visual Studio Code, a [settings file](./.vscode/settings.json) is included that enables schema validation while editing the test files. diff --git a/test/data-model-errors.json b/test/data-model-errors.json deleted file mode 100644 index 0a6bd67641..0000000000 --- a/test/data-model-errors.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "Variant Key Mismatch": [ - ".match {$foo :x} * * {{foo}}", - ".match {$foo :x} {$bar :x} * {{foo}}" - ], - "Missing Fallback Variant": [ - ".match {:foo} 1 {{_}}", - ".match {:foo} other {{_}}", - ".match {:foo} {:bar} * 1 {{_}} 1 * {{_}}" - ], - "Missing Selector Annotation": [ - ".match {$foo} one {{one}} * {{other}}", - ".input {$foo} .match {$foo} one {{one}} * {{other}}", - ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}" - ], - "Duplicate Declaration": [ - ".input {$foo} .input {$foo} {{_}}", - ".input {$foo} .local $foo = {42} {{_}}", - ".local $foo = {42} .input {$foo} {{_}}", - ".local $foo = {:unknown} .local $foo = {42} {{_}}", - ".local $foo = {$bar} .local $bar = {42} {{_}}", - ".local $foo = {$foo} {{_}}", - ".local $foo = {$bar} .local $bar = {$baz} {{_}}", - ".local $foo = {$bar :func} .local $bar = {$baz} {{_}}", - ".local $foo = {42 :func opt=$foo} {{_}}", - ".local $foo = {42 :func opt=$bar} .local $bar = {42} {{_}}" - ], - "Duplicate Option Name": [ - "bad {:placeholder option=x option=x}", - "bad {:placeholder ns:option=x ns:option=y}" - ] -} diff --git a/test/schemas/v0-0-1/tests.schema.json b/test/schemas/v0-0-1/tests.schema.json new file mode 100644 index 0000000000..fbe06e2a36 --- /dev/null +++ b/test/schemas/v0-0-1/tests.schema.json @@ -0,0 +1,390 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0-0-1/tests.schema.json", + "title": "MessageFormat 2 data-driven tests", + "description": "This is the main schema for MessageFormat 2 test source data.", + "type": "object", + "additionalProperties": false, + "required": [ + "tests" + ], + "properties": { + "scenario": { + "type": "string", + "description": "Identifier for the tests in the file" + }, + "description": { + "type": "string", + "description": "Information about the test scenario." + }, + "defaultTestProperties": { + "$ref": "#/$defs/defaultTestProperties" + }, + "tests": { + "type": "array", + "items": { + "$ref": "#/$defs/test" + }, + "minItems": 1 + } + }, + "$comment": "This allOf specifies required test properties that allow a default. A value will be required in 'defaultTestProperties' if one is not provided for every individual test.", + "allOf": [ + { + "anyOf": [ + { + "properties": { + "defaultTestProperties": { + "required": [ + "locale" + ] + } + } + }, + { + "properties": { + "tests": { + "type": "array", + "items": { + "required": [ + "locale" + ] + } + } + } + } + ] + }, + { + "anyOf": [ + { + "properties": { + "defaultTestProperties": { + "required": [ + "src" + ] + } + } + }, + { + "properties": { + "tests": { + "type": "array", + "items": { + "required": [ + "src" + ] + } + } + } + } + ] + }, + { + "$comment": "Only one assertion is required. It doesn't matter which type.", + "anyOf": [ + { + "properties": { + "defaultTestProperties": { + "$ref": "#/$defs/anyExp" + } + } + }, + { + "properties": { + "tests": { + "type": "array", + "items": { + "$ref": "#/$defs/anyExp" + } + } + } + } + ] + } + ], + "$defs": { + "defaultTestProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "locale": { + "$ref": "#/$defs/locale" + }, + "src": { + "$ref": "#/$defs/src" + }, + "params": { + "$ref": "#/$defs/params" + }, + "exp": { + "$ref": "#/$defs/exp" + }, + "expCleanSrc": { + "$ref": "#/$defs/expCleanSrc" + }, + "expParts": { + "$ref": "#/$defs/expParts" + }, + "expErrors": { + "$ref": "#/$defs/expErrors" + } + } + }, + "test": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string", + "description": "Information about the test." + }, + "locale": { + "$ref": "#/$defs/locale" + }, + "src": { + "$ref": "#/$defs/src" + }, + "params": { + "$ref": "#/$defs/params" + }, + "exp": { + "$ref": "#/$defs/exp" + }, + "expCleanSrc": { + "$ref": "#/$defs/expCleanSrc" + }, + "expParts": { + "$ref": "#/$defs/expParts" + }, + "expErrors": { + "$ref": "#/$defs/expErrors" + }, + "only": { + "type": "boolean", + "description": "Normally not set. A flag to use during development to only run one or more specific tests." + } + } + }, + "locale": { + "description": "The locale to use for formatting.", + "type": "string" + }, + "src": { + "description": "The MF2 syntax source.", + "type": "string" + }, + "params": { + "description": "Parameters to pass in to the formatter for resolving external variables.", + "type": "array", + "items": { + "$ref": "#/$defs/var" + } + }, + "var": { + "type": "object", + "oneOf": [ + { + "additionalProperties": false, + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": {} + } + }, + { + "additionalProperties": false, + "required": [ + "name", + "type", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "const": "datetime" + }, + "value": { + "$comment": "Should be converted to a datetime.", + "type": "string" + } + } + } + ] + }, + "exp": { + "description": "The expected result of formatting the message to a string.", + "type": "string" + }, + "expCleanSrc": { + "description": "The expected normalized form of `src`, for testing stringifiers.", + "type": "string" + }, + "expParts": { + "description": "The expected result of formatting the message to parts.", + "type": "array", + "items": { + "oneOf": [ + { + "description": "Message literal part.", + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "literal" + }, + "value": { + "type": "string" + } + } + }, + { + "description": "Message markup part.", + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "kind", + "name" + ], + "properties": { + "type": { + "const": "markup" + }, + "kind": { + "enum": [ + "open", + "standalone", + "close" + ] + }, + "source": { + "type": "string" + }, + "name": { + "type": "string" + }, + "options": { + "type": "object" + } + } + }, + { + "description": "Message expression part.", + "type": "object", + "required": [ + "type", + "source" + ], + "not": { + "required": [ + "parts", + "value" + ] + }, + "properties": { + "type": { + "type": "string" + }, + "source": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "parts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "source": { + "type": "string" + }, + "value": {} + }, + "required": [ + "type" + ] + } + }, + "value": {} + } + } + ] + } + }, + "expErrors": { + "description": "The runtime errors expected to be emitted when formatting the message. If expErrors is either absent or empty, the message must be formatted without errors.", + "type": [ + "array", + "boolean" + ], + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "enum": [ + "syntax-error", + "variant-key-mismatch", + "missing-fallback-variant", + "missing-selector-annotation", + "duplicate-declaration", + "duplicate-option-name", + "unresolved-variable", + "unknown-function", + "unsupported-expression", + "invalid-expression", + "operand-mismatch", + "unsupported-statement", + "selection-error", + "formatting-error", + "bad-input" + ] + } + } + } + }, + "anyExp": { + "anyOf": [ + { + "required": [ + "exp" + ] + }, + { + "required": [ + "expCleanSrc" + ] + }, + { + "required": [ + "expParts" + ] + }, + { + "required": [ + "expErrors" + ] + } + ] + } + } +} diff --git a/test/syntax-errors.json b/test/syntax-errors.json deleted file mode 100644 index fc4537131c..0000000000 --- a/test/syntax-errors.json +++ /dev/null @@ -1,56 +0,0 @@ -[ - ".", - "{", - "}", - "{}", - "{{", - "{{}", - "{{}}}", - "{|foo| #markup}", - "{{missing end brace}", - "{{missing end braces", - "{{missing end {$braces", - "{{extra}} content", - "empty { } placeholder", - "missing space {42:func}", - "missing space {|foo|:func}", - "missing space {|foo|@bar}", - "missing space {:func@bar}", - "missing space {:func @bar@baz}", - "missing space {:func @bar=42@baz}", - "missing space {+reserved@bar}", - "missing space {&private@bar}", - "bad {:} placeholder", - "bad {\\u0000placeholder}", - "no-equal {|42| :number minimumFractionDigits 2}", - "bad {:placeholder option=}", - "bad {:placeholder option value}", - "bad {:placeholder option:value}", - "bad {:placeholder option}", - "bad {:placeholder:}", - "bad {::placeholder}", - "bad {:placeholder::foo}", - "bad {:placeholder option:=x}", - "bad {:placeholder :option=x}", - "bad {:placeholder option::x=y}", - "bad {$placeholder option}", - "bad {:placeholder @attribute=}", - "bad {:placeholder @attribute=@foo}", - "no {placeholder end", - "no {$placeholder end", - "no {:placeholder end", - "no {|placeholder| end", - "no {|literal} end", - "no {|literal or placeholder end", - ".local bar = {|foo|} {{_}}", - ".local #bar = {|foo|} {{_}}", - ".local $bar {|foo|} {{_}}", - ".local $bar = |foo| {{_}}", - ".match {#foo} * {{foo}}", - ".match {} * {{foo}}", - ".match {|foo| :x} {|bar| :x} ** {{foo}}", - ".match * {{foo}}", - ".match {|x| :x} * foo", - ".match {|x| :x} * {{foo}} extra", - ".match |x| * {{foo}}" -] diff --git a/test/test-core.json b/test/test-core.json deleted file mode 100644 index 0e7049fdb1..0000000000 --- a/test/test-core.json +++ /dev/null @@ -1,212 +0,0 @@ -[ - { "src": "hello", "exp": "hello" }, - { "src": "hello {world}", "exp": "hello world" }, - { - "src": "hello { world\t\n}", - "exp": "hello world", - "cleanSrc": "hello {world}" - }, - { - "src": "hello {\u3000world\r}", - "exp": "hello world", - "cleanSrc": "hello {world}" - }, - { "src": "hello {|world|}", "exp": "hello world" }, - { "src": "hello {||}", "exp": "hello " }, - { - "src": "hello {$place}", - "params": { "place": "world" }, - "exp": "hello world" - }, - { - "src": "hello {$place-.}", - "params": { "place-.": "world" }, - "exp": "hello world" - }, - { - "src": "hello {$place}", - "errors": [{ "type": "unresolved-var" }], - "exp": "hello {$place}" - }, - { - "src": "{$one} and {$two}", - "params": { "one": 1.3, "two": 4.2 }, - "exp": "1.3 and 4.2" - }, - { - "src": "{$one} et {$two}", - "locale": "fr", - "params": { "one": 1.3, "two": 4.2 }, - "exp": "1,3 et 4,2" - }, - { "src": ".local $foo = {bar} {{bar {$foo}}}", "exp": "bar bar" }, - { "src": ".local $foo = {|bar|} {{bar {$foo}}}", "exp": "bar bar" }, - { - "src": ".local $foo = {|bar|} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar bar" - }, - { - "src": ".local $foo = {$bar} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar foo" - }, - { - "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "baz": "foo" }, - "exp": "bar foo" - }, - { - "src": ".input {$foo} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar foo" - }, - { - "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "foo": "foo" }, - "exp": "bar foo" - }, - { - "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "baz": "foo" }, - "exp": "bar foo" - }, - { "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}", "exp": "42 42" }, - { - "src": "{#tag}", - "exp": "", - "parts": [{ "type": "markup", "kind": "open", "name": "tag" }] - }, - { - "src": "{#tag}content", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "open", "name": "tag" }, - { "type": "literal", "value": "content" } - ] - }, - { - "src": "{#ns:tag}content{/ns:tag}", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "open", "name": "ns:tag" }, - { "type": "literal", "value": "content" }, - { "type": "markup", "kind": "close", "name": "ns:tag" } - ] - }, - { - "src": "{/tag}content", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "close", "name": "tag" }, - { "type": "literal", "value": "content" } - ] - }, - { - "src": "{#tag foo=bar}", - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "open", - "name": "tag", - "options": { "foo": "bar" } - } - ] - }, - { - "src": "{#tag foo=bar/}", - "cleanSrc": "{#tag foo=bar /}", - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "standalone", - "name": "tag", - "options": { "foo": "bar" } - } - ] - }, - { - "src": "{#tag a:foo=|foo| b:bar=$bar}", - "params": { "bar": "b a r" }, - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "open", - "name": "tag", - "options": { "a:foo": "foo", "b:bar": "b a r" } - } - ] - }, - { - "src": "{/tag foo=bar}", - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "close", - "name": "tag", - "options": { "foo": "bar" } - } - ] - }, - { - "src": "{42 @foo @bar=13}", - "exp": "42", - "parts": [{ "type": "string", "value": "42" }] - }, - { - "src": "{42 @foo=$bar}", - "exp": "42", - "parts": [{ "type": "string", "value": "42" }] - }, - { - "src": "foo {+reserved}", - "exp": "foo {+}", - "parts": [ - { "type": "literal", "value": "foo " }, - { "type": "fallback", "source": "+" } - ], - "errors": [{ "type": "unsupported-annotation" }] - }, - { - "src": "foo {&private}", - "exp": "foo {&}", - "parts": [ - { "type": "literal", "value": "foo " }, - { "type": "fallback", "source": "&" } - ], - "errors": [{ "type": "unsupported-annotation" }] - }, - { - "src": "foo {?reserved @a @b=$c}", - "exp": "foo {?}", - "parts": [ - { "type": "literal", "value": "foo " }, - { "type": "fallback", "source": "?" } - ], - "errors": [{ "type": "unsupported-annotation" }] - }, - { - "src": ".foo {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }] - }, - { - "src": ".foo{42}{{bar}}", - "cleanSrc": ".foo {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }] - }, - { - "src": ".foo |}lit{| {42}{{bar}}", - "cleanSrc": ".foo |}lit{| {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }] - } -] diff --git a/test/test-core.json.d.ts b/test/test-core.json.d.ts deleted file mode 100644 index 3bff7634ab..0000000000 --- a/test/test-core.json.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type TestMessage = { - /** The MF2 message to be tested. */ - src: string; - /** The locale to use for formatting. Defaults to 'en-US'. */ - locale?: string; - /** Parameters to pass in to the formatter for resolving external variables. */ - params?: Record; - /** The expected result of formatting the message to a string. */ - exp: string; - /** The expected result of formatting the message to parts. */ - parts?: Array; - /** A normalixed form of `src`, for testing stringifiers. */ - cleanSrc?: string; - /** The runtime errors expected to be emitted when formatting the message. */ - errors?: Array<{ type: string }>; - /** Normally not set. A flag to use during development to only run one or more specific tests. */ - only?: boolean; -}; - -declare const data: TestMessage[]; -export default data; diff --git a/test/test-functions.json b/test/test-functions.json deleted file mode 100644 index 03080a2b6c..0000000000 --- a/test/test-functions.json +++ /dev/null @@ -1,322 +0,0 @@ -{ - "date": [ - { "src": "{:date}", "exp": "{:date}", "errors": [{ "type": "bad-input" }] }, - { - "src": "{horse :date}", - "exp": "{|horse|}", - "errors": [{ "type": "bad-input" }] - }, - { "src": "{|2006-01-02| :date}" }, - { "src": "{|2006-01-02T15:04:06| :date}" }, - { "src": "{|2006-01-02| :date style=long}" }, - { - "src": ".local $d = {|2006-01-02| :date style=long} {{{$d :date}}}" - }, - { - "src": ".local $t = {|2006-01-02T15:04:06| :time} {{{$t :date}}}" - } - ], - "time": [ - { "src": "{:time}", "exp": "{:time}", "errors": [{ "type": "bad-input" }] }, - { - "src": "{horse :time}", - "exp": "{|horse|}", - "errors": [{ "type": "bad-input" }] - }, - { "src": "{|2006-01-02T15:04:06| :time}" }, - { - "src": "{|2006-01-02T15:04:06| :time style=medium}" - }, - { - "src": ".local $t = {|2006-01-02T15:04:06| :time style=medium} {{{$t :time}}}" - }, - { - "src": ".local $d = {|2006-01-02T15:04:06| :date} {{{$d :time}}}" - } - ], - "datetime": [ - { - "src": "{:datetime}", - "exp": "{:datetime}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "{$x :datetime}", - "exp": "{$x}", - "params": { "x": true }, - "errors": [{ "type": "bad-input" }] - }, - { - "src": "{horse :datetime}", - "exp": "{|horse|}", - "errors": [{ "name": "RangeError" }] - }, - { "src": "{|2006-01-02T15:04:06| :datetime}" }, - { - "src": "{|2006-01-02T15:04:06| :datetime year=numeric month=|2-digit|}" - }, - { - "src": "{|2006-01-02T15:04:06| :datetime dateStyle=long}" - }, - { - "src": "{|2006-01-02T15:04:06| :datetime timeStyle=medium}" - }, - { - "src": "{$dt :datetime}", - "params": { "dt": "2006-01-02T15:04:06" } - } - ], - "integer": [ - { "src": "hello {4.2 :integer}", "exp": "hello 4" }, - { "src": "hello {-4.20 :integer}", "exp": "hello -4" }, - { "src": "hello {0.42e+1 :integer}", "exp": "hello 4" }, - { - "src": ".match {$foo :integer} one {{one}} * {{other}}", - "params": { "foo": 1.2 }, - "exp": "one" - } - ], - "number": [ - { "src": "hello {4.2 :number}", "exp": "hello 4.2" }, - { "src": "hello {-4.20 :number}", "exp": "hello -4.2" }, - { "src": "hello {0.42e+1 :number}", "exp": "hello 4.2" }, - { - "src": "hello {foo :number}", - "exp": "hello {|foo|}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "invalid number literal {.1 :number}", - "exp": "invalid number literal {|.1|}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "invalid number literal {1. :number}", - "exp": "invalid number literal {|1.|}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "invalid number literal {01 :number}", - "exp": "invalid number literal {|01|}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "invalid number literal {|+1| :number}", - "exp": "invalid number literal {|+1|}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "invalid number literal {0x1 :number}", - "exp": "invalid number literal {|0x1|}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "hello {:number}", - "exp": "hello {:number}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "hello {4.2 :number minimumFractionDigits=2}", - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=|2|}", - "exp": "hello 4.20" - }, - { - "src": "hello {4.2 :number minimumFractionDigits=$foo}", - "params": { "foo": 2 }, - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=$foo}", - "params": { "foo": "2" }, - "exp": "hello 4.20" - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.2" - }, - { - "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.20" - }, - { - "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar {$bar}", - "errors": [{ "type": "bad-option" }] - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar {$bar}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": ".input {$foo :number} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "exp": "bar 4.2" - }, - { - "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "exp": "bar 4.20" - }, - { - "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "exp": "bar {$foo}", - "errors": [{ "type": "bad-option" }] - }, - { - "src": ".input {$foo :number} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar {$foo}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": ".match {$foo :number} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 1 }, - "exp": "one one" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 2 }, - "exp": "one other" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 2, "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$foo :number} .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", - "params": { "foo": 2 }, - "exp": "{$none}", - "errors": [{ "type": "unresolved-var" }] - }, - { - "src": "{42 :number @foo @bar=13}", - "exp": "42", - "parts": [ - { "type": "number", "parts": [{ "type": "integer", "value": "42" }] } - ] - } - ], - "ordinal": [ - { - "src": ".match {$foo :ordinal} one {{st}} two {{nd}} few {{rd}} * {{th}}", - "params": { "foo": 1 }, - "exp": "th", - "errors": [{ "type": "missing-func" }, { "type": "not-selectable" }] - }, - { - "src": "hello {42 :ordinal}", - "exp": "hello {|42|}", - "errors": [{ "type": "missing-func" }] - } - ], - "plural": [ - { - "src": ".match {$foo :plural} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "other", - "errors": [{ "type": "missing-func" }, { "type": "not-selectable" }] - }, - { - "src": "hello {42 :plural}", - "exp": "hello {|42|}", - "errors": [{ "type": "missing-func" }] - } - ], - "string": [ - { - "src": ".match {$foo :string} |1| {{one}} * {{other}}", - "params": { "foo": "1" }, - "exp": "one" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": null }, - "exp": "other" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "exp": "other", - "errors": [{ "type": "unresolved-var" }] - } - ] -} diff --git a/test/test-functions.json.d.ts b/test/test-functions.json.d.ts deleted file mode 100644 index 423efb5674..0000000000 --- a/test/test-functions.json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { TestMessage } from './test-core.json'; - -declare const data: Record; -export default data; diff --git a/test/tests/data-model-errors.json b/test/tests/data-model-errors.json new file mode 100644 index 0000000000..7dbfbd66a0 --- /dev/null +++ b/test/tests/data-model-errors.json @@ -0,0 +1,169 @@ +{ + "scenario": "Data model errors", + "description": "Each `src` should produce an error when processed. Each test description indicates the error reason, but this text should not be asserted against. Error names are defined in the spec.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": ".match {$foo :x} * * {{foo}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + { + "src": ".match {$foo :x} {$bar :x} * {{foo}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + { + "src": ".match {:foo} 1 {{_}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + { + "src": ".match {:foo} other {{_}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + { + "src": ".match {:foo} {:bar} * 1 {{_}} 1 * {{_}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + { + "src": ".match {$foo} one {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + { + "src": ".input {$foo} .match {$foo} one {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + { + "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + { + "src": ".input {$foo} .input {$foo} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".input {$foo} .local $foo = {42} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {42} .input {$foo} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {:unknown} .local $foo = {42} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {$bar} .local $bar = {42} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {$foo} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {$bar} .local $bar = {$baz} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {$bar :func} .local $bar = {$baz} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {42 :func opt=$foo} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {42 :func opt=$bar} .local $bar = {42} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": "bad {:placeholder option=x option=x}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + { + "src": "bad {:placeholder ns:option=x ns:option=y}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + } + ] +} diff --git a/test/tests/functions/date.json b/test/tests/functions/date.json new file mode 100644 index 0000000000..472b46d861 --- /dev/null +++ b/test/tests/functions/date.json @@ -0,0 +1,43 @@ +{ + "scenario": "Date function", + "description": "The built-in formatter for dates.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": false + }, + "tests": [ + { + "src": "{:date}", + "exp": "{:date}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "{horse :date}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "operand-mismatch" + } + ] + }, + { + "src": "{|2006-01-02| :date}" + }, + { + "src": "{|2006-01-02T15:04:06| :date}" + }, + { + "src": "{|2006-01-02| :date style=long}" + }, + { + "src": ".local $d = {|2006-01-02| :date style=long} {{{$d :date}}}" + }, + { + "src": ".local $t = {|2006-01-02T15:04:06| :time} {{{$t :date}}}" + } + ] +} diff --git a/test/tests/functions/datetime.json b/test/tests/functions/datetime.json new file mode 100644 index 0000000000..18e0cd9744 --- /dev/null +++ b/test/tests/functions/datetime.json @@ -0,0 +1,65 @@ +{ + "scenario": "Datetime function", + "description": "The built-in formatter for datetimes.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": false + }, + "tests": [ + { + "src": "{:datetime}", + "exp": "{:datetime}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "{$x :datetime}", + "exp": "{$x}", + "params": [ + { + "name": "x", + "value": true + } + ], + "expErrors": [ + { + "type": "bad-input" + } + ] + }, + { + "src": "{horse :datetime}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "operand-mismatch" + } + ] + }, + { + "src": "{|2006-01-02T15:04:06| :datetime}" + }, + { + "src": "{|2006-01-02T15:04:06| :datetime year=numeric month=|2-digit|}" + }, + { + "src": "{|2006-01-02T15:04:06| :datetime dateStyle=long}" + }, + { + "src": "{|2006-01-02T15:04:06| :datetime timeStyle=medium}" + }, + { + "src": "{$dt :datetime}", + "params": [ + { + "type": "datetime", + "name": "dt", + "value": "2006-01-02T15:04:06" + } + ] + } + ] +} diff --git a/test/tests/functions/integer.json b/test/tests/functions/integer.json new file mode 100644 index 0000000000..1c16f54c7b --- /dev/null +++ b/test/tests/functions/integer.json @@ -0,0 +1,31 @@ +{ + "scenario": "Integer function", + "description": "The built-in formatter for integers.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": "hello {4.2 :integer}", + "exp": "hello 4" + }, + { + "src": "hello {-4.20 :integer}", + "exp": "hello -4" + }, + { + "src": "hello {0.42e+1 :integer}", + "exp": "hello 4" + }, + { + "src": ".match {$foo :integer} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1.2 + } + ], + "exp": "one" + } + ] +} diff --git a/test/tests/functions/number.json b/test/tests/functions/number.json new file mode 100644 index 0000000000..515600c458 --- /dev/null +++ b/test/tests/functions/number.json @@ -0,0 +1,415 @@ +{ + "scenario": "Number function", + "description": "The built-in formatter for numbers.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": "hello {4.2 :number}", + "exp": "hello 4.2" + }, + { + "src": "hello {-4.20 :number}", + "exp": "hello -4.2" + }, + { + "src": "hello {0.42e+1 :number}", + "exp": "hello 4.2" + }, + { + "src": "hello {foo :number}", + "exp": "hello {|foo|}", + "expErrors": [ + { + "type": "operand-mismatch" + } + ] + }, + { + "src": "invalid number literal {.1 :number}", + "exp": "invalid number literal {|.1|}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "invalid number literal {1. :number}", + "exp": "invalid number literal {|1.|}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "invalid number literal {01 :number}", + "exp": "invalid number literal {|01|}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "invalid number literal {|+1| :number}", + "exp": "invalid number literal {|+1|}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "invalid number literal {0x1 :number}", + "exp": "invalid number literal {|0x1|}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "hello {:number}", + "exp": "hello {:number}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "hello {4.2 :number minimumFractionDigits=2}", + "exp": "hello 4.20" + }, + { + "src": "hello {|4.2| :number minimumFractionDigits=|2|}", + "exp": "hello 4.20" + }, + { + "src": "hello {4.2 :number minimumFractionDigits=$foo}", + "params": [ + { + "name": "foo", + "value": 2 + } + ], + "exp": "hello 4.20" + }, + { + "src": "hello {|4.2| :number minimumFractionDigits=$foo}", + "params": [ + { + "name": "foo", + "value": "2" + } + ], + "exp": "hello 4.20" + }, + { + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": 4.2 + } + ], + "exp": "bar 4.2" + }, + { + "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": 4.2 + } + ], + "exp": "bar 4.20" + }, + { + "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": 4.2 + } + ], + "exp": "bar {$bar}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": "foo" + } + ], + "exp": "bar {$bar}", + "expErrors": [ + { + "type": "bad-input" + } + ] + }, + { + "src": ".input {$foo :number} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": 4.2 + } + ], + "exp": "bar 4.2" + }, + { + "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": 4.2 + } + ], + "exp": "bar 4.20" + }, + { + "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": 4.2 + } + ], + "exp": "bar {$foo}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": ".input {$foo :number} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar {$foo}", + "expErrors": [ + { + "type": "bad-input" + } + ] + }, + { + "src": ".match {$foo :number} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "=1" + }, + { + "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "=1" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + }, + { + "name": "bar", + "value": 1 + } + ], + "exp": "one one" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + }, + { + "name": "bar", + "value": 2 + } + ], + "exp": "one other" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": [ + { + "name": "foo", + "value": 2 + }, + { + "name": "bar", + "value": 2 + } + ], + "exp": "other" + }, + { + "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".input {$foo :number} .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 2 + } + ], + "exp": "other" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 2 + } + ], + "exp": "other" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 2 + } + ], + "exp": "other" + }, + { + "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", + "params": [ + { + "name": "foo", + "value": 2 + } + ], + "exp": "{$none}", + "expErrors": [ + { + "type": "unresolved-variable" + } + ] + }, + { + "src": "{42 :number @foo @bar=13}", + "exp": "42", + "expParts": [ + { + "type": "number", + "source": "|42|", + "parts": [ + { + "type": "integer", + "value": "42" + } + ] + } + ] + } + ] +} diff --git a/test/tests/functions/string.json b/test/tests/functions/string.json new file mode 100644 index 0000000000..e1782f1a04 --- /dev/null +++ b/test/tests/functions/string.json @@ -0,0 +1,48 @@ +{ + "scenario": "String function", + "description": "The built-in formatter for strings.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": ".match {$foo :string} |1| {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": "1" + } + ], + "exp": "one" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": null + } + ], + "exp": "other" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "exp": "other", + "expErrors": [ + { + "type": "unresolved-variable" + } + ] + } + ] +} diff --git a/test/tests/functions/time.json b/test/tests/functions/time.json new file mode 100644 index 0000000000..4c5dd22bb6 --- /dev/null +++ b/test/tests/functions/time.json @@ -0,0 +1,40 @@ +{ + "scenario": "Time function", + "description": "The built-in formatter for times.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": false + }, + "tests": [ + { + "src": "{:time}", + "exp": "{:time}", + "expErrors": [ + { + "type": "invalid-expression" + } + ] + }, + { + "src": "{horse :time}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "operand-mismatch" + } + ] + }, + { + "src": "{|2006-01-02T15:04:06| :time}" + }, + { + "src": "{|2006-01-02T15:04:06| :time style=medium}" + }, + { + "src": ".local $t = {|2006-01-02T15:04:06| :time style=medium} {{{$t :time}}}" + }, + { + "src": ".local $d = {|2006-01-02T15:04:06| :date} {{{$d :time}}}" + } + ] +} diff --git a/test/tests/syntax-errors.json b/test/tests/syntax-errors.json new file mode 100644 index 0000000000..752d67b8a8 --- /dev/null +++ b/test/tests/syntax-errors.json @@ -0,0 +1,176 @@ +{ + "scenario": "Syntax errors", + "description": "Strings that produce syntax errors when parsed.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ + { + "src": "." + }, + { + "src": "{" + }, + { + "src": "}" + }, + { + "src": "{}" + }, + { + "src": "{{" + }, + { + "src": "{{}" + }, + { + "src": "{{}}}" + }, + { + "src": "{|foo| #markup}" + }, + { + "src": "{{missing end brace}" + }, + { + "src": "{{missing end braces" + }, + { + "src": "{{missing end {$braces" + }, + { + "src": "{{extra}} content" + }, + { + "src": "empty { } placeholder" + }, + { + "src": "missing space {42:func}" + }, + { + "src": "missing space {|foo|:func}" + }, + { + "src": "missing space {|foo|@bar}" + }, + { + "src": "missing space {:func@bar}" + }, + { + "src": "missing space {:func @bar@baz}" + }, + { + "src": "missing space {:func @bar=42@baz}" + }, + { + "src": "missing space {+reserved@bar}" + }, + { + "src": "missing space {&private@bar}" + }, + { + "src": "bad {:} placeholder" + }, + { + "src": "bad {\\u0000placeholder}" + }, + { + "src": "no-equal {|42| :number minimumFractionDigits 2}" + }, + { + "src": "bad {:placeholder option=}" + }, + { + "src": "bad {:placeholder option value}" + }, + { + "src": "bad {:placeholder option:value}" + }, + { + "src": "bad {:placeholder option}" + }, + { + "src": "bad {:placeholder:}" + }, + { + "src": "bad {::placeholder}" + }, + { + "src": "bad {:placeholder::foo}" + }, + { + "src": "bad {:placeholder option:=x}" + }, + { + "src": "bad {:placeholder :option=x}" + }, + { + "src": "bad {:placeholder option::x=y}" + }, + { + "src": "bad {$placeholder option}" + }, + { + "src": "bad {:placeholder @attribute=}" + }, + { + "src": "bad {:placeholder @attribute=@foo}" + }, + { + "src": "no {placeholder end" + }, + { + "src": "no {$placeholder end" + }, + { + "src": "no {:placeholder end" + }, + { + "src": "no {|placeholder| end" + }, + { + "src": "no {|literal} end" + }, + { + "src": "no {|literal or placeholder end" + }, + { + "src": ".local bar = {|foo|} {{_}}" + }, + { + "src": ".local #bar = {|foo|} {{_}}" + }, + { + "src": ".local $bar {|foo|} {{_}}" + }, + { + "src": ".local $bar = |foo| {{_}}" + }, + { + "src": ".match {#foo} * {{foo}}" + }, + { + "src": ".match {} * {{foo}}" + }, + { + "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}" + }, + { + "src": ".match * {{foo}}" + }, + { + "src": ".match {|x| :x} * foo" + }, + { + "src": ".match {|x| :x} * {{foo}} extra" + }, + { + "src": ".match |x| * {{foo}}" + } + ] +} diff --git a/test/tests/syntax.json b/test/tests/syntax.json new file mode 100644 index 0000000000..7206e9f06b --- /dev/null +++ b/test/tests/syntax.json @@ -0,0 +1,414 @@ +{ + "scenario": "Syntax", + "description": "Test cases that do not depend on any registry definitions.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": "hello", + "exp": "hello" + }, + { + "src": "hello {world}", + "exp": "hello world" + }, + { + "src": "hello { world\t\n}", + "exp": "hello world", + "expCleanSrc": "hello {world}" + }, + { + "src": "hello {\u3000world\r}", + "exp": "hello world", + "expCleanSrc": "hello {world}" + }, + { + "src": "hello {|world|}", + "exp": "hello world" + }, + { + "src": "hello {||}", + "exp": "hello " + }, + { + "src": "hello {$place}", + "params": [ + { + "name": "place", + "value": "world" + } + ], + "exp": "hello world" + }, + { + "src": "hello {$place-.}", + "params": [ + { + "name": "place-.", + "value": "world" + } + ], + "exp": "hello world" + }, + { + "src": "hello {$place}", + "expErrors": [ + { + "type": "unresolved-variable" + } + ], + "exp": "hello {$place}" + }, + { + "src": "{$one} and {$two}", + "params": [ + { + "name": "one", + "value": 1.3 + }, + { + "name": "two", + "value": 4.2 + } + ], + "exp": "1.3 and 4.2" + }, + { + "src": "{$one} et {$two}", + "locale": "fr", + "params": [ + { + "name": "one", + "value": 1.3 + }, + { + "name": "two", + "value": 4.2 + } + ], + "exp": "1,3 et 4,2" + }, + { + "src": ".local $foo = {bar} {{bar {$foo}}}", + "exp": "bar bar" + }, + { + "src": ".local $foo = {|bar|} {{bar {$foo}}}", + "exp": "bar bar" + }, + { + "src": ".local $foo = {|bar|} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar bar" + }, + { + "src": ".local $foo = {$bar} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", + "params": [ + { + "name": "baz", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".input {$foo} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", + "params": [ + { + "name": "baz", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}", + "exp": "42 42" + }, + { + "src": "{#tag}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag" + } + ] + }, + { + "src": "{#tag}content", + "exp": "content", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag" + }, + { + "type": "literal", + "value": "content" + } + ] + }, + { + "src": "{#ns:tag}content{/ns:tag}", + "exp": "content", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "ns:tag" + }, + { + "type": "literal", + "value": "content" + }, + { + "type": "markup", + "kind": "close", + "name": "ns:tag" + } + ] + }, + { + "src": "{/tag}content", + "exp": "content", + "expParts": [ + { + "type": "markup", + "kind": "close", + "name": "tag" + }, + { + "type": "literal", + "value": "content" + } + ] + }, + { + "src": "{#tag foo=bar}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag", + "options": { + "foo": "bar" + } + } + ] + }, + { + "src": "{#tag foo=bar/}", + "expCleanSrc": "{#tag foo=bar /}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "standalone", + "name": "tag", + "options": { + "foo": "bar" + } + } + ] + }, + { + "src": "{#tag a:foo=|foo| b:bar=$bar}", + "params": [ + { + "name": "bar", + "value": "b a r" + } + ], + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag", + "options": { + "a:foo": "foo", + "b:bar": "b a r" + } + } + ] + }, + { + "src": "{/tag foo=bar}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "close", + "name": "tag", + "options": { + "foo": "bar" + } + } + ] + }, + { + "src": "{42 @foo @bar=13}", + "exp": "42", + "expParts": [ + { + "type": "literal", + "value": "42" + } + ] + }, + { + "src": "{42 @foo=$bar}", + "exp": "42", + "expParts": [ + { + "type": "literal", + "value": "42" + } + ] + }, + { + "src": "foo {+reserved}", + "exp": "foo {+}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "+" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ] + }, + { + "src": "foo {&private}", + "exp": "foo {&}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "&" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ] + }, + { + "src": "foo {?reserved @a @b=$c}", + "exp": "foo {?}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "?" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ] + }, + { + "src": ".foo {42} {{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ] + }, + { + "src": ".foo{42}{{bar}}", + "expCleanSrc": ".foo {42} {{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ] + }, + { + "src": ".foo |}lit{| {42}{{bar}}", + "expCleanSrc": ".foo |}lit{| {42} {{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ] + } + ] +} diff --git a/test/validate.py b/test/validate.py new file mode 100755 index 0000000000..3981973307 --- /dev/null +++ b/test/validate.py @@ -0,0 +1,59 @@ +#!/bin/env python3 + +import argparse +import json +import jsonschema +import logging +import os +import sys + + +def parse_args(args): + parser = argparse.ArgumentParser( + prog='Test File Schema Validator', + description='Validate all of the test case JSON files against the JSON Schema file' + ) + + parser.add_argument('--schema-dir', + help='The directory containing the JSON schema file for the test cases (dir named according to a semantic version)') + parser.add_argument('--test-dir', + default='tests', + help='The base directory in which all test files are contained') + + # ignore the executable's name from the arg list, which is in the first position + return parser.parse_args(args[1:]) + +def main(args): + # initialize logging + logging.basicConfig(format='[%(asctime)s] [%(levelname)s] %(message)s', level=logging.INFO) + + # parse args + parsed_args = parse_args(args) + + # get schema file from the schema dir + schema_dir = parsed_args.schema_dir + schema_file_paths = os.listdir(path = schema_dir) + assert len(schema_file_paths) == 1, "there should only be 1 schema(s) definition file for a given schema version" + schema_file_path = os.path.join(schema_dir, schema_file_paths[0]) + logging.debug("schema file path: %s", schema_file_path) + schema_file = open(schema_file_path, encoding='utf-8', mode='r') + schema = json.load(schema_file) + + # iterate over all files in the test case file dir and validate against the schema + test_dir = parsed_args.test_dir + for (dirpath, dirnames, filenames) in os.walk(test_dir): + for test_filename in filenames: + test_file_path = os.path.join(dirpath, test_filename) + logging.debug("test file path: %s", test_file_path) + test_file = open(test_file_path, encoding='utf-8', mode='r') + logging.debug("verifying test case file: %s", test_file_path) + tests = json.load(test_file) + jsonschema.validate(tests, schema) + + test_file.close() + schema_file.close() + + logging.info("Completed verification of all test case files successfully") + +if __name__ == "__main__": + main(sys.argv)