From 747d5eef3b049538b926a3f45ca854da7ea62359 Mon Sep 17 00:00:00 2001 From: Tim Chevalier Date: Fri, 19 Jul 2024 14:47:32 -0700 Subject: [PATCH] ICU-22834 Update tests to reflect MF2 schema in conformance repo This also updates the spec tests from the current version of the MFWG repository and removes some duplicate tests. Spec tests now reflect the message-format-wg repo as of https://github.com/unicode-org/message-format-wg/commit/5612f3b0508d63770b218d581c465aae878f5573 It also updates both the ICU4C and ICU4J parsers to follow the current test schema in the conformance repository. This includes adding code to both parsers to allow `src` to be either a single string or an array of strings (per https://github.com/unicode-org/conformance/pull/255 ), and eliminating `srcs` in tests. It also includes other changes to make updated spec tests pass: ICU4C: Allow trailing whitespace for complex messages, due to spec change ICU4C: Parse number literals correctly in Number::format ICU4J: Allow trailing whitespace after complex body, per spec change ICU4C: Fix bug that was assuming an .input variable can't have a reserved annotation ICU4C: Fix bug where unsupported '.i' was parsed as an '.input' ICU4C/ICU4J: Handle markup with space after the initial left curly brace ICU4C: Check for duplicate variant errors ICU4C/ICU4J: Handle leading whitespace in complex messages ICU4J: Treat whitespace after .input keyword as optional ICU4J: Don't format unannotated number literals as numbers --- icu4c/source/common/unicode/utypes.h | 3 +- icu4c/source/common/utypes.cpp | 3 +- icu4c/source/i18n/messageformat2_checker.cpp | 29 +- .../source/i18n/messageformat2_data_model.cpp | 34 +- icu4c/source/i18n/messageformat2_errors.cpp | 8 + icu4c/source/i18n/messageformat2_errors.h | 1 + .../i18n/messageformat2_function_registry.cpp | 41 +- icu4c/source/i18n/messageformat2_macros.h | 16 +- icu4c/source/i18n/messageformat2_parser.cpp | 232 ++--- icu4c/source/i18n/messageformat2_parser.h | 21 +- .../source/i18n/messageformat2_serializer.cpp | 8 +- icu4c/source/i18n/messageformat2_serializer.h | 3 +- .../i18n/unicode/messageformat2_data_model.h | 2 +- .../intltest/messageformat2test_read_json.cpp | 498 +++------ .../icu/message2/MFDataModelFormatter.java | 6 +- .../java/com/ibm/icu/message2/MFParser.java | 21 +- .../ibm/icu/dev/test/message2/CoreTest.java | 29 +- .../test/message2/DataModelErrorsTest.java | 44 - .../test/message2/DefaultTestProperties.java | 23 + .../dev/test/message2/FirstReleaseTests.java | 33 - .../icu/dev/test/message2/FunctionsTest.java | 38 - .../dev/test/message2/IcuFunctionsTest.java | 38 - .../ibm/icu/dev/test/message2/MF2Test.java | 24 + .../com/ibm/icu/dev/test/message2/Param.java | 23 + .../dev/test/message2/ParserSmokeTest.java | 13 +- .../SelectorsWithVariousArgumentsTest.java | 39 - .../ibm/icu/dev/test/message2/Sources.java | 26 + .../test/message2/StringToListAdapter.java | 52 + .../dev/test/message2/SyntaxErrorsTest.java | 37 - .../ibm/icu/dev/test/message2/TestUtils.java | 72 +- .../com/ibm/icu/dev/test/message2/Unit.java | 31 +- testdata/message2/README.txt | 55 +- .../message2/alias-selector-annotations.json | 12 +- testdata/message2/duplicate-declarations.json | 44 +- testdata/message2/icu-parser-tests.json | 116 +-- testdata/message2/icu-test-functions.json | 71 +- .../message2/icu-test-previous-release.json | 242 +---- testdata/message2/icu-test-selectors.json | 399 +++++-- .../invalid-number-literals-diagnostics.json | 16 +- testdata/message2/invalid-options.json | 35 +- testdata/message2/markup.json | 19 +- testdata/message2/matches-whitespace.json | 12 +- testdata/message2/more-data-model-errors.json | 235 ++++- testdata/message2/more-functions.json | 45 +- testdata/message2/more-syntax-errors.json | 5 - testdata/message2/reserved-syntax.json | 40 - testdata/message2/resolution-errors.json | 29 +- testdata/message2/runtime-errors.json | 23 +- testdata/message2/spec/data-model-errors.json | 213 +++- testdata/message2/spec/functions/date.json | 46 + .../message2/spec/functions/datetime.json | 68 ++ testdata/message2/spec/functions/integer.json | 32 + testdata/message2/spec/functions/number.json | 407 ++++++++ testdata/message2/spec/functions/string.json | 51 + testdata/message2/spec/functions/time.json | 43 + testdata/message2/spec/syntax-errors.json | 236 ++++- testdata/message2/spec/syntax.json | 970 ++++++++++++++++++ testdata/message2/spec/test-core.json | 219 ---- testdata/message2/spec/test-functions.json | 343 ------- .../spec/unsupported-expressions.json | 53 + .../message2/spec/unsupported-statements.json | 18 + .../syntax-errors-diagnostics-multiline.json | 16 +- .../message2/syntax-errors-diagnostics.json | 332 +++--- .../message2/syntax-errors-end-of-input.json | 17 +- testdata/message2/tricky-declarations.json | 24 +- testdata/message2/valid-tests.json | 65 +- 66 files changed, 3682 insertions(+), 2317 deletions(-) delete mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java create mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java delete mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java delete mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java delete mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java create mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java create mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java delete mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java create mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java create mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java delete mode 100644 icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java delete mode 100644 testdata/message2/more-syntax-errors.json delete mode 100644 testdata/message2/reserved-syntax.json create mode 100644 testdata/message2/spec/functions/date.json create mode 100644 testdata/message2/spec/functions/datetime.json create mode 100644 testdata/message2/spec/functions/integer.json create mode 100644 testdata/message2/spec/functions/number.json create mode 100644 testdata/message2/spec/functions/string.json create mode 100644 testdata/message2/spec/functions/time.json create mode 100644 testdata/message2/spec/syntax.json delete mode 100644 testdata/message2/spec/test-core.json delete mode 100644 testdata/message2/spec/test-functions.json create mode 100644 testdata/message2/spec/unsupported-expressions.json create mode 100644 testdata/message2/spec/unsupported-statements.json diff --git a/icu4c/source/common/unicode/utypes.h b/icu4c/source/common/unicode/utypes.h index ba27be222349..fcec29546af7 100644 --- a/icu4c/source/common/unicode/utypes.h +++ b/icu4c/source/common/unicode/utypes.h @@ -599,12 +599,13 @@ typedef enum UErrorCode { U_MF_OPERAND_MISMATCH_ERROR, /**< An operand provided to a function does not have the required form for that function @internal ICU 75 technology preview @deprecated This API is for technology preview only. */ U_MF_UNSUPPORTED_STATEMENT_ERROR, /**< A message includes a reserved statement. @internal ICU 75 technology preview @deprecated This API is for technology preview only. */ U_MF_UNSUPPORTED_EXPRESSION_ERROR, /**< A message includes syntax reserved for future standardization or private implementation use. @internal ICU 75 technology preview @deprecated This API is for technology preview only. */ + U_MF_DUPLICATE_VARIANT_ERROR, /**< A message includes a variant with the same key list as another variant. @internal ICU 76 technology preview @deprecated This API is for technology preview only. */ #ifndef U_HIDE_DEPRECATED_API /** * One more than the highest normal formatting API error code. * @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420. */ - U_FMT_PARSE_ERROR_LIMIT = 0x10121, + U_FMT_PARSE_ERROR_LIMIT = 0x10122, #endif // U_HIDE_DEPRECATED_API /* diff --git a/icu4c/source/common/utypes.cpp b/icu4c/source/common/utypes.cpp index 715994d67f04..2f449ffea033 100644 --- a/icu4c/source/common/utypes.cpp +++ b/icu4c/source/common/utypes.cpp @@ -141,7 +141,8 @@ _uFmtErrorName[U_FMT_PARSE_ERROR_LIMIT - U_FMT_PARSE_ERROR_START] = { "U_MF_DUPLICATE_DECLARATION_ERROR", "U_MF_OPERAND_MISMATCH_ERROR", "U_MF_UNSUPPORTED_STATEMENT_ERROR", - "U_MF_UNSUPPORTED_EXPRESSION_ERROR" + "U_MF_UNSUPPORTED_EXPRESSION_ERROR", + "U_MF_DUPLICATE_VARIANT_ERROR" }; static const char * const diff --git a/icu4c/source/i18n/messageformat2_checker.cpp b/icu4c/source/i18n/messageformat2_checker.cpp index 192167583fff..824b798410d5 100644 --- a/icu4c/source/i18n/messageformat2_checker.cpp +++ b/icu4c/source/i18n/messageformat2_checker.cpp @@ -22,6 +22,7 @@ Checks data model errors The following are checked here: Variant Key Mismatch +Duplicate Variant Missing Fallback Variant (called NonexhaustivePattern here) Missing Selector Annotation Duplicate Declaration @@ -162,6 +163,7 @@ void Checker::checkVariants(UErrorCode& status) { // Check that one variant includes only wildcards bool defaultExists = false; + bool duplicatesExist = false; for (int32_t i = 0; i < dataModel.numVariants(); i++) { const SelectorKeys& k = variants[i].getKeys(); @@ -173,10 +175,35 @@ void Checker::checkVariants(UErrorCode& status) { return; } defaultExists |= areDefaultKeys(keys, len); + + // Check if this variant's keys are duplicated by any other variant's keys + if (!duplicatesExist) { + // This check takes quadratic time, but it can be optimized if checking + // this property turns out to be a bottleneck. + for (int32_t j = 0; j < i; j++) { + const SelectorKeys& k1 = variants[j].getKeys(); + const Key* keys1 = k1.getKeysInternal(); + bool allEqual = true; + // This variant was already checked, + // so we know keys1.len == len + for (int32_t kk = 0; kk < len; kk++) { + if (!(keys[kk] == keys1[kk])) { + allEqual = false; + break; + } + } + if (allEqual) { + duplicatesExist = true; + } + } + } + } + + if (duplicatesExist) { + errors.addError(StaticErrorType::DuplicateVariant, status); } if (!defaultExists) { errors.addError(StaticErrorType::NonexhaustivePattern, status); - return; } } diff --git a/icu4c/source/i18n/messageformat2_data_model.cpp b/icu4c/source/i18n/messageformat2_data_model.cpp index f7368b3808b8..a8762e4004f6 100644 --- a/icu4c/source/i18n/messageformat2_data_model.cpp +++ b/icu4c/source/i18n/messageformat2_data_model.cpp @@ -186,6 +186,9 @@ bool Key::operator==(const Key& other) const { if (isWildcard()) { return other.isWildcard(); } + if (other.isWildcard()) { + return false; + } return (asLiteral() == other.asLiteral()); } @@ -833,23 +836,19 @@ const Expression& Binding::getValue() const { } else { const Operator* rator = rhs.getOperator(errorCode); bool hasOperator = U_SUCCESS(errorCode); - if (hasOperator && rator->isReserved()) { - errorCode = U_INVALID_STATE_ERROR; + // Clear error code -- the "error" from the absent operator + // is handled + errorCode = U_ZERO_ERROR; + b = Binding(variableName, std::move(rhs)); + b.local = false; + if (hasOperator) { + rator = b.getValue().getOperator(errorCode); + U_ASSERT(U_SUCCESS(errorCode)); + b.annotation = &rator->contents; } else { - // Clear error code -- the "error" from the absent operator - // is handled - errorCode = U_ZERO_ERROR; - b = Binding(variableName, std::move(rhs)); - b.local = false; - if (hasOperator) { - rator = b.getValue().getOperator(errorCode); - U_ASSERT(U_SUCCESS(errorCode)); - b.annotation = std::get_if(&(rator->contents)); - } else { - b.annotation = nullptr; - } - U_ASSERT(!hasOperator || b.annotation != nullptr); + b.annotation = nullptr; } + U_ASSERT(!hasOperator || b.annotation != nullptr); } } return b; @@ -857,7 +856,8 @@ const Expression& Binding::getValue() const { const OptionMap& Binding::getOptionsInternal() const { U_ASSERT(annotation != nullptr); - return annotation->getOptions(); + U_ASSERT(std::holds_alternative(*annotation)); + return std::get_if(annotation)->getOptions(); } void Binding::updateAnnotation() { @@ -867,7 +867,7 @@ void Binding::updateAnnotation() { return; } U_ASSERT(U_SUCCESS(localErrorCode) && !rator->isReserved()); - annotation = std::get_if(&(rator->contents)); + annotation = &rator->contents; } Binding::Binding(const Binding& other) : var(other.var), expr(other.expr), local(other.local) { diff --git a/icu4c/source/i18n/messageformat2_errors.cpp b/icu4c/source/i18n/messageformat2_errors.cpp index cbb9e1497d69..1fb31377cd1c 100644 --- a/icu4c/source/i18n/messageformat2_errors.cpp +++ b/icu4c/source/i18n/messageformat2_errors.cpp @@ -135,6 +135,10 @@ namespace message2 { status = U_MF_VARIANT_KEY_MISMATCH_ERROR; break; } + case StaticErrorType::DuplicateVariant: { + status = U_MF_DUPLICATE_VARIANT_ERROR; + break; + } case StaticErrorType::NonexhaustivePattern: { status = U_MF_NONEXHAUSTIVE_PATTERN_ERROR; break; @@ -211,6 +215,10 @@ namespace message2 { dataModelError = true; break; } + case StaticErrorType::DuplicateVariant: { + dataModelError = true; + break; + } case StaticErrorType::NonexhaustivePattern: { dataModelError = true; break; diff --git a/icu4c/source/i18n/messageformat2_errors.h b/icu4c/source/i18n/messageformat2_errors.h index ef2ad20faddb..604c8bcf57ac 100644 --- a/icu4c/source/i18n/messageformat2_errors.h +++ b/icu4c/source/i18n/messageformat2_errors.h @@ -54,6 +54,7 @@ namespace message2 { enum StaticErrorType { DuplicateDeclarationError, DuplicateOptionName, + DuplicateVariant, MissingSelectorAnnotation, NonexhaustivePattern, SyntaxError, diff --git a/icu4c/source/i18n/messageformat2_function_registry.cpp b/icu4c/source/i18n/messageformat2_function_registry.cpp index e9bbc03e737e..17955760ecfb 100644 --- a/icu4c/source/i18n/messageformat2_function_registry.cpp +++ b/icu4c/source/i18n/messageformat2_function_registry.cpp @@ -7,11 +7,14 @@ #if !UCONFIG_NO_MF2 +#include + #include "unicode/dtptngen.h" #include "unicode/messageformat2_data_model_names.h" #include "unicode/messageformat2_function_registry.h" #include "unicode/smpdtfmt.h" #include "charstr.h" +#include "double-conversion.h" #include "messageformat2_allocation.h" #include "messageformat2_function_registry_internal.h" #include "messageformat2_macros.h" @@ -421,12 +424,11 @@ static FormattedPlaceholder notANumber(const FormattedPlaceholder& input) { return FormattedPlaceholder(input, FormattedValue(UnicodeString("NaN"))); } -static FormattedPlaceholder stringAsNumber(const number::LocalizedNumberFormatter& nf, const FormattedPlaceholder& input, UErrorCode& errorCode) { +static double parseNumberLiteral(const FormattedPlaceholder& input, UErrorCode& errorCode) { if (U_FAILURE(errorCode)) { return {}; } - double numberValue; // Copying string to avoid GCC dangling-reference warning // (although the reference is safe) UnicodeString inputStr = input.asFormattable().getString(errorCode); @@ -434,12 +436,39 @@ static FormattedPlaceholder stringAsNumber(const number::LocalizedNumberFormatte if (U_FAILURE(errorCode)) { return {}; } - UErrorCode localErrorCode = U_ZERO_ERROR; - strToDouble(inputStr, numberValue, localErrorCode); - if (U_FAILURE(localErrorCode)) { + + // Hack: Check for cases that are forbidden by the MF2 grammar + // but allowed by StringToDouble + int32_t len = inputStr.length(); + + if (len > 0 && ((inputStr[0] == '+') + || (inputStr[0] == '0' && len > 1 && inputStr[1] != '.') + || (inputStr[len - 1] == '.') + || (inputStr[0] == '.'))) { errorCode = U_MF_OPERAND_MISMATCH_ERROR; + return 0; + } + + // Otherwise, convert to double using double_conversion::StringToDoubleConverter + using namespace double_conversion; + int processedCharactersCount = 0; + StringToDoubleConverter converter(0, 0, 0, "", ""); + double result = + converter.StringToDouble(reinterpret_cast(inputStr.getBuffer()), + len, + &processedCharactersCount); + if (processedCharactersCount != len) { + errorCode = U_MF_OPERAND_MISMATCH_ERROR; + } + return result; +} + +static FormattedPlaceholder tryParsingNumberLiteral(const number::LocalizedNumberFormatter& nf, const FormattedPlaceholder& input, UErrorCode& errorCode) { + double numberValue = parseNumberLiteral(input, errorCode); + if (U_FAILURE(errorCode)) { return notANumber(input); } + UErrorCode savedStatus = errorCode; number::FormattedNumber result = nf.formatDouble(numberValue, errorCode); // Ignore U_USING_DEFAULT_WARNING @@ -590,7 +619,7 @@ FormattedPlaceholder StandardFunctions::Number::format(FormattedPlaceholder&& ar } case UFMT_STRING: { // Try to parse the string as a number - return stringAsNumber(realFormatter, arg, errorCode); + return tryParsingNumberLiteral(realFormatter, arg, errorCode); } default: { // Other types can't be parsed as a number diff --git a/icu4c/source/i18n/messageformat2_macros.h b/icu4c/source/i18n/messageformat2_macros.h index ee8cf0779e6b..f06ed1a5a977 100644 --- a/icu4c/source/i18n/messageformat2_macros.h +++ b/icu4c/source/i18n/messageformat2_macros.h @@ -60,19 +60,11 @@ using namespace pluralimpl; // Fallback #define REPLACEMENT ((UChar32) 0xFFFD) -// MessageFormat2 uses four keywords: `.input`, `.local`, `.when`, and `.match`. +// MessageFormat2 uses three keywords: `.input`, `.local`, and `.match`. -static constexpr UChar32 ID_INPUT[] = { - 0x2E, 0x69, 0x6E, 0x70, 0x75, 0x74, 0 /* ".input" */ -}; - -static constexpr UChar32 ID_LOCAL[] = { - 0x2E, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0 /* ".local" */ -}; - -static constexpr UChar32 ID_MATCH[] = { - 0x2E, 0x6D, 0x61, 0x74, 0x63, 0x68, 0 /* ".match" */ -}; +static constexpr std::u16string_view ID_INPUT = u".input"; +static constexpr std::u16string_view ID_LOCAL = u".local"; +static constexpr std::u16string_view ID_MATCH = u".match"; // Returns immediately if `errorCode` indicates failure #define CHECK_ERROR(errorCode) \ diff --git a/icu4c/source/i18n/messageformat2_parser.cpp b/icu4c/source/i18n/messageformat2_parser.cpp index fe5bfb484803..0635074f69c3 100644 --- a/icu4c/source/i18n/messageformat2_parser.cpp +++ b/icu4c/source/i18n/messageformat2_parser.cpp @@ -456,12 +456,11 @@ void Parser::parseToken(UChar32 c, UErrorCode& errorCode) { the string beginning at `source[index]` No postcondition -- a message can end with a '}' token */ -template -void Parser::parseToken(const UChar32 (&token)[N], UErrorCode& errorCode) { +void Parser::parseToken(const std::u16string_view& token, UErrorCode& errorCode) { U_ASSERT(inBounds(source, index)); int32_t tokenPos = 0; - while (tokenPos < N - 1) { + while (tokenPos < (int32_t) token.length()) { if (source[index] != token[tokenPos]) { ERROR(parseError, errorCode, index); return; @@ -478,13 +477,12 @@ void Parser::parseToken(const UChar32 (&token)[N], UErrorCode& errorCode) { the string beginning at `source[index']`), then consumes optional whitespace again */ -template -void Parser::parseTokenWithWhitespace(const UChar32 (&token)[N], UErrorCode& errorCode) { +void Parser::parseTokenWithWhitespace(const std::u16string_view& token, UErrorCode& errorCode) { // No need for error check or bounds check before parseOptionalWhitespace parseOptionalWhitespace(errorCode); // Establish precondition CHECK_BOUNDS(source, index, parseError, errorCode); - parseToken(token); + parseToken(token, errorCode); parseOptionalWhitespace(errorCode); // Guarantee postcondition CHECK_BOUNDS(source, index, parseError, errorCode); @@ -641,80 +639,40 @@ FunctionName Parser::parseFunction(UErrorCode& errorCode) { Precondition: source[index] == BACKSLASH Consume an escaped character. + Corresponds to `escaped-char` in the grammar. - Generalized to handle `reserved-escape`, `text-escape`, - or `literal-escape`, depending on the `kind` argument. - - Appends result to `str` + No postcondition (a message can end with an escaped char) */ -void Parser::parseEscapeSequence(EscapeKind kind, - UnicodeString &str, - UErrorCode& errorCode) { +UnicodeString Parser::parseEscapeSequence(UErrorCode& errorCode) { U_ASSERT(inBounds(source, index)); U_ASSERT(source[index] == BACKSLASH); normalizedInput += BACKSLASH; index++; // Skip the initial backslash - CHECK_BOUNDS(source, index, parseError, errorCode); - - #define SUCCEED \ - /* Append to the output string */ \ - str += source[index]; \ - /* Update normalizedInput */ \ - normalizedInput += source[index]; \ - /* Consume the character */ \ - index++; \ - /* Guarantee postcondition */ \ - CHECK_BOUNDS(source, index, parseError, errorCode); \ - return; - - // Expect a '{', '|' or '}' - switch (source[index]) { - case LEFT_CURLY_BRACE: - case RIGHT_CURLY_BRACE: { - // Allowed in a `text-escape` or `reserved-escape` - switch (kind) { - case TEXT: - case RESERVED: { - SUCCEED; + UnicodeString str; + if (inBounds(source, index)) { + // Expect a '{', '|' or '}' + switch (source[index]) { + case LEFT_CURLY_BRACE: + case RIGHT_CURLY_BRACE: + case PIPE: + case BACKSLASH: { + /* Append to the output string */ + str += source[index]; + /* Update normalizedInput */ + normalizedInput += source[index]; + /* Consume the character */ + index++; + return str; } default: { + // No other characters are allowed here break; } } - break; } - case PIPE: { - // Allowed in a `literal-escape` or `reserved-escape` - switch (kind) { - case LITERAL: - case RESERVED: { - SUCCEED; - } - default: { - break; - } - } - break; - } - case BACKSLASH: { - // Allowed in any escape sequence - SUCCEED; - } - default: { - // No other characters are allowed here - break; - } - } // If control reaches here, there was an error ERROR(parseError, errorCode, index); -} - -/* - Consume an escaped pipe or backslash, matching the `literal-escape` - nonterminal in the grammar -*/ -void Parser::parseLiteralEscape(UnicodeString &str, UErrorCode& errorCode) { - parseEscapeSequence(LITERAL, str, errorCode); + return str; } @@ -736,7 +694,7 @@ Literal Parser::parseQuotedLiteral(UErrorCode& errorCode) { bool done = false; while (!done) { if (source[index] == BACKSLASH) { - parseLiteralEscape(contents, errorCode); + contents += parseEscapeSequence(errorCode); } else if (isQuotedChar(source[index])) { contents += source[index]; normalizedInput += source[index]; @@ -1142,10 +1100,6 @@ Arbitrary lookahead is required to parse attribute lists, similarly to option li } } -void Parser::parseReservedEscape(UnicodeString &str, UErrorCode& errorCode) { - parseEscapeSequence(RESERVED, str, errorCode); -} - /* Consumes a non-empty sequence of reserved-chars, reserved-escapes, and literals (as in 1*(reserved-char / reserved-escape / literal) in the `reserved-body` rule) @@ -1177,8 +1131,7 @@ void Parser::parseReservedChunk(Reserved::Builder& result, UErrorCode& status) { if (source[index] == BACKSLASH) { // reserved-escape - parseReservedEscape(chunk, status); - result.add(Literal(false, chunk), status); + result.add(Literal(false, parseEscapeSequence(status)), status); chunk.setTo(u"", 0); } else if (source[index] == PIPE || isUnquotedStart(source[index])) { result.add(parseLiteral(status), status); @@ -1718,15 +1671,17 @@ void Parser::parseUnsupportedStatement(UErrorCode& status) { dataModel.addUnsupportedStatement(builder.build(status), status); } -// Terrible hack to get around the ambiguity between `matcher` and `reserved-statement` -bool Parser::nextIsMatch() const { - for(int32_t i = 0; i < 6; i++) { - if (!inBounds(source, index + i) || source[index + i] != ID_MATCH[i]) { +// Terrible hack to get around the ambiguity between unsupported keywords +// and supported keywords +bool Parser::nextIs(const std::u16string_view &keyword) const { + for(int32_t i = 0; i < (int32_t) keyword.length(); i++) { + if (!inBounds(source, index + i) || source[index + i] != keyword[i]) { return false; } } return true; } + /* Consume a possibly-empty sequence of declarations separated by whitespace; each declaration matches the `declaration` nonterminal in the grammar @@ -1740,19 +1695,17 @@ void Parser::parseDeclarations(UErrorCode& status) { while (source[index] == PERIOD) { CHECK_BOUNDS(source, index + 1, parseError, status); - if (source[index + 1] == ID_LOCAL[1]) { + // Check for unsupported statements first + // Lookahead is needed to disambiguate keyword from known keywords + if (!nextIs(ID_MATCH) && !nextIs(ID_LOCAL) && !nextIs(ID_INPUT)) { + parseUnsupportedStatement(status); + } else if (source[index + 1] == ID_LOCAL[1]) { parseLocalDeclaration(status); } else if (source[index + 1] == ID_INPUT[1]) { parseInputDeclaration(status); } else { - // Unsupported statement - // Lookahead is needed to disambiguate this from a `match` - if (!nextIsMatch()) { - parseUnsupportedStatement(status); - } else { - // Done parsing declarations - break; - } + // Done parsing declarations + break; } // Avoid looping infinitely @@ -1765,49 +1718,22 @@ void Parser::parseDeclarations(UErrorCode& status) { } /* - Consume an escaped curly brace, or backslash, matching the `text-escape` - nonterminal in the grammar -*/ -void Parser::parseTextEscape(UnicodeString &str, UErrorCode& status) { - parseEscapeSequence(TEXT, str, status); -} - -/* - Consume a non-empty sequence of text characters and escaped text characters, - matching the `text` nonterminal in the grammar + Consume a text character + matching the `text-char` nonterminal in the grammar - No postcondition (a message can end with a text) + No postcondition (a message can end with a text-char) */ -UnicodeString Parser::parseText(UErrorCode& status) { +UnicodeString Parser::parseTextChar(UErrorCode& status) { UnicodeString str; - if (!inBounds(source, index)) { - // Text can be empty - return str; - } - - if (!(isTextChar(source[index] || source[index] == BACKSLASH))) { - // Error -- text is expected here + if (!inBounds(source, index) || !(isTextChar(source[index]))) { + // Error -- text-char is expected here ERROR(parseError, status, index); - return str; - } - - while (true) { - if (source[index] == BACKSLASH) { - parseTextEscape(str, status); - } else if (isTextChar(source[index])) { - normalizedInput += source[index]; - str += source[index]; - index++; - maybeAdvanceLine(); - } else { - break; - } - if (!inBounds(source, index)) { - // OK for text to end a message - break; - } + } else { + normalizedInput += source[index]; + str += source[index]; + index++; + maybeAdvanceLine(); } - return str; } @@ -2026,9 +1952,22 @@ std::variant Parser::parsePlaceholder(UErrorCode& status) { return exprFallback(status); } - // Check if it's markup or an expression - if (source[index + 1] == NUMBER_SIGN || source[index + 1] == SLASH) { - // Markup + // Need to look ahead arbitrarily since can appear before the '{' and '#' + // in markup + int32_t tempIndex = index + 1; + bool isMarkup = false; + while (inBounds(source, tempIndex)) { + if (source[tempIndex] == NUMBER_SIGN || source[tempIndex] == SLASH) { + isMarkup = true; + break; + } + if (!isWhitespace(source[tempIndex])){ + break; + } + tempIndex++; + } + + if (isMarkup) { return parseMarkup(status); } return parseExpression(status); @@ -2058,9 +1997,18 @@ Pattern Parser::parseSimpleMessage(UErrorCode& status) { } break; } + case BACKSLASH: { + // Must be escaped-char + result.add(parseEscapeSequence(status), status); + break; + } + case RIGHT_CURLY_BRACE: { + // Distinguish unescaped '}' from end of quoted pattern + break; + } default: { - // Must be text - result.add(parseText(status), status); + // Must be text-char + result.add(parseTextChar(status), status); break; } } @@ -2232,21 +2180,31 @@ void Parser::parseBody(UErrorCode& status) { void Parser::parse(UParseError &parseErrorResult, UErrorCode& status) { CHECK_ERROR(status); - bool simple = true; - // Message can be empty, so we need to only look ahead - // if we know it's non-empty + bool complex = false; + // First, "look ahead" to determine if this is a simple or complex + // message. To do that, check the first non-whitespace character. + while (inBounds(source, index) && isWhitespace(source[index])) { + index++; + } if (inBounds(source, index)) { if (source[index] == PERIOD || (index < static_cast(source.length()) + 1 && source[index] == LEFT_CURLY_BRACE && source[index + 1] == LEFT_CURLY_BRACE)) { - // A complex message begins with a '.' or '{' - parseDeclarations(status); - parseBody(status); - simple = false; + complex = true; } } - if (simple) { + // Reset index + index = 0; + + // Message can be empty, so we need to only look ahead + // if we know it's non-empty + if (complex) { + parseOptionalWhitespace(status); + parseDeclarations(status); + parseBody(status); + parseOptionalWhitespace(status); + } else { // Simple message // For normalization, quote the pattern normalizedInput += LEFT_CURLY_BRACE; diff --git a/icu4c/source/i18n/messageformat2_parser.h b/icu4c/source/i18n/messageformat2_parser.h index 92c0475d67db..9367c0e981dd 100644 --- a/icu4c/source/i18n/messageformat2_parser.h +++ b/icu4c/source/i18n/messageformat2_parser.h @@ -91,10 +91,6 @@ namespace message2 { parseError.postContext[0] = '\0'; } - // Used so `parseEscapeSequence()` can handle all types of escape sequences - // (literal, text, and reserved) - typedef enum { LITERAL, TEXT, RESERVED } EscapeKind; - static void translateParseError(const MessageParseError&, UParseError&); static void setParseError(MessageParseError&, uint32_t); void maybeAdvanceLine(); @@ -111,19 +107,16 @@ namespace message2 { void parseOptionalWhitespace(UErrorCode&); void parseToken(UChar32, UErrorCode&); void parseTokenWithWhitespace(UChar32, UErrorCode&); - template - void parseToken(const UChar32 (&)[N], UErrorCode&); - template - void parseTokenWithWhitespace(const UChar32 (&)[N], UErrorCode&); - bool nextIsMatch() const; + void parseToken(const std::u16string_view&, UErrorCode&); + void parseTokenWithWhitespace(const std::u16string_view&, UErrorCode&); + bool nextIs(const std::u16string_view&) const; UnicodeString parseName(UErrorCode&); UnicodeString parseIdentifier(UErrorCode&); UnicodeString parseDigits(UErrorCode&); VariableName parseVariableName(UErrorCode&); FunctionName parseFunction(UErrorCode&); - void parseEscapeSequence(EscapeKind, UnicodeString&, UErrorCode&); - void parseLiteralEscape(UnicodeString&, UErrorCode&); - Literal parseUnquotedLiteral(UErrorCode&); + UnicodeString parseEscapeSequence(UErrorCode&); + Literal parseUnquotedLiteral(UErrorCode&); Literal parseQuotedLiteral(UErrorCode&); Literal parseLiteral(UErrorCode&); template @@ -134,7 +127,6 @@ namespace message2 { void parseOption(OptionAdder&, UErrorCode&); template void parseOptions(OptionAdder&, UErrorCode&); - void parseReservedEscape(UnicodeString&, UErrorCode&); void parseReservedChunk(Reserved::Builder&, UErrorCode&); Reserved parseReserved(UErrorCode&); Reserved parseReservedBody(Reserved::Builder&, UErrorCode&); @@ -143,8 +135,7 @@ namespace message2 { Markup parseMarkup(UErrorCode&); Expression parseExpression(UErrorCode&); std::variant parsePlaceholder(UErrorCode&); - void parseTextEscape(UnicodeString&, UErrorCode&); - UnicodeString parseText(UErrorCode&); + UnicodeString parseTextChar(UErrorCode&); Key parseKey(UErrorCode&); SelectorKeys parseNonEmptyKeys(UErrorCode&); void errorPattern(UErrorCode& status); diff --git a/icu4c/source/i18n/messageformat2_serializer.cpp b/icu4c/source/i18n/messageformat2_serializer.cpp index 23c123e6f116..2d007fa4bf5a 100644 --- a/icu4c/source/i18n/messageformat2_serializer.cpp +++ b/icu4c/source/i18n/messageformat2_serializer.cpp @@ -35,12 +35,8 @@ void Serializer::emit(const UnicodeString& s) { result += s; } -template -void Serializer::emit(const UChar32 (&token)[N]) { - // Don't emit the terminator - for (int32_t i = 0; i < N - 1; i++) { - emit(token[i]); - } +void Serializer::emit(const std::u16string_view& token) { + result.append(token); } void Serializer::emit(const Literal& l) { diff --git a/icu4c/source/i18n/messageformat2_serializer.h b/icu4c/source/i18n/messageformat2_serializer.h index 4b72d1ca715f..38d3412665a4 100644 --- a/icu4c/source/i18n/messageformat2_serializer.h +++ b/icu4c/source/i18n/messageformat2_serializer.h @@ -38,8 +38,7 @@ namespace message2 { void whitespace(); void emit(UChar32); - template - void emit(const UChar32 (&)[N]); + void emit(const std::u16string_view&); void emit(const UnicodeString&); void emit(const Literal&); void emit(const Key&); diff --git a/icu4c/source/i18n/unicode/messageformat2_data_model.h b/icu4c/source/i18n/unicode/messageformat2_data_model.h index 942a03f73591..65dc836caeb3 100644 --- a/icu4c/source/i18n/unicode/messageformat2_data_model.h +++ b/icu4c/source/i18n/unicode/messageformat2_data_model.h @@ -2599,7 +2599,7 @@ namespace message2 { // If non-null, the referent is a member of `expr` so // its lifetime is the same as the lifetime of the enclosing Binding // (as long as there's no mutation) - const Callable* annotation = nullptr; + const std::variant* annotation = nullptr; const OptionMap& getOptionsInternal() const; diff --git a/icu4c/source/test/intltest/messageformat2test_read_json.cpp b/icu4c/source/test/intltest/messageformat2test_read_json.cpp index 33e65a92ce85..0c65764e7fb5 100644 --- a/icu4c/source/test/intltest/messageformat2test_read_json.cpp +++ b/icu4c/source/test/intltest/messageformat2test_read_json.cpp @@ -18,52 +18,44 @@ using namespace nlohmann; using namespace icu::message2; -static UErrorCode getExpectedErrorFromString(const std::string& errorName) { - if (errorName == "Variant Key Mismatch") { +static UErrorCode getExpectedRuntimeErrorFromString(const std::string& errorName) { + if (errorName == "syntax-error") { + return U_MF_SYNTAX_ERROR; + } + if (errorName == "variant-key-mismatch") { return U_MF_VARIANT_KEY_MISMATCH_ERROR; } - if (errorName == "Missing Fallback Variant") { + if (errorName == "missing-fallback-variant") { return U_MF_NONEXHAUSTIVE_PATTERN_ERROR; } - if (errorName == "Missing Selector Annotation") { + if (errorName == "missing-selector-annotation") { return U_MF_MISSING_SELECTOR_ANNOTATION_ERROR; } - if (errorName == "Duplicate Declaration") { - return U_MF_DUPLICATE_DECLARATION_ERROR; - } - if (errorName == "Unsupported Statement") { - return U_MF_UNSUPPORTED_STATEMENT_ERROR; - } -// Arbitrary default - return U_MF_DUPLICATE_OPTION_NAME_ERROR; -} - -static UErrorCode getExpectedRuntimeErrorFromString(const std::string& errorName) { - if (errorName == "parse-error" || errorName == "empty-token" || errorName == "extra-content") { - return U_MF_SYNTAX_ERROR; - } - if (errorName == "key-mismatch") { - return U_MF_VARIANT_KEY_MISMATCH_ERROR; - } - if (errorName == "missing-var" || errorName == "unresolved-var") { + if (errorName == "unresolved-variable") { return U_MF_UNRESOLVED_VARIABLE_ERROR; } - if (errorName == "unsupported-annotation") { + if (errorName == "unsupported-expression") { return U_MF_UNSUPPORTED_EXPRESSION_ERROR; } - if (errorName == "bad-input" || errorName == "RangeError") { + if (errorName == "bad-operand") { return U_MF_OPERAND_MISMATCH_ERROR; } if (errorName == "bad-option") { return U_MF_FORMATTING_ERROR; } - if (errorName == "missing-func") { + if (errorName == "unknown-function") { return U_MF_UNKNOWN_FUNCTION_ERROR; } if (errorName == "duplicate-declaration") { return U_MF_DUPLICATE_DECLARATION_ERROR; } - if (errorName == "selector-error") { + if (errorName == "duplicate-option-name") { + return U_MF_DUPLICATE_OPTION_NAME_ERROR; + } + if (errorName == "duplicate-variant") { + return U_MF_DUPLICATE_VARIANT_ERROR; + } + if (errorName == "bad-selector") { return U_MF_SELECTOR_ERROR; } if (errorName == "formatting-error") { @@ -88,56 +80,86 @@ static void makeTestName(char* buffer, size_t size, std::string fileName, int32_ snprintf(buffer, size, "test from file: %s[%u]", fileName.c_str(), ++testNum); } -static bool setArguments(TestCase::Builder& test, const json::object_t& params, UErrorCode& errorCode) { +static bool setArguments(TestMessageFormat2& t, + TestCase::Builder& test, + const std::vector& params, + UErrorCode& errorCode) { if (U_FAILURE(errorCode)) { return true; } + bool schemaError = false; for (auto argsIter = params.begin(); argsIter != params.end(); ++argsIter) { - const UnicodeString argName = u_str(argsIter->first); - // Determine type of value - if (argsIter->second.is_number()) { - test.setArgument(argName, - argsIter->second.template get()); - } else if (argsIter->second.is_string()) { - test.setArgument(argName, - u_str(argsIter->second.template get())); - } else if (argsIter->second.is_object()) { - // Dates: represent in tests as { "date" : timestamp }, to distinguish - // from number values - auto obj = argsIter->second.template get(); - if (obj["date"].is_number()) { - test.setDateArgument(argName, obj["date"]); - } else if (obj["decimal"].is_string()) { - // Decimal strings: represent in tests as { "decimal" : string }, - // to distinguish from string values - test.setDecimalArgument(argName, obj["decimal"].template get(), errorCode); + auto j_object = argsIter->template get(); + if (!j_object["name"].is_null()) { + const UnicodeString argName = u_str(j_object["name"].template get()); + if (!j_object["value"].is_null()) { + json val = j_object["value"]; + // Determine type of value + if (val.is_number()) { + test.setArgument(argName, + val.template get()); + } else if (val.is_string()) { + test.setArgument(argName, + u_str(val.template get())); + } else if (val.is_object()) { + // Dates: represent in tests as { "date" : timestamp }, to distinguish + // from number values + auto obj = val.template get(); + if (obj["date"].is_number()) { + test.setDateArgument(argName, val["date"]); + } else if (obj["decimal"].is_string()) { + // Decimal strings: represent in tests as { "decimal" : string }, + // to distinguish from string values + test.setDecimalArgument(argName, obj["decimal"].template get(), errorCode); + } + } else if (val.is_boolean() || val.is_null()) { + return false; // For now, boolean and null arguments are unsupported + } + } else { + schemaError = true; + break; } - } else if (argsIter->second.is_boolean() || argsIter->second.is_null()) { - return false; // For now, boolean and null arguments are unsupported + } else { + schemaError = true; + break; + } + } + if (schemaError) { + t.logln("Warning: test with missing 'name' or 'value' in params"); + if (U_SUCCESS(errorCode)) { + errorCode = U_ILLEGAL_ARGUMENT_ERROR; } } return true; } + +/* + Test files are expected to follow the schema in: + https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json + as of https://github.com/unicode-org/conformance/pull/255 +*/ static void runValidTest(TestMessageFormat2& icuTest, const std::string& testName, + const std::string& defaultError, const json& j, IcuTestErrorCode& errorCode) { auto j_object = j.template get(); std::string messageText; + // src can be a single string or an array of strings if (!j_object["src"].is_null()) { - messageText = j_object["src"].template get(); - } else { - if (!j_object["srcs"].is_null()) { - auto strings = j_object["srcs"].template get>(); + if (j_object["src"].is_string()) { + messageText = j_object["src"].template get(); + } else { + auto strings = j_object["src"].template get>(); for (const auto &piece : strings) { messageText += piece; } } - // Otherwise, it should probably be an error, but we just - // treat this as the empty string } + // Otherwise, it should probably be an error, but we just + // treat this as the empty string TestCase::Builder test = successTest(testName, messageText); @@ -160,16 +182,17 @@ static void runValidTest(TestMessageFormat2& icuTest, } if (!j_object["params"].is_null()) { - // Map from string to json - auto params = j_object["params"].template get(); - if (!setArguments(test, params, errorCode)) { + // `params` is an array of objects + auto params = j_object["params"].template get>(); + if (!setArguments(icuTest, test, params, errorCode)) { return; // Skip tests with unsupported arguments } } - if (!j_object["errors"].is_null()) { + bool expectedError = false; + if (!j_object["expErrors"].is_null()) { // Map from string to string - auto errors = j_object["errors"].template get>>(); + auto errors = j_object["expErrors"].template get>>(); // We only emit the first error, so we just hope the first error // in the list in the test is also the error we emit U_ASSERT(errors.size() > 0); @@ -182,22 +205,18 @@ static void runValidTest(TestMessageFormat2& icuTest, return; } test.setExpectedError(getExpectedRuntimeErrorFromString(errorType)); + expectedError = true; + } else if (defaultError.length() > 0) { + test.setExpectedError(getExpectedRuntimeErrorFromString(defaultError)); + expectedError = true; } - TestCase t = test.build(); - TestUtils::runTestCase(icuTest, t, errorCode); -} - -static void runSyntaxErrorTest(TestMessageFormat2& icuTest, - const std::string& testName, - const json& j, - IcuTestErrorCode& errorCode) { - auto messageText = j["src"].template get(); - TestCase::Builder test = successTest(testName, messageText) - .setExpectedError(U_MF_SYNTAX_ERROR); - - auto j_object = j.template get(); + // If no expected result and no error, then set the test builder to expect success + if (j_object["exp"].is_null() && !expectedError) { + test.setNoSyntaxError(); + } + // Check for expected diagnostic values int32_t lineNumber = 0; int32_t offset = -1; if (!j_object["char"].is_null()) { @@ -209,117 +228,14 @@ static void runSyntaxErrorTest(TestMessageFormat2& icuTest, if (offset != -1) { test.setExpectedLineNumberAndOffset(lineNumber, offset); } - TestCase t = test.build(); - TestUtils::runTestCase(icuTest, t, errorCode); -} - -// File name is relative to message2/ in the test data directory -static void runICU4JSyntaxTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string testFileName(testDataDirectory); - testFileName.append("message2/"); - testFileName.append(fileName); - std::ifstream testFile(testFileName); - json data = json::parse(testFile); - - // Map from string to json, where the strings are the function names - auto tests = data.template get(); - for (auto iter = tests.begin(); iter != tests.end(); ++iter) { - int32_t testNum = 0; - auto categoryName = iter->first; - t.logln("ICU4J syntax tests:"); - t.logln(u_str(iter->second.dump())); - - // Array of tests - auto testsForThisCategory = iter->second.template get>(); - - TestCase::Builder test; - test.setNoSyntaxError(); - for (auto testsIter = testsForThisCategory.begin(); - testsIter != testsForThisCategory.end(); - ++testsIter) { - char testName[100]; - makeTestName(testName, sizeof(testName), categoryName, ++testNum); - t.logln(testName); - - // Tests here are just strings, and we test that they run without syntax errors - test.setPattern(u_str(*testsIter)); - TestCase testCase = test.build(); - TestUtils::runTestCase(t, testCase, errorCode); - } - - } -} - -// File name is relative to message2/ in the test data directory -static void runICU4JSelectionTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string testFileName(testDataDirectory); - testFileName.append("message2/"); - testFileName.append(fileName); - std::ifstream testFile(testFileName); - json data = json::parse(testFile); - - int32_t testNum = 0; - for (auto iter = data.begin(); iter != data.end(); ++iter) { - // Each test has a "shared" and a "variations" field - auto j_object = iter->get(); - auto shared = j_object["shared"]; - auto variations = j_object["variations"]; - // Skip ignored tests - if (!j_object["ignoreCpp"].is_null()) { - return; - } - - // shared has a "srcs" field - auto strings = shared["srcs"].template get>(); - std::string messageText; - for (const auto &piece : strings) { - messageText += piece; - } - - t.logln(u_str("ICU4J selectors tests: " + fileName)); - t.logln(u_str(iter->dump())); - - TestCase::Builder test; - char testName[100]; - makeTestName(testName, sizeof(testName), fileName, ++testNum); - test.setName(testName); - - // variations has "params" and "exp" fields, and an optional "locale" - for (auto variationsIter = variations.begin(); variationsIter != variations.end(); ++variationsIter) { - auto variation = variationsIter->get(); - auto params = variation["params"]; - auto exp = variation["exp"]; - - test.setExpected(u_str(exp)); - test.setPattern(u_str(messageText)); - test.setExpectSuccess(); - setArguments(test, params, errorCode); - - if (!variation["locale"].is_null()) { - std::string localeStr = variation["locale"].template get(); - test.setLocale(Locale(localeStr.c_str())); - } - - TestCase testCase = test.build(); - TestUtils::runTestCase(t, testCase, errorCode); - } - } + TestCase t = test.build(); + TestUtils::runTestCase(icuTest, t, errorCode); } // File name is relative to message2/ in the test data directory -static void runValidTestsFromJsonFile(TestMessageFormat2& t, +static void runTestsFromJsonFile(TestMessageFormat2& t, const std::string& fileName, IcuTestErrorCode& errorCode) { const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); @@ -334,156 +250,41 @@ static void runValidTestsFromJsonFile(TestMessageFormat2& t, int32_t testNum = 0; char testName[100]; - for (auto iter = data.begin(); iter != data.end(); ++iter) { - makeTestName(testName, sizeof(testName), fileName, ++testNum); - t.logln(testName); - - t.logln(u_str(iter->dump())); - - runValidTest(t, testName, *iter, errorCode); - } -} - -// File name is relative to message2/ in the test data directory -static void runDataModelErrorTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string dataModelErrorsFileName(testDataDirectory); - dataModelErrorsFileName.append("message2/"); - dataModelErrorsFileName.append(fileName); - std::ifstream dataModelErrorsFile(dataModelErrorsFileName); - json data = json::parse(dataModelErrorsFile); - - // Do tests for data model errors - // This file is an object where the keys are error names - // and the values are arrays of strings. - // The whole file can be represented - // as a map from strings to a vector of strings. - using dataModelErrorType = std::map>; - auto dataModelErrorTests = data.template get(); - for (auto iter = dataModelErrorTests.begin(); iter != dataModelErrorTests.end(); ++iter) { - auto errorName = iter->first; - auto messages = iter->second; - - UErrorCode expectedError = getExpectedErrorFromString(errorName); - int32_t testNum = 0; - char testName[100]; - TestCase::Builder testBuilder; - for (auto messagesIter = messages.begin(); messagesIter != messages.end(); ++messagesIter) { - makeTestName(testName, sizeof(testName), errorName, testNum); - testBuilder.setName(testName); - t.logln(u_str(fileName + ": " + testName)); - testNum++; - UnicodeString messageText = u_str(*messagesIter); - t.logln(messageText); - - TestCase test = testBuilder.setPattern(messageText) - .setExpectedError(expectedError) - .build(); - TestUtils::runTestCase(t, test, errorCode); + auto j_object = data.template get(); + + // Some files have an expected error + std::string defaultError; + if (!j_object["defaultTestProperties"].is_null() + && !j_object["defaultTestProperties"]["expErrors"].is_null()) { + auto expErrors = j_object["defaultTestProperties"]["expErrors"]; + // expErrors might also be a boolean, in which case we ignore it -- + // so we have to check if it's an array + if (expErrors.is_array()) { + auto expErrorsObj = expErrors.template get>(); + if (expErrorsObj.size() > 0) { + if (!expErrorsObj[0]["type"].is_null()) { + defaultError = expErrorsObj[0]["type"].template get(); + } + } } } -} - -// File name is relative to message2/ in the test data directory -static void runSyntaxErrorTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string syntaxErrorsFileName(testDataDirectory); - syntaxErrorsFileName.append("message2/"); - syntaxErrorsFileName.append(fileName); - std::ifstream syntaxErrorsFile(syntaxErrorsFileName); - json data = json::parse(syntaxErrorsFile); - - // Do tests for syntax errors - // This file is just an array of strings - int32_t testNum = 0; - char testName[100]; - TestCase::Builder testBuilder; - for (auto iter = data.begin(); iter != data.end(); ++iter) { - makeTestName(testName, sizeof(testName), fileName, ++testNum); - testBuilder.setName(testName); - t.logln(testName); - - json json_string = *iter; - UnicodeString cpp_string = u_str(json_string.template get()); - - t.logln(cpp_string); - TestCase test = testBuilder.setPattern(cpp_string) - .setExpectedError(U_MF_SYNTAX_ERROR) - .build(); - TestUtils::runTestCase(t, test, errorCode); - } -} - -// File name is relative to message2/ in the test data directory -static void runSyntaxTestsWithDiagnosticsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string testFileName(testDataDirectory); - testFileName.append("message2/"); - testFileName.append(fileName); - std::ifstream testFile(testFileName); - json data = json::parse(testFile); - - int32_t testNum = 0; - char testName[100]; - - for (auto iter = data.begin(); iter != data.end(); ++iter) { - makeTestName(testName, sizeof(testName), fileName, ++testNum); - t.logln(testName); - t.logln(u_str(iter->dump())); - - runSyntaxErrorTest(t, testName, *iter, errorCode); - } -} - -// File name is relative to message2/ in the test data directory -static void runFunctionTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - // Get the test data directory - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string functionTestsFileName(testDataDirectory); - functionTestsFileName.append("message2/"); - functionTestsFileName.append(fileName); - std::ifstream functionTestsFile(functionTestsFileName); - json data = json::parse(functionTestsFile); - - // Map from string to json, where the strings are the function names - auto tests = data.template get(); - for (auto iter = tests.begin(); iter != tests.end(); ++iter) { - int32_t testNum = 0; - auto functionName = iter->first; - t.logln(u_str("Function tests: " + fileName)); - t.logln(u_str(iter->second.dump())); - - // Array of tests - auto testsForThisFunction = iter->second.template get>(); - for (auto testsIter = testsForThisFunction.begin(); - testsIter != testsForThisFunction.end(); - ++testsIter) { - char testName[100]; - makeTestName(testName, sizeof(testName), functionName, ++testNum); + if (!j_object["tests"].is_null()) { + auto tests = j_object["tests"].template get>(); + for (auto iter = tests.begin(); iter != tests.end(); ++iter) { + makeTestName(testName, sizeof(testName), fileName, ++testNum); t.logln(testName); - runValidTest(t, testName, *testsIter, errorCode); - } + t.logln(u_str(iter->dump())); + runValidTest(t, testName, defaultError, *iter, errorCode); + } + } else { + // Test doesn't follow schema -- probably an error + t.logln("Warning: no tests in filename: "); + t.logln(u_str(fileName)); + (UErrorCode&) errorCode = U_ILLEGAL_ARGUMENT_ERROR; } - } void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { @@ -493,27 +294,34 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { // Tests directly under testdata/message2 are specific to ICU4C. // Do spec tests for syntax errors - runSyntaxErrorTestsFromJsonFile(*this, "spec/syntax-errors.json", errorCode); - runSyntaxErrorTestsFromJsonFile(*this, "more-syntax-errors.json", errorCode); + runTestsFromJsonFile(*this, "spec/syntax-errors.json", errorCode); // Do tests for data model errors - runDataModelErrorTestsFromJsonFile(*this, "spec/data-model-errors.json", errorCode); - runDataModelErrorTestsFromJsonFile(*this, "more-data-model-errors.json", errorCode); + runTestsFromJsonFile(*this, "spec/data-model-errors.json", errorCode); + runTestsFromJsonFile(*this, "more-data-model-errors.json", errorCode); + + // Do tests for reserved statements/expressions + runTestsFromJsonFile(*this, "spec/unsupported-expressions.json", errorCode); + runTestsFromJsonFile(*this, "spec/unsupported-statements.json", errorCode); // Do valid spec tests - runValidTestsFromJsonFile(*this, "spec/test-core.json", errorCode); + runTestsFromJsonFile(*this, "spec/syntax.json", errorCode); // Do valid function tests - runFunctionTestsFromJsonFile(*this, "spec/test-functions.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/date.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/datetime.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/integer.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/number.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/string.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/time.json", errorCode); // Other tests (non-spec) - runFunctionTestsFromJsonFile(*this, "more-functions.json", errorCode); - runValidTestsFromJsonFile(*this, "valid-tests.json", errorCode); - runValidTestsFromJsonFile(*this, "resolution-errors.json", errorCode); - runValidTestsFromJsonFile(*this, "reserved-syntax.json", errorCode); - runValidTestsFromJsonFile(*this, "matches-whitespace.json", errorCode); - runValidTestsFromJsonFile(*this, "alias-selector-annotations.json", errorCode); - runValidTestsFromJsonFile(*this, "runtime-errors.json", errorCode); + runTestsFromJsonFile(*this, "more-functions.json", errorCode); + runTestsFromJsonFile(*this, "valid-tests.json", errorCode); + runTestsFromJsonFile(*this, "resolution-errors.json", errorCode); + runTestsFromJsonFile(*this, "matches-whitespace.json", errorCode); + runTestsFromJsonFile(*this, "alias-selector-annotations.json", errorCode); + runTestsFromJsonFile(*this, "runtime-errors.json", errorCode); // Re: the expected output for the first test in this file: // Note: the more "correct" fallback output seems like it should be "1.000 3" (ignoring the @@ -523,10 +331,10 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { // Probably this is going to change anyway so that any data model error gets replaced // with a fallback for the whole message. // The second test has a similar issue with the output. - runValidTestsFromJsonFile(*this, "tricky-declarations.json", errorCode); + runTestsFromJsonFile(*this, "tricky-declarations.json", errorCode); // Markup is ignored when formatting to string - runValidTestsFromJsonFile(*this, "markup.json", errorCode); + runTestsFromJsonFile(*this, "markup.json", errorCode); // TODO(duplicates): currently the expected output is based on using // the last definition of the duplicate-declared variable; @@ -534,24 +342,24 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { // however if https://github.com/unicode-org/message-format-wg/pull/704 lands, // it'll be a moot point since the output will be expected to be the fallback string // (This applies to the expected output for all the U_DUPLICATE_DECLARATION_ERROR tests) - runValidTestsFromJsonFile(*this, "duplicate-declarations.json", errorCode); + runTestsFromJsonFile(*this, "duplicate-declarations.json", errorCode); // TODO(options): // Bad options. The spec is unclear about this // -- see https://github.com/unicode-org/message-format-wg/issues/738 // The current behavior is to set a U_MF_FORMATTING_ERROR for any invalid options. - runValidTestsFromJsonFile(*this, "invalid-options.json", errorCode); + runTestsFromJsonFile(*this, "invalid-options.json", errorCode); - runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-end-of-input.json", errorCode); - runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-diagnostics.json", errorCode); - runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "invalid-number-literals-diagnostics.json", errorCode); - runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-diagnostics-multiline.json", errorCode); + runTestsFromJsonFile(*this, "syntax-errors-end-of-input.json", errorCode); + runTestsFromJsonFile(*this, "syntax-errors-diagnostics.json", errorCode); + runTestsFromJsonFile(*this, "invalid-number-literals-diagnostics.json", errorCode); + runTestsFromJsonFile(*this, "syntax-errors-diagnostics-multiline.json", errorCode); // ICU4J tests - runFunctionTestsFromJsonFile(*this, "icu-test-functions.json", errorCode); - runICU4JSyntaxTestsFromJsonFile(*this, "icu-parser-tests.json", errorCode); - runICU4JSelectionTestsFromJsonFile(*this, "icu-test-selectors.json", errorCode); - runValidTestsFromJsonFile(*this, "icu-test-previous-release.json", errorCode); + runTestsFromJsonFile(*this, "icu-test-functions.json", errorCode); + runTestsFromJsonFile(*this, "icu-parser-tests.json", errorCode); + runTestsFromJsonFile(*this, "icu-test-selectors.json", errorCode); + runTestsFromJsonFile(*this, "icu-test-previous-release.json", errorCode); } #endif /* #if !UCONFIG_NO_MF2 */ diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java index e33a7c9804a8..2ba70854d093 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java @@ -436,10 +436,8 @@ private static Object resolveLiteralOrVariable( Map arguments) { if (value instanceof Literal) { String val = ((Literal) value).value; - Number nr = OptUtils.asNumber(val); - if (nr != null) { - return nr; - } + // "The resolution of a text or literal MUST resolve to a string." + // https://github.com/unicode-org/message-format-wg/blob/main/spec/formatting.md#literal-resolution return val; } else if (value instanceof VariableRef) { String varName = ((VariableRef) value).name; diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java index 073aafd8ef32..a5c826f87397 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java @@ -46,22 +46,32 @@ public static MFDataModel.Message parse(String input) throws MFParseException { // Parser proper private MFDataModel.Message parseImpl() throws MFParseException { MFDataModel.Message result; + // Determine if message is simple or complex; this requires + // looking through whitespace. + int savedPosition = input.getPosition(); + skipOptionalWhitespaces(); int cp = input.peekChar(); if (cp == '.') { // declarations or .match + // No need to restore whitespace result = getComplexMessage(); } else if (cp == '{') { // `{` or `{{` cp = input.readCodePoint(); cp = input.peekChar(); if (cp == '{') { // `{{`, complex body without declarations input.backup(1); // let complexBody deal with the wrapping {{ and }} + // abnf: complex-message = [s] *(declaration [s]) complex-body [s] MFDataModel.Pattern pattern = getQuotedPattern(); + skipOptionalWhitespaces(); result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern); } else { // placeholder - input.backup(1); // We want the '{' present, to detect the part as placeholder. + // Restore whitespace if applicable + input.gotoPosition(savedPosition); MFDataModel.Pattern pattern = getPattern(); result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern); } } else { + // Restore whitespace if applicable + input.gotoPosition(savedPosition); MFDataModel.Pattern pattern = getPattern(); result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern); } @@ -586,9 +596,10 @@ private MFDataModel.Message getComplexMessage() throws MFParseException { } else { // Expect {{...}} or end of message skipOptionalWhitespaces(); int cp = input.peekChar(); - // complex-message = *(declaration [s]) complex-body + // abnf: complex-message = [s] *(declaration [s]) complex-body [s] checkCondition(cp != EOF, "Expected a quoted pattern or .match; got end-of-input"); MFDataModel.Pattern pattern = getQuotedPattern(); + skipOptionalWhitespaces(); // Trailing whitespace is allowed checkCondition(input.atEnd(), "Content detected after the end of the message."); return new MFDataModel.PatternMessage(declarations, pattern); } @@ -648,9 +659,7 @@ private MFDataModel.Variant getVariant() throws MFParseException { } keys.add(key); } - // Only want to skip whitespace if we parsed at least one key -- - // otherwise, we might fail to catch trailing whitespace at the end of - // the message, which is a parse error + // Only want to skip whitespace if we parsed at least one key if (!keys.isEmpty()) { skipOptionalWhitespaces(); } @@ -712,7 +721,7 @@ private MFDataModel.Declaration getDeclaration() throws MFParseException { MFDataModel.Expression expression; switch (declName) { case "input": - skipMandatoryWhitespaces(); + skipOptionalWhitespaces(); expression = getPlaceholder(); String inputVarName = null; checkCondition(expression instanceof MFDataModel.VariableExpression, diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java index 3305f2435a50..0dd3bf6be59c 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java @@ -4,11 +4,14 @@ package com.ibm.icu.dev.test.message2; import java.io.Reader; +import java.lang.reflect.Type; +import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import com.google.gson.reflect.TypeToken; import com.ibm.icu.dev.test.CoreTestFmwk; @SuppressWarnings({"static-method", "javadoc"}) @@ -16,14 +19,30 @@ public class CoreTest extends CoreTestFmwk { private static final String[] JSON_FILES = {"alias-selector-annotations.json", "duplicate-declarations.json", + "icu-parser-tests.json", + "icu-test-functions.json", + "icu-test-previous-release.json", + "icu-test-selectors.json", + "invalid-number-literals-diagnostics.json", "invalid-options.json", "markup.json", "matches-whitespace.json", - "reserved-syntax.json", + "more-data-model-errors.json", + "more-functions.json", "resolution-errors.json", "runtime-errors.json", - "spec/test-core.json", + "spec/data-model-errors.json", + "spec/syntax-errors.json", + "spec/syntax.json", + "spec/functions/date.json", + "spec/functions/datetime.json", + "spec/functions/integer.json", + "spec/functions/number.json", + "spec/functions/string.json", + "spec/functions/time.json", "syntax-errors-diagnostics.json", + "syntax-errors-diagnostics-multiline.json", + "syntax-errors-end-of-input.json", "tricky-declarations.json", "valid-tests.json"}; @@ -31,9 +50,9 @@ public class CoreTest extends CoreTestFmwk { public void test() throws Exception { for (String jsonFile : JSON_FILES) { try (Reader reader = TestUtils.jsonReader(jsonFile)) { - Unit[] unitList = TestUtils.GSON.fromJson(reader, Unit[].class); - for (Unit unit : unitList) { - TestUtils.runTestCase(unit); + MF2Test tests = TestUtils.GSON.fromJson(reader, MF2Test.class); + for (Unit unit : tests.tests) { + TestUtils.runTestCase(tests.defaultTestProperties, unit); } } } diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java deleted file mode 100644 index d8ba5699c260..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java +++ /dev/null @@ -1,44 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.Map; -import java.util.Map.Entry; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.google.gson.reflect.TypeToken; -import com.ibm.icu.dev.test.CoreTestFmwk; -import com.ibm.icu.message2.MessageFormatter; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class DataModelErrorsTest extends CoreTestFmwk { - private static final String[] JSON_FILES = {"spec/data-model-errors.json", - "more-data-model-errors.json"}; - - @Test - public void test() throws Exception { - for (String jsonFile : JSON_FILES) { - try (Reader reader = TestUtils.jsonReader(jsonFile)) { - Type mapType = new TypeToken>(){/* not code */}.getType(); - Map unitList = TestUtils.GSON.fromJson(reader, mapType); - for (Entry tests : unitList.entrySet()) { - for (String pattern : tests.getValue()) { - try { - MessageFormatter.builder().setPattern(pattern).build().formatToString(null); - fail("Undetected errors in '" + tests.getKey() + "': '" + pattern + "'"); - } catch (Exception e) { - // We expected an error, so it's all good - } - } - } - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java new file mode 100644 index 000000000000..d0b463baf1f3 --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java @@ -0,0 +1,23 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json + +// Class corresponding to the json test files. +// Since this is serialized by Gson, the field names should match the keys in the .json files. +class DefaultTestProperties { + // Unused fields ignored + final String locale; + final Object[] expErrors; + + DefaultTestProperties(String locale, Object[] expErrors) { + this.locale = locale; + this.expErrors = expErrors; + } +} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java deleted file mode 100644 index 994366a559aa..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java +++ /dev/null @@ -1,33 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.ibm.icu.dev.test.CoreTestFmwk; - -/* - * This is the equivalent of the `FromJsonTest` class in the previous release. - * That class was originally a json file, converted to some hard-coded tests in the Java class. - * Now that we can use gson for testing we reverted those tests back to json, tested in this class. - */ -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class FirstReleaseTests extends CoreTestFmwk { - private static final String JSON_FILE = "icu-test-previous-release.json"; - - @Test - public void test() throws Exception { - try (Reader reader = TestUtils.jsonReader(JSON_FILE)) { - Unit[] unitList = TestUtils.GSON.fromJson(reader, Unit[].class); - for (Unit unit : unitList) { - TestUtils.runTestCase(unit); - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java deleted file mode 100644 index c76c60a50327..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java +++ /dev/null @@ -1,38 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.Map; -import java.util.Map.Entry; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.google.gson.reflect.TypeToken; -import com.ibm.icu.dev.test.CoreTestFmwk; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class FunctionsTest extends CoreTestFmwk { - private static final String[] JSON_FILES = {"spec/test-functions.json", - "more-functions.json"}; - - @Test - public void test() throws Exception { - for (String jsonFile : JSON_FILES) { - try (Reader reader = TestUtils.jsonReader(jsonFile)) { - Type mapType = new TypeToken>(){/* not code */}.getType(); - Map unitList = TestUtils.GSON.fromJson(reader, mapType); - for (Entry testGroup : unitList.entrySet()) { - for (Unit unit : testGroup.getValue()) { - TestUtils.runTestCase(unit); - } - } - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java deleted file mode 100644 index f286d5c2c743..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java +++ /dev/null @@ -1,38 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.Map; -import java.util.Map.Entry; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.google.gson.reflect.TypeToken; -import com.ibm.icu.dev.test.CoreTestFmwk; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class IcuFunctionsTest extends CoreTestFmwk { - private static final String JSON_FILE = "icu-test-functions.json"; - - @Test - public void test() throws Exception { - try (Reader reader = TestUtils.jsonReader(JSON_FILE)) { - Type mapType = - new TypeToken>() { - /* not code */ - }.getType(); - Map unitList = TestUtils.GSON.fromJson(reader, mapType); - for (Entry testGroup : unitList.entrySet()) { - for (Unit unit : testGroup.getValue()) { - TestUtils.runTestCase(unit); - } - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java new file mode 100644 index 000000000000..502fde2a6093 --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java @@ -0,0 +1,24 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json + +// Class corresponding to the json test files. +// Since this is serialized by Gson, the field names should match the keys in the .json files. +class MF2Test { + // Unused fields ignored + final DefaultTestProperties defaultTestProperties; + final Unit[] tests; + + MF2Test(DefaultTestProperties defaultTestProperties, + Unit[] tests) { + this.defaultTestProperties = defaultTestProperties; + this.tests = tests; + } +} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java new file mode 100644 index 000000000000..9c77fe73443b --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java @@ -0,0 +1,23 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json + +// Class corresponding to the json test files. +// Since this is serialized by Gson, the field names should match the keys in the .json files. +class Param { + // Unused fields ignored + final String name; + final Object value; + + Param(String name, Object value) { + this.name = name; + this.value = value; + } +} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java index 7b2a3b988c9e..638b38521b27 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java @@ -29,16 +29,5 @@ public void testNullInput() throws Exception { MFParser.parse(null); } - @Test - public void test() throws Exception { - try (Reader reader = TestUtils.jsonReader(JSON_FILE)) { - Type mapType = new TypeToken>(){/* not code */}.getType(); - Map unitList = TestUtils.GSON.fromJson(reader, mapType); - for (Entry testGroup : unitList.entrySet()) { - for (String unit : testGroup.getValue()) { - MFParser.parse(unit); - } - } - } - } + // Other tests in CoreTest.java } diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java deleted file mode 100644 index 8762f4757259..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java +++ /dev/null @@ -1,39 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.ibm.icu.dev.test.CoreTestFmwk; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class SelectorsWithVariousArgumentsTest extends CoreTestFmwk { - private static final String JSON_FILE = "icu-test-selectors.json"; - - @Test - public void test() throws Exception { - try (Reader reader = TestUtils.jsonReader(JSON_FILE)) { - TestWithVariations[] unitList = - TestUtils.GSON.fromJson(reader, TestWithVariations[].class); - for (TestWithVariations testWithVar : unitList) { - Unit sharedUnit = testWithVar.shared; - for (Unit variation : testWithVar.variations) { - Unit mergedUnit = sharedUnit.merge(variation); - TestUtils.runTestCase(mergedUnit); - } - } - } - } - - class TestWithVariations { - String comment; - Unit shared; - Unit[] variations; - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java new file mode 100644 index 000000000000..39a931a5a589 --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java @@ -0,0 +1,26 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +// Class corresponding to the json test files. +// See Unit.java and StringToListAdapter.java for how this is used. +// Workaround for not being able to get the class of a generic type. + +class Sources { + final List sources; + + Sources(List sources) { + this.sources = sources; + } + + @Override + public String toString() { + return ("[" + sources.toString() + "]"); + } +} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java new file mode 100644 index 000000000000..facc9f00c2df --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java @@ -0,0 +1,52 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.JsonToken; + +// Helper class that converts a single String to a List +// so that the `src` property can be either a single string or an array of strings. +// Used in the TestUtils class. + +// Uses ArrayList instead of List so that when registering, it's possible +// to get ArrayList.class +public class StringToListAdapter extends TypeAdapter { + public Sources read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + if (reader.peek() == JsonToken.BEGIN_ARRAY) { + ArrayList result = new ArrayList(); + reader.beginArray(); + while (reader.hasNext()) { + result.add(reader.nextString()); + } + reader.endArray(); + return new Sources(result); + } + if (reader.peek() == JsonToken.STRING) { + String str = reader.nextString(); + ArrayList result = new ArrayList(); + result.add(str); + return new Sources(result); + } + throw new IOException(); + } + public void write(JsonWriter writer, Sources value) throws IOException { + writer.beginArray(); + for (String s : value.sources) { + writer.value(s); + } + writer.endArray(); + } +} + diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java deleted file mode 100644 index 204f79555161..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java +++ /dev/null @@ -1,37 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.ibm.icu.dev.test.CoreTestFmwk; -import com.ibm.icu.message2.MessageFormatter; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class SyntaxErrorsTest extends CoreTestFmwk { - private static final String[] JSON_FILES = {"more-syntax-errors.json", - "spec/syntax-errors.json"}; - - @Test - public void test() throws Exception { - for (String jsonFile : JSON_FILES) { - try (Reader reader = TestUtils.jsonReader(jsonFile)) { - String[] srcList = TestUtils.GSON.fromJson(reader, String[].class); - for (String source : srcList) { - try { - MessageFormatter.builder().setPattern(source).build(); - fail("Pattern expected to fail, but didn't: '" + source + "'"); - } catch (Exception e) { - // If we get here it is fine - } - } - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java index 572da833e67a..4a276216462c 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java @@ -17,6 +17,7 @@ import java.util.Date; import java.util.Locale; import java.util.Map; +import java.util.TreeMap; import org.junit.Ignore; @@ -28,7 +29,11 @@ /** Utility class, has no test methods. */ @Ignore("Utility class, has no test methods.") public class TestUtils { - static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create(); + + static final Gson GSON = new GsonBuilder() + .setDateFormat("yyyy-MM-dd HH:mm:ss") + .registerTypeAdapter(Sources.class, new StringToListAdapter()) + .create(); // ======= Legacy TestCase utilities, no json-compatible ======== @@ -72,62 +77,73 @@ private static String reportCase(TestCase testCase) { // ======= Same functionality with Unit, usable with JSON ======== - static void rewriteDates(Map params) { + static void rewriteDates(Param[] params) { // For each value in `params` that's a map with the single key // `date` and a double value d, // return a map with that value changed to Date(d) // In JSON this looks like: - // "params": {"exp": { "date": 1722746637000 } } - for (Map.Entry pair : params.entrySet()) { - if (pair.getValue() instanceof Map) { - Map innerMap = (Map) pair.getValue(); + // "params": [{"name": "exp"}, { "value": { "date": 1722746637000 } }] + for (int i = 0; i < params.length; i++) { + Param pair = params[i]; + if (pair.value instanceof Map) { + Map innerMap = (Map) pair.value; if (innerMap.size() == 1 && innerMap.containsKey("date") && innerMap.get("date") instanceof Double) { Long dateValue = Double.valueOf((Double) innerMap.get("date")).longValue(); - params.put(pair.getKey(), new Date(dateValue)); + params[i] = new Param(pair.name, new Date(dateValue)); } } } } - static void rewriteDecimals(Map params) { + static void rewriteDecimals(Param[] params) { // For each value in `params` that's a map with the single key // `decimal` and a string value s // return a map with that value changed to Decimal(s) // In JSON this looks like: - // "params": {"val": {"decimal": "1234567890123456789.987654321"}}, - for (Map.Entry pair : params.entrySet()) { - if (pair.getValue() instanceof Map) { - Map innerMap = (Map) pair.getValue(); + // "params": [{"name": "val"}, {"value": {"decimal": "1234567890123456789.987654321"}}] + for (int i = 0; i < params.length; i++) { + Param pair = params[i]; + if (pair.value instanceof Map) { + Map innerMap = (Map) pair.value; if (innerMap.size() == 1 && innerMap.containsKey("decimal") && innerMap.get("decimal") instanceof String) { String decimalValue = (String) innerMap.get("decimal"); - params.put(pair.getKey(), new com.ibm.icu.math.BigDecimal(decimalValue)); + params[i] = new Param(pair.name, new com.ibm.icu.math.BigDecimal(decimalValue)); } } } } + static Map paramsToMap(Param[] params) { + if (params == null) { + return null; + } + TreeMap result = new TreeMap(); + for (Param pair : params) { + result.put(pair.name, pair.value); + } + return result; + } - static boolean expectsErrors(Unit unit) { - return unit.errors != null && !unit.errors.isEmpty(); + static boolean expectsErrors(DefaultTestProperties defaults, Unit unit) { + return (unit.expErrors != null && !unit.expErrors.isEmpty()) + || (defaults.expErrors != null && defaults.expErrors.length > 0); } - static void runTestCase(Unit unit) { - runTestCase(unit, null); + static void runTestCase(DefaultTestProperties defaults, Unit unit) { + runTestCase(defaults, unit, null); } - static void runTestCase(Unit unit, Map params) { + static void runTestCase(DefaultTestProperties defaults, Unit unit, Param[] params) { if (unit.ignoreJava != null) { return; } StringBuilder pattern = new StringBuilder(); - if (unit.srcs != null) { - for (String src : unit.srcs) { + if (unit.src != null) { + for (String src : unit.src.sources) { pattern.append(src); } - } else if (unit.src != null) { - pattern.append(unit.src); } // We can call the "complete" constructor with null values, but we want to test that @@ -136,6 +152,8 @@ static void runTestCase(Unit unit, Map params) { MessageFormatter.builder().setPattern(pattern.toString()); if (unit.locale != null && !unit.locale.isEmpty()) { mfBuilder.setLocale(Locale.forLanguageTag(unit.locale)); + } else if (defaults.locale != null) { + mfBuilder.setLocale(Locale.forLanguageTag(defaults.locale)); } else { mfBuilder.setLocale(Locale.US); } @@ -147,16 +165,18 @@ static void runTestCase(Unit unit, Map params) { rewriteDates(params); rewriteDecimals(params); } - String result = mf.formatToString(params); - if (expectsErrors(unit)) { + String result = mf.formatToString(paramsToMap(params)); + if (expectsErrors(defaults, unit)) { fail(reportCase(unit) + "\nExpected error, but it didn't happen.\n" + "Result: '" + result + "'"); } else { - assertEquals(reportCase(unit), unit.exp, result); + if (unit.exp != null) { + assertEquals(reportCase(unit), unit.exp, result); + } } } catch (IllegalArgumentException | NullPointerException e) { - if (!expectsErrors(unit)) { + if (!expectsErrors(defaults, unit)) { fail(reportCase(unit) + "\nNo error was expected here, but it happened:\n" + e.getMessage()); diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java index 3ff18ecd7cc2..810594f29253 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java @@ -10,29 +10,27 @@ // Class corresponding to the json test files. // Since this is serialized by Gson, the field names should match the keys in the .json files. class Unit { - final String src; - final List srcs; + // For why this is not an ArrayList, see StringToListAdapter.java + final Sources src; final String locale; - final Map params; + final Param[] params; final String exp; final String ignoreJava; - final List errors; + final List expErrors; Unit( - String src, - List srcs, + Sources src, String locale, - Map params, + Param[] params, String exp, String ignoreJava, - List errors) { + List expErrors) { this.src = src; - this.srcs = srcs; this.locale = locale; this.params = params; this.exp = exp; this.ignoreJava = ignoreJava; - this.errors = errors; + this.expErrors = expErrors; } class Error { @@ -56,7 +54,9 @@ public String toString() { @Override public String toString() { StringJoiner result = new StringJoiner(", ", "UnitTest {", "}"); - result.add("src=" + escapeString(src)); + if (src != null) { + result.add("src=" + src.sources.toString()); + } if (params != null) { result.add("params=" + params); } @@ -75,14 +75,13 @@ public String toString() { * @return a new unit created by merging `this` unit and `other` */ public Unit merge(Unit other) { - String newSrc = other.src != null ? other.src : this.src; - List newSrcs = other.srcs != null ? other.srcs : this.srcs; + Sources newSrc = other.src != null ? other.src : this.src; String newLocale = other.locale != null ? other.locale : this.locale; - Map newParams = other.params != null ? other.params : this.params; + Param[] newParams = other.params != null ? other.params : this.params; String newExp = other.exp != null ? other.exp : this.exp; String newIgnore = other.ignoreJava != null ? other.ignoreJava : this.ignoreJava; - List newErrors = other.errors != null ? other.errors : this.errors; - return new Unit(newSrc, newSrcs, newLocale, newParams, newExp, newIgnore, newErrors); + List newExpErrors = other.expErrors != null ? other.expErrors : this.expErrors; + return new Unit(newSrc, newLocale, newParams, newExp, newIgnore, newExpErrors); } private static String escapeString(String str) { diff --git a/testdata/message2/README.txt b/testdata/message2/README.txt index 17c7c656c320..b803ebc0425e 100644 --- a/testdata/message2/README.txt +++ b/testdata/message2/README.txt @@ -1,19 +1,14 @@ © 2024 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html -The format of the JSON files in this directory follows the same format as `test-core.json` -in the spec, described in: +The format of the JSON files in this directory and subdirectories +follow the test schema defined in the Conformance repository: -https://github.com/unicode-org/message-format-wg/blob/main/test/README.md +https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json -The `parts` field is not used. +(as of https://github.com/unicode-org/conformance/pull/255 or later). -# JSON extensions - -An additional `comment` field may be present, which is only for human readers. - -A "srcs" field, whose value is an array of strings, may be present instead -of "src". The strings are concatenated to get the message. +# JSON notes In the "params" field, a date parameter can be expressed as: { "date": n } @@ -46,43 +41,3 @@ If present, "char" reflects the expected character offset and "line" reflects the expected line number in the parse error. The files with "diagnostics" in the name have these fields filled in. -# ICU4C vs. ICU4J tests - -The following tests are run in both ICU4C and ICU4J: - -* alias-selector-annotations.json -* duplicate-declarations.json -* icu-parser-tests.json - - Two tests removed while single-sourcing tests, because a `{{}}` message body - had to be added to get it to parse in ICU4C, and this broke the test in ICU4J. - These tests are in icu-parser-tests-old.json -* icu-test-functions.json - - Some tests marked as ignored -* icu-test-previous-release.json - - Some tests marked as ignored -* icu-test-selectors.json -* markup.json -* matches-whitespace.json - - Some tests marked as ignored -* more-data-model-errors.json -* more-syntax-errors.json -* reserved-syntax.json - - All tests marked as ignored in Java (resolution errors are suppressed) -* resolution-errors.json - - All tests marked as ignored in Java (resolution errors are suppressed) -* runtime-errors.json - - All tests marked as ignored in Java (message function errors are suppressed) -* syntax-errors-diagnostics.json -* tricky-declarations.json -* valid-tests.json - - Some tests marked as ignored -* spec/* - - Some tests in test-core.json and test-functions.json marked as ignored - -The following tests are only run in ICU4C, either because ICU4J doesn't check -for invalid options, or because ICU4J doesn't report line/column numbers for -parse errors: -* invalid-number-literals-diagnostics.json -* invalid-options.json -* syntax-errors-diagnostics-multiline.json -* syntax-errors-end-of-input.json diff --git a/testdata/message2/alias-selector-annotations.json b/testdata/message2/alias-selector-annotations.json index 71ad134d821d..c064d40d4c82 100644 --- a/testdata/message2/alias-selector-annotations.json +++ b/testdata/message2/alias-selector-annotations.json @@ -1,4 +1,10 @@ -[ +{ + "scenario": "Selector annotations", + "description": "Tests for indirectly annotated selectors", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": ".local $one = {|The one| :string}\n .match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", "exp": "Value is not one" @@ -7,4 +13,6 @@ "src": ".local $one = {|The one| :string}\n .local $two = {$one}\n .match {$two}\n 1 {{Value is one}}\n * {{Value is not one}}", "exp": "Value is not one" } -] + ] +} + diff --git a/testdata/message2/duplicate-declarations.json b/testdata/message2/duplicate-declarations.json index 6ea54daa003e..cd3acc1576d3 100644 --- a/testdata/message2/duplicate-declarations.json +++ b/testdata/message2/duplicate-declarations.json @@ -1,25 +1,43 @@ -[ +{ + "scenario": "Duplicate declaration errors", + "description": "Tests that should trigger a duplicate declaration error", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + "tests": [ { "src": ".local $foo = {$foo} .local $foo = {42} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar 42", - "errors": [{ "type": "duplicate-declaration" }] + "params": [{ "name": "foo", "value": "foo" }], + "exp": "bar 42" }, { "src": ".local $foo = {42} .local $foo = {42} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar 42", - "errors": [{ "type": "duplicate-declaration" }] + "params": [{ "name": "foo", "value": "foo" }], + "exp": "bar 42" }, { "src": ".local $foo = {:unknown} .local $foo = {42} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar 42", - "errors": [{ "type": "duplicate-declaration" }] + "params": [{ "name": "foo", "value": "foo" }], + "exp": "bar 42" }, { "src": ".local $x = {42} .local $y = {$x} .local $x = {13} {{{$x} {$y}}}", - "exp": "13 42", - "errors": [{ "type": "duplicate-declaration" }] + "exp": "13 42" + }, + { + "src": ".local $foo = {$foo} {{bar {$foo}}}", + "params": [{ "name": "foo", "value": "foo" }], + "exp": "bar foo" + }, + { + "src": ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}", + "params": [{ "name": "baz", "value": "foo" }], + "exp": "bar {$bar}" } -] + ] +} diff --git a/testdata/message2/icu-parser-tests.json b/testdata/message2/icu-parser-tests.json index 0cdfe0a467aa..a28cfc61687f 100644 --- a/testdata/message2/icu-parser-tests.json +++ b/testdata/message2/icu-parser-tests.json @@ -1,61 +1,61 @@ { - "Simple messages": [ - "", - "Hello", - "Hello world!", - "Hello \t \n \r \\{ world!", - "Hello world {:datetime}", - "Hello world {foo}", - "Hello {0} world", - "Hello {123} world", - "Hello {-123} world", - "Hello {3.1416} world", - "Hello {-3.1416} world", - "Hello {123E+2} world", - "Hello {123E-2} world", - "Hello {123.456E+2} world", - "Hello {123.456E-2} world", - "Hello {-123.456E+2} world", - "Hello {-123.456E-2} world", - "Hello {-123E+2} world", - "Hello {-123E-2} world", - "Hello world {$exp}", - "Hello world {$exp :datetime}", - "Hello world {|2024-02-27| :datetime}", - "Hello world {$exp :datetime style=long} and more", - "Hello world {$exp :function number=1234} and more", - "Hello world {$exp :function unquoted=left } and more", - "Hello world {$exp :function quoted=|Something| } and more", - "Hello world {$exp :function quoted=|Something with spaces| } and more", - "Hello world {$exp :function quoted=|Something with \\| spaces and \\| escapes| } and more", - "Hello world {$exp :function number=1234 unquoted=left quoted=|Something|}", - "Hello world {$exp :function number=1234 unquoted=left quoted=|Something longer|}", - "Hello world {$exp :function number=1234 unquoted=left quoted=|Something \\| longer|}" - ], - "Attributes": [ - "Hello world {$exp}", - "Hello world {$exp @attr}", - "Hello world {$exp @valid @attr=a @attrb=123 @atrn=|foo bar|}", - "Hello world {$exp :date @valid @attr=aaaa @attrb=123 @atrn=|foo bar|}", - "Hello world {$exp :date year=numeric month=long day=numeric int=12 @valid @attr=a @attrb=123 @atrn=|foo bar|}" - ], - "Reserved and private": [ - "Reserved {$exp &foo |something more protected|} and more", - "Reserved {$exp %foo |something quoted \\| inside|} and more", - "{{.starting with dot is OK here}}", - "{{Some string pattern, with {$foo} and {$exp :date style=long}!}}" - ], - "Simple messages, with declarations": [ - ".input {$pi :number} {{}}", - ".input {$exp :date} {{}}", - ".local $foo = {$exp} {{}}", - ".local $foo = {$exp :date} {{}}", - ".local $foo = {$exp :date year=numeric month=long day=numeric} {{}}", - ".local $bar = {$foo :date month=medium} {{}}", - ".something |reserved=| {$foo :date} {{}}" - ], - "Multiple declarations in one message": [ - ".input {$a :date} .local $exp = {$a :date style=full} {{Your card expires on {$exp}!}}", - ".input {$a :date} .local $b = {$a :date year=numeric month=long day=numeric} .local $c = {$b :date month=medium} .someting |reserved = \\| and more| {$x :date} {$y :date} {$z :number} {{}}" + "scenario": "Valid tests", + "description": "Additional valid tests", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { "src": "" }, + { "src": "Hello" }, + { "src": "Hello world!" }, + { "src": "Hello \t \n \r \\{ world!" }, + { "src": "Hello world {:datetime}" }, + { "src": "Hello world {foo}" }, + { "src": "Hello {0} world" }, + { "src": "Hello {123} world" }, + { "src": "Hello {-123} world" }, + { "src": "Hello {3.1416} world" }, + { "src": "Hello {-3.1416} world" }, + { "src": "Hello {123E+2} world" }, + { "src": "Hello {123E-2} world" }, + { "src": "Hello {123.456E+2} world" }, + { "src": "Hello {123.456E-2} world" }, + { "src": "Hello {-123.456E+2} world" }, + { "src": "Hello {-123.456E-2} world" }, + { "src": "Hello {-123E+2} world" }, + { "src": "Hello {-123E-2} world" }, + { "src": "Hello world {$exp}" }, + { "src": "Hello world {$exp :datetime}" }, + { "src": "Hello world {|2024-02-27| :datetime}" }, + { "src": "Hello world {$exp :datetime style=long} and more" }, + { "src": "Hello world {$exp :function number=1234} and more" }, + { "src": "Hello world {$exp :function unquoted=left } and more" }, + { "src": "Hello world {$exp :function quoted=|Something| } and more" }, + { "src": "Hello world {$exp :function quoted=|Something with spaces| } and more" }, + { "src": "Hello world {$exp :function quoted=|Something with \\| spaces and \\| escapes| } and more" }, + { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something|}" }, + { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something longer|}" }, + { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something \\| longer|}" }, + { "src": "Hello world {$exp}" }, + { "src": "Hello world {$exp @attr}" }, + { "src": "Hello world {$exp @valid @attr=a @attrb=123 @atrn=|foo bar|}" }, + { "src": "Hello world {$exp :date @valid @attr=aaaa @attrb=123 @atrn=|foo bar|}" }, + { "src": "Hello world {$exp :date year=numeric month=long day=numeric int=12 @valid @attr=a @attrb=123 @atrn=|foo bar|}" }, + { "src": "Reserved {$exp &foo |something more protected|} and more" }, + { "src": "Reserved {$exp %foo |something quoted \\| inside|} and more" }, + { "src": "{{.starting with dot is OK here}}" }, + { "src": "{{Some string pattern \\}, with {$foo} and {$exp :date style=long}!}}" }, + { "src": ".input {$pi :number} {{}}" }, + { "src": ".input {$exp :date} {{}}" }, + { "src": ".local $foo = {$exp} {{}}" }, + { "src": ".local $foo = {$exp :date} {{}}" }, + { "src": ".local $foo = {$exp :date year=numeric month=long day=numeric} {{}}" }, + { "src": ".local $bar = {$foo :date month=medium} {{}}" }, + { "src": ".something |reserved=| {$foo :date} {{}}" }, + { "src": ".input {$a :date} .local $exp = {$a :date style=full} {{Your card expires on {$exp}!}}" }, + { "src": ".input {$a :date} .local $b = {$a :date year=numeric month=long day=numeric} .local $c = {$b :date month=medium} .someting |reserved = \\| and more| {$x :date} {$y :date} {$z :number} {{}}" }, + { "src": ".input {$x :number} {{_}}" }, + { "src": ".local $foo = {|1|} {{_}}" }, + { "src": ".unsupported |statement| {$x :number} {{_}}" } ] } diff --git a/testdata/message2/icu-test-functions.json b/testdata/message2/icu-test-functions.json index 2dfd91cb8030..a97446addf0e 100644 --- a/testdata/message2/icu-test-functions.json +++ b/testdata/message2/icu-test-functions.json @@ -1,95 +1,98 @@ { - "Date and time formats": [ + "scenario": "Function tests", + "description": "Tests for ICU-specific formatting behavior.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "Expires on {$exp}", "exp": "Expires on 8/3/24, 9:43 PM", "comment": "Modified from ICU4J copy to add params (likewise with the other date/time tests); 1722746637000 is 2024-08-03 21:43:57 PDT", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime}", "exp": "Expires on 8/3/24, 9:43 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime icu:skeleton=yMMMMdjmsSSEE}", "exp": "Expires on Sat, August 3, 2024 at 9:43:57.00 PM", - "params": {"exp": { "date": 1722746637000 } }, + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }], "ignoreCpp": "ICU-22754 Skeleton option not implemented yet" }, { "src": "Expires on {$exp :datetime dateStyle=full}", "exp": "Expires on Saturday, August 3, 2024", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime dateStyle=long}", "exp": "Expires on August 3, 2024", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime dateStyle=medium}", "exp": "Expires on Aug 3, 2024", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime timeStyle=long}", "exp": "Expires on 9:43:57 PM PDT", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime timeStyle=medium}", "exp": "Expires on 9:43:57 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime timeStyle=short}", "exp": "Expires on 9:43 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime dateStyle=full timeStyle=medium}", "exp": "Expires on Saturday, August 3, 2024 at 9:43:57 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime year=numeric month=long}", "exp": "Expires on August 2024", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}", "exp": "Expires on 3 Saturday 2024, 9:43 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "comment": "Make sure we ignore date / time fields if needed", "src": "Expires on {$exp :date year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}", "exp": "Expires on 3 Saturday 2024", - "params": {"exp": { "date": 1722746637000 } }, + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }], "ignoreCpp": "ICU-22754 ICU4C doesn't accept field options for `:date` or `:time` -- see spec" }, { "comment": "Make sure we ignore date / time fields if needed", "src": "Expires at {$exp :time year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}", "exp": "Expires at 9:43 PM", - "params": {"exp": { "date": 1722746637000 } }, + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }], "ignoreCpp": "ICU-22754 ICU4C doesn't accept field options for `:date` or `:time` -- see spec" }, { "comment": "Make sure we ignore date / time fields if needed", "src": "Expires at {$exp :time style=long dateStyle=full timeStyle=medium}", "exp": "Expires at 9:43:57 PM PDT", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "comment": "Make sure we ignore date / time fields if needed", "src": "Expires on {$exp :date style=long dateStyle=full timeStyle=medium}", "exp": "Expires on August 3, 2024", - "params": {"exp": { "date": 1722746637000 } } - } - ], - "Literals" : [ + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] + }, { "src": "Expires on {|2025-02-27| :datetime dateStyle=full}", "exp": "Expires on Thursday, February 27, 2025" @@ -115,12 +118,10 @@ "src": "Expires at {|2024-07-02T19:23:45+03:30| :datetime timeStyle=full}", "exp": "Expires at 7:23:45 PM GMT+03:30", "ignoreCpp": "ICU-22754 Time zones not working yet (bug)" - } - ], - "Chaining" : [ + }, { "comment": "Horibly long, but I really wanted to test multiple declarations with overrides, and you can't join strings in JSON", - "srcs": [ + "src": [ ".input {$exp :datetime timeStyle=short}\n", ".input {$user :string}\n", ".local $longExp = {$exp :datetime dateStyle=long}\n", @@ -128,30 +129,30 @@ "{{Hello John, you want '{$exp}', '{$longExp}', or '{$zooExp}' or even '{$exp :datetime dateStyle=full}'?}}" ], "exp": "Hello John, you want '9:43 PM', 'August 3, 2024 at 9:43 PM', or '8/3/24, 9:43:57 PM Pacific Daylight Time' or even 'Saturday, August 3, 2024 at 9:43 PM'?", - "params": {"exp": { "date": 1722746637000 }, "user": "John", "tsOver" : "full" }, + "params": [{"name": "exp", "value": { "date": 1722746637000 }}, + {"name": "user", "value": "John"}, + {"name": "tsOver", "value": "full" }], "ignoreCpp": "ICU-22754 ICU4C doesn't implement this kind of function composition yet. See https://github.com/unicode-org/message-format-wg/issues/515" }, { - "srcs": [ + "src": [ ".input {$exp :datetime year=numeric month=numeric day=|2-digit|}\n", ".local $longExp = {$exp :datetime month=long weekday=long}\n", "{{Expires on '{$exp}' ('{$longExp}').}}" ], "exp": "Expires on '8/03/2024' ('Saturday, August 03, 2024').", - "params": {"exp": { "date": 1722746637000 } } - } - ], - "Number formatter" : [ + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] + }, { "src": "Format {$val} number", - "params": { "val": 31 }, + "params": [{ "name": "val", "value": 31 }], "exp": "Format 31 number" }, { "src": "Format {123456789.9876} number", "locale": "en-IN", - "exp": "Format 12,34,56,789.9876 number", - "ignoreCpp": "ICU-22754 No default formatting for numbers, so it's formatted as a literal string. Is this in the spec?" + "exp": "Format 123456789.9876 number", + "comment": "Number literals are not formatted as numbers by default" }, { "src": "Format {|3.1416|} number", @@ -161,8 +162,8 @@ { "src": "Format {|3.1416|} number", "locale": "ar-AR-u-nu-arab", - "exp": "Format ٣٫١٤١٦ number", - "ignoreCpp": "ICU-22754 No default formatting for numbers, so it's formatted as a literal string. Is this in the spec?" + "exp": "Format 3.1416 number", + "comment": "Number literals are not formatted as numbers by default" }, { "src": "Format {3.1415926 :number}", diff --git a/testdata/message2/icu-test-previous-release.json b/testdata/message2/icu-test-previous-release.json index cc863411db95..0a1e27dff6e2 100644 --- a/testdata/message2/icu-test-previous-release.json +++ b/testdata/message2/icu-test-previous-release.json @@ -1,38 +1,10 @@ -[ - { - "src": "hello", - "exp": "hello" - }, - { - "src": "hello {|world|}", - "exp": "hello world" - }, - { - "src": "hello {||}", - "exp": "hello " - }, - { - "src": "hello {$place}", - "params": { "place": "world" }, - "exp": "hello world" - }, - { - "src": "hello {$place}", - "exp": "hello {$place}", - "errors": [{ "type": "unresolved-var" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "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" - }, +{ + "scenario": "Tests from original ICU4J release", + "description": "Tests taken from the September 2022 MF2 ICU4J release", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "hello {|4.2| :number}", "exp": "hello 4.2" @@ -43,254 +15,108 @@ "exp": "hello \u0664\u066B\u0662" }, { - "src": "hello {|foo| :number}", - "exp": "hello {|foo|}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "hello {:number}", - "exp": "hello {:number}", - "comment": "This is different from JS, should be an error.", - "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.0 }, - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=$foo}", - "params": { "foo": "2" }, - "exp": "hello 4.20", - "errorsJs": ["invalid-type"] - }, - { - "src": ".local $foo = {|bar|} {{bar {$foo}}}", - "exp": "bar bar" - }, - { + "comment": "This is not an error! foo is not used before the local declaration, so the local declaration of $foo shadows the input variable.", "src": ".local $foo = {bar} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "expectedJs": "bar foo", - "comment": "It is undefined if we allow arguments to override local variables, or it is an error. And undefined who wins if that happens, the local variable of the argument.", - "exp": "bar bar" - }, - { - "src": ".local $foo = {$bar} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar foo" + "exp": "bar bar", + "params": [{ "name": "foo", "value": "foo" }] }, { "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": 4.2 }, + "params": [{ "name": "bar", "value": 4.2 }], "exp": "bar 4.2" }, - { - "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.20" - }, - { - "ignoreJava": "Maybe. Because `minimumFractionDigits=foo`", - "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.2", - "errors": [{ "type": "bad-option" }] - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar {$bar}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, { "src": ".local $bar = {$baz} .local $foo = {$bar} {{bar {$foo}}}", - "params": { "baz": "foo" }, + "params": [{ "name": "baz", "value": "foo" }], "exp": "bar foo" }, - { - "patternJs": ".match {$foo} 1 {{one}} * {{other}}", - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": "1" }, - "exp": "one" - }, { "src": ".match {$foo :number} 1 {{one}} * {{other}}", - "params": { "foo": "1" }, + "params": [{ "name": "foo", "value": "1" }], "exp": "one", "ignoreJava": "See ICU-22809" }, { "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": "1" }, - "exp": "one" - }, - { - "patternJs": ".match {$foo} 1 {{one}} * {{other}}", - "src": ".match {$foo :number} 1 {{one}} * {{other}}", - "params": { "foo": 1 }, + "params": [{ "name": "foo", "value": "1" }], "exp": "one" }, { "src": ".match {$foo :number} 1 {{one}} * {{other}}", - "params": { "foo": 1 }, + "params": [{ "name": "foo", "value": 1 }], "exp": "one" }, { "ignoreJava": "Can't pass null in a map", + "ignoreCpp": "Same as Java", "src": ".match {$foo} 1 {{one}} * {{other}}", - "params": { "foo": null }, + "params": [{ "name": "foo", "value": null }], "exp": "other" }, { - "srcJs": ".match {$foo} 1 {{one}} * {{other}}", "src": ".match {$foo :number} 1 {{one}} * {{other}}", "exp": "other", - "errors": [{ "type": "missing-var" }] - }, - { - "srcJs": ".match {$foo} one {{one}} * {{other}}", - "src": ".match {$foo :number} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "srcJs": ".match {$foo} 1 {{=1}} one {{one}} * {{other}}", - "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "srcJs": ".match {$foo} one {{one}} 1 {{=1}} * {{other}}", - "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 1 }, - "exp": "one one" - }, - { - "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 2 }, - "exp": "one other" - }, - { - "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 2, "bar": 2 }, - "exp": "other" + "expErrors": [{ "type": "unresolved-variable" }] }, { - "srcJs": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", - "params": { "bar": 1 }, + "params": [{ "name": "bar", "value": 1 }], "exp": "one" }, { - "srcJs": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", - "params": { "bar": 2 }, + "params": [{ "name": "bar", "value": 2 }], "exp": "other" }, { - "srcJs": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", - "params": { "foo": 1, "none": "" }, + "params": [{ "name": "foo", "value": 1 }, {"name": "none", "value": "" }], "exp": "one" }, { - "srcJs": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", "src": ".local $bar = {$none :number} .match {$foo :string} one {{one}} * {{{$bar}}}", - "params": { "foo": 2 }, + "params": [{ "name": "foo", "value": 2 }], "exp": "{$none}", - "errors": [{ "type": "unresolved-var" }], + "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, { "src": "{{#tag}}", "exp": "#tag" }, - { - "src": "{#tag}", - "exp": "" - }, - { - "src": "{#tag}content", - "exp": "content" - }, { "src": "{#tag}content{/tag}", "exp": "content" }, - { - "src": "{#tag}content", - "exp": "content" - }, - { - "comment": "When we format markup to string we generate no output", - "src": "{#tag foo=bar}", - "exp": "" - }, { "src": "{#tag foo=foo bar=$bar}", - "params": { "bar": "b a r" }, + "params": [{ "name": "bar", "value": "b a r" }], "exp": "" }, { "src": "bad {#markup/} test", - "exp": "bad test", - "errorsJs": [{ "type": "extra-content" }] - }, - { - "src": "{#tag foo=bar}", - "exp": "", - "errorsJs": [{ "type": "extra-content" }] - }, - { - "src": "no braces", - "exp": "no braces", - "errorsJs": [{ "type": "parse-error" }, { "type": "junk-element" }] + "exp": "bad test" }, { "src": "no braces {$foo}", - "params": { "foo": 2 }, - "exp": "no braces 2", - "errorsJs": [{ "type": "parse-error" }, { "type": "junk-element" }] + "params": [{ "name": "foo", "value": 2 }], + "exp": "no braces 2" }, { "src": "empty { }", "exp": "empty ", - "errors": [{ "type": "parse-error" }, { "type": "junk-element" }], + "expErrors": [{ "type": "syntax-error" }], "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/703" }, { "src": "bad {:}", "exp": "bad {:}", - "errors": [{ "type": "empty-token" }, { "type": "missing-func" }] - }, - { - "src": "bad {placeholder}", - "exp": "bad placeholder", - "errorsJs": [{ "type": "parse-error" }, { "type": "extra-content" }, { "type": "junk-element" }] + "expErrors": [{ "type": "syntax-error" }, { "type": "unknown-function" }] }, { "src": "{bad {$placeholder option}}", "exp": "bad {$placeholder}", - "errors": [{ "type": "extra-content" }, { "type": "extra-content" }, { "type": "missing-var" }], + "expErrors": [{ "type": "syntax-error"}, { "type": "unresolved-variable" }], "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/703" }, { @@ -300,14 +126,14 @@ { "src": ".match {$foo :string} * * {{foo}}", "exp": "foo", - "errors": [{ "type": "key-mismatch" }, { "type": "missing-var" }], + "expErrors": [{ "type": "variant-key-mismatch" }, { "type": "unresolved-variable" }], "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/735" }, { "src": ".match {$foo :string} {$bar :string} * {{foo}}", "exp": "foo", - "errors": [{ "type": "key-mismatch" }, { "type": "missing-var" }, { "type": "missing-var" }], + "expErrors": [{ "type": "variant-key-mismatch" }, { "type": "unresolved-variable" }], "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/735" } -] - + ] +} diff --git a/testdata/message2/icu-test-selectors.json b/testdata/message2/icu-test-selectors.json index efe67e7c4c9e..102bdfd88f50 100644 --- a/testdata/message2/icu-test-selectors.json +++ b/testdata/message2/icu-test-selectors.json @@ -1,46 +1,171 @@ -[ +{ + "scenario": "Match tests", + "description": "Tests for various match constructs", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "comment": "Testing simple plural", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 0}], + "exp": "0 files" + }, { "comment": "Testing simple plural", - "shared": { - "srcs": [ + "src": [ ".match {$count :number}\n", "one {{{$count} file}}\n", " * {{{$count} files}}" - ] - }, - "variations" : [ - { "params": { "count": 0 }, "exp": "0 files" }, - { "params": { "count": 1 }, "exp": "1 file" }, - { "params": { "count": 3 }, "exp": "3 files" }, - { "params": { "count": 0 }, "locale": "fr", "exp": "0 file" }, - { "params": { "count": 1 }, "locale": "fr", "exp": "1 file" }, - { "params": { "count": 3 }, "locale": "fr", "exp": "3 files" } - ] + ], + "params": [{"name": "count", "value": 1}], + "exp": "1 file" + }, + { + "comment": "Testing simple plural", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 3}], + "exp": "3 files" + }, + { + "comment": "Testing simple plural", + "locale": "fr", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 0}], + "exp": "0 file" + }, + { + "comment": "Testing simple plural", + "locale": "fr", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 1}], + "exp": "1 file" + }, + { + "comment": "Testing simple plural", + "locale": "fr", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 3}], + "exp": "3 files" + }, + { + "comment": "Testing simple plural, but swap variant order", + "src": [ + ".match {$count :number}\n", + " * {{You deleted {$count} files}}\n", + "one {{You deleted {$count} file}}" + ], + "params": [{"name": "count", "value": 1}], + "exp": "You deleted 1 file" }, { "comment": "Testing simple plural, but swap variant order", - "shared": { - "srcs": [ + "src": [ ".match {$count :number}\n", " * {{You deleted {$count} files}}\n", "one {{You deleted {$count} file}}" - ] - }, - "variations" : [ - { - "params": { "count": 1 }, - "exp": "You deleted 1 file" - }, - { - "params": { "count": 3 }, - "exp": "You deleted 3 files" - } - ] + ], + "params": [{"name": "count", "value": 3}], + "exp": "You deleted 3 files" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 1}], + "exp": "You got the gold medal" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 2}], + "exp": "You got the silver medal" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 3}], + "exp": "You got the bronze medal" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 7}], + "exp": "You finished in the 7th place" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 21}], + "exp": "You finished in the 21st place" }, { "comment": "Ordinal, with mixed order and exact matches", - "shared": { - "srcs": [ + "src": [ ".match {$place :number select=ordinal}\n", "* {{You finished in the {$place}th place}}\n", "two {{You finished in the {$place}nd place}}\n", @@ -49,83 +174,199 @@ "2 {{You got the silver medal}}\n", "3 {{You got the bronze medal}}\n", "few {{You finished in the {$place}rd place}}" - ] - }, - "variations" : [ - { "params": { "place": 1 }, "exp": "You got the gold medal" }, - { "params": { "place": 2 }, "exp": "You got the silver medal" }, - { "params": { "place": 3 }, "exp": "You got the bronze medal" }, - { "params": { "place": 7 }, "exp": "You finished in the 7th place" }, - { "params": { "place": 21 }, "exp": "You finished in the 21st place" }, - { "params": { "place": 22 }, "exp": "You finished in the 22nd place" }, - { "params": { "place": 23 }, "exp": "You finished in the 23rd place" }, - { "params": { "place": 28 }, "exp": "You finished in the 28th place" } - ] + ], + "params": [{"name": "place", "value": 22}], + "exp": "You finished in the 22nd place" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 23}], + "exp": "You finished in the 23rd place" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 28}], + "exp": "You finished in the 28th place" + }, + { + "comment": "Plural combinations, mixed order", + "src": [ + ".match {$fileCount :number} {$folderCount :number}\n", + " * * {{You found {$fileCount} files in {$folderCount} folders}}\n", + " one one {{You found {$fileCount} file in {$folderCount} folder}}\n", + " one * {{You found {$fileCount} file in {$folderCount} folders}}\n", + " * one {{You found {$fileCount} files in {$folderCount} folder}}" + ], + "params": [{"name": "fileCount", "value": 1}, + {"name": "folderCount", "value": 1}], + "exp": "You found 1 file in 1 folder" + }, + { + "comment": "Plural combinations, mixed order", + "src": [ + ".match {$fileCount :number} {$folderCount :number}\n", + " * * {{You found {$fileCount} files in {$folderCount} folders}}\n", + " one one {{You found {$fileCount} file in {$folderCount} folder}}\n", + " one * {{You found {$fileCount} file in {$folderCount} folders}}\n", + " * one {{You found {$fileCount} files in {$folderCount} folder}}" + ], + "params": [{"name": "fileCount", "value": 1}, + {"name": "folderCount", "value": 5}], + "exp": "You found 1 file in 5 folders" + }, + { + "comment": "Plural combinations, mixed order", + "src": [ + ".match {$fileCount :number} {$folderCount :number}\n", + " * * {{You found {$fileCount} files in {$folderCount} folders}}\n", + " one one {{You found {$fileCount} file in {$folderCount} folder}}\n", + " one * {{You found {$fileCount} file in {$folderCount} folders}}\n", + " * one {{You found {$fileCount} files in {$folderCount} folder}}" + ], + "params": [{"name": "fileCount", "value": 7}, + {"name": "folderCount", "value": 1}], + "exp": "You found 7 files in 1 folder" }, { "comment": "Plural combinations, mixed order", - "shared": { - "srcs": [ + "src": [ ".match {$fileCount :number} {$folderCount :number}\n", " * * {{You found {$fileCount} files in {$folderCount} folders}}\n", " one one {{You found {$fileCount} file in {$folderCount} folder}}\n", " one * {{You found {$fileCount} file in {$folderCount} folders}}\n", " * one {{You found {$fileCount} files in {$folderCount} folder}}" - ] - }, - "variations" : [ - { "params": { "fileCount": 1, "folderCount": 1 }, "exp": "You found 1 file in 1 folder" }, - { "params": { "fileCount": 1, "folderCount": 5 }, "exp": "You found 1 file in 5 folders" }, - { "params": { "fileCount": 7, "folderCount": 1 }, "exp": "You found 7 files in 1 folder" }, - { "params": { "fileCount": 7, "folderCount": 3 }, "exp": "You found 7 files in 3 folders" } - ] + ], + "params": [{"name": "fileCount", "value": 7}, + {"name": "folderCount", "value": 3}], + "exp": "You found 7 files in 3 folders" + }, + { + "comment": "Test that the selection honors the formatting option (`1.00 dollars`)", + "src": [ + ".local $c = {$price :number minimumFractionDigits=$minF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1 }, + { "name": "minF", "value": 0 }], + "exp": "1 dollar" }, { "comment": "Test that the selection honors the formatting option (`1.00 dollars`)", - "shared": { - "srcs": [ + "src": [ ".local $c = {$price :number minimumFractionDigits=$minF}\n", ".match {$c}\n", " one {{{$c} dollar}}\n", " * {{{$c} dollars}}" - ] - }, - "variations" : [ - { "params": { "price": 1, "minF": 0 }, "exp": "1 dollar" }, - { "params": { "price": 1, "minF": 2 }, "exp": "1.00 dollars" } - ] + ], + "params": [{ "name": "price", "value": 1}, + { "name": "minF", "value": 2 }], + "exp": "1.00 dollars" }, { "comment": "Test that the selection honors the formatting option (`1.00 dollars`)", - "shared": { - "srcs": [ + "src": [ ".local $c = {$price :number maximumFractionDigits=$maxF}\n", ".match {$c}\n", " one {{{$c} dollar}}\n", " * {{{$c} dollars}}" - ] - }, - "variations" : [ - { "params": { "price": 1.25, "maxF": 0 }, "exp": "1 dollar" }, - { "params": { "price": 1.25, "maxF": 2 }, "exp": "1.25 dollars" } - ] + ], + "params": [{ "name": "price", "value": 1.25}, + { "name": "maxF", "value": 0 }], + "exp": "1 dollar" + }, + { + "comment": "Test that the selection honors the formatting option (`1.00 dollars`)", + "src": [ + ".local $c = {$price :number maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1.25}, + { "name": "maxF", "value": 2 }], + "exp": "1.25 dollars" + }, + { + "comment": "Test that the selection honors the `:integer` over options", + "src": [ + ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1}, + { "name": "maxF", "value": 0 }], + "exp": "1 dollar" + }, + { + "comment": "Test that the selection honors the `:integer` over options", + "src": [ + ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1}, + { "name": "maxF", "value": 2 }], + "exp": "1 dollar" + }, + { + "comment": "Test that the selection honors the `:integer` over options", + "src": [ + ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1.25}, + { "name": "maxF", "value": 0 }], + "exp": "1 dollar" + }, + { + "comment": "Test that the selection honors the `:integer` over options", + "src": [ + ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1.25 }, + { "name": "maxF", "value": 2 }], + "exp": "1 dollar" }, { "comment": "Test that the selection honors the `:integer` over options", - "shared": { - "srcs": [ + "src": [ ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", ".match {$c}\n", " one {{{$c} dollar}}\n", " * {{{$c} dollars}}" - ] - }, - "variations" : [ - { "params": { "price": 1, "maxF": 0 }, "exp": "1 dollar" }, - { "params": { "price": 1, "maxF": 2 }, "exp": "1 dollar" }, - { "params": { "price": 1.25, "maxF": 0 }, "exp": "1 dollar" }, - { "params": { "price": 1.25, "maxF": 2 }, "exp": "1 dollar" }, - { "params": { "price": 4.12345, "maxF": 4 }, "exp": "4 dollars" } - ] + ], + "params": [{ "name": "price", "value": 4.12345 }, + { "name": "maxF", "value": 4 }], + "exp": "4 dollars" } -] + ] +} diff --git a/testdata/message2/invalid-number-literals-diagnostics.json b/testdata/message2/invalid-number-literals-diagnostics.json index 1c1e84f53f37..d35c16b23386 100644 --- a/testdata/message2/invalid-number-literals-diagnostics.json +++ b/testdata/message2/invalid-number-literals-diagnostics.json @@ -1,4 +1,15 @@ -[ +{ + "scenario": "Number literal syntax errors", + "description": "Syntax errors with number literals; for ICU4C, the character offset in the parse error is checked", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ { "src": "{00}", "char": 2}, { "src": "{042}", "char": 2}, { "src": "{1.}", "char": 3}, @@ -10,4 +21,5 @@ { "src": "{1e+}", "char": 4}, { "src": "{1e-}", "char": 4}, { "src": "{1.0e2.0}", "char": 6} -] + ] +} diff --git a/testdata/message2/invalid-options.json b/testdata/message2/invalid-options.json index 8b4740ee9bcb..698583cd5578 100644 --- a/testdata/message2/invalid-options.json +++ b/testdata/message2/invalid-options.json @@ -1,34 +1,47 @@ -[ - { "src": ".local $foo = {1 :number minimumIntegerDigits=-1} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], +{ + "scenario": "Bad options for built-in functions", + "description": "Tests for the bad-option error; only run in ICU4C for now", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "bad-option" + } + ] + }, + "tests": [ + { "comment": "Neither impl validates options right now; see https://github.com/unicode-org/message-format-wg/issues/738", + "src": ".local $foo = {1 :number minimumIntegerDigits=-1} {{bar {$foo}}}", + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number minimumIntegerDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number minimumFractionDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number maximumFractionDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number minimumSignificantDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number maximumSignificantDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :integer minimumIntegerDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :integer maximumSignificantDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" } -] + ] +} diff --git a/testdata/message2/markup.json b/testdata/message2/markup.json index 52c86b7aa637..29b4408e2c4a 100644 --- a/testdata/message2/markup.json +++ b/testdata/message2/markup.json @@ -1,14 +1,15 @@ -[ - { "src": "{#tag}", "exp": "" }, +{ + "scenario": "Markup", + "description": "Tests for valid markup strings", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "{#tag/}", "exp": "" }, { "src": "{/tag}", "exp": "" }, - { "src": "{#tag}content", "exp": "content" }, { "src": "{#tag}content{/tag}", "exp": "content" }, - { "src": "{/tag}content", "exp": "content" }, - { "src": "{#tag foo=bar}", "exp": "" }, - { "src": "{/tag foo=bar}", "exp": "" }, - { "src": "{#tag foo=bar/}", "exp": "" }, { "src": "{#tag foo=|foo| bar=$bar}", - "params": { "bar": "b a r" }, + "params": [{ "name": "bar", "value": "b a r" }], "exp": "" } -] + ] +} diff --git a/testdata/message2/matches-whitespace.json b/testdata/message2/matches-whitespace.json index d0b2c4ecdfe7..a0af4c4d143e 100644 --- a/testdata/message2/matches-whitespace.json +++ b/testdata/message2/matches-whitespace.json @@ -1,4 +1,10 @@ -[ +{ + "scenario": "Matches with whitespace", + "description": "Tests for valid match constructs with whitespace in various places", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": ".match {one :string} {bar :string} one * {{one}} * * {{other}}", "exp": "one" }, { "src": ".match {foo :string} {bar :string}one * {{one}} * * {{other}}", @@ -23,4 +29,6 @@ }, { "src": ".match {foo :string} {bar :string} one * {{one}} * * {{foo}}", "exp": "foo" } -] + ] +} + diff --git a/testdata/message2/more-data-model-errors.json b/testdata/message2/more-data-model-errors.json index 0c86c58a5a12..14c33c66d353 100644 --- a/testdata/message2/more-data-model-errors.json +++ b/testdata/message2/more-data-model-errors.json @@ -1,34 +1,205 @@ { - "Variant Key Mismatch": [ - ".match {$foo :number} {$bar :number} one{{one}}", - ".match {$foo :number} {$bar :number} one {{one}}", - ".match {$foo :number} {$bar :number} one {{one}}", - ".match {$foo :number} * * {{foo}}", - ".match {$one :number}\n 1 2 {{Too many}}\n * {{Otherwise}}", - ".match {$one :number} {$two :number}\n 1 2 {{Two keys}}\n * {{Missing a key}}\n * * {{Otherwise}}", - ".match {$foo :x} {$bar :x} * {{foo}}" - ], - "Missing Fallback Variant": [ - ".match {$one :number}\n 1 {{Value is one}}\n 2 {{Value is two}}", - ".match {$one :number} {$two :number}\n 1 * {{First is one}}\n * 1 {{Second is one}}" - ], - "Missing Selector Annotation": [ - ".match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", - ".local $one = {|The one|}\n .match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", - ".match {|horse| ^private}\n 1 {{The value is one.}} * {{The value is not one.}}", - ".match {$foo !select} |1| {{one}} * {{other}}", - ".match {$foo ^select} |1| {{one}} * {{other}}", - ".input {$foo} .match {$foo} one {{one}} * {{other}}", - ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}" - ], - "Duplicate Declaration": [ - ".local $x = {|1|} .input {$x :number} {{{$x}}}", - ".input {$x :number} .input {$x :string} {{{$x}}}" - ], - "Duplicate Option Name": [ - "{:foo a=1 b=2 a=1}", - "{:foo a=1 a=1}", - "{:foo a=1 a=2}", - "{|x| :foo a=1 a=2}" - ] + "scenario": "Additional data model errors", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": ".match {$foo :number} {$bar :number} one{{one}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$foo :number} {$bar :number} one {{one}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$foo :number} {$bar :number} one {{one}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$foo :number} * * {{foo}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$one :number}\n 1 2 {{Too many}}\n * {{Otherwise}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$one :number} {$two :number}\n 1 2 {{Two keys}}\n * {{Missing a key}}\n * * {{Otherwise}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$foo :x} {$bar :x} * {{foo}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$one :number}\n 1 {{Value is one}}\n 2 {{Value is two}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + + { + "src": ".match {$one :number} {$two :number}\n 1 * {{First is one}}\n * 1 {{Second is one}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + + { + "src": ".match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".local $one = {|The one|}\n .match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".match {|horse| ^private}\n 1 {{The value is one.}} * {{The value is not one.}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".match {$foo !select} |1| {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".match {$foo ^select} |1| {{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": ".local $x = {|1|} .input {$x :number} {{{$x}}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + + { + "src": ".input {$x :number} .input {$x :string} {{{$x}}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + + { + "src": "{:foo a=1 b=2 a=1}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + + { + "src": "{:foo a=1 a=1}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + + { + "src": "{:foo a=1 a=2}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + + { + "src": "{|x| :foo a=1 a=2}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + } + ] } diff --git a/testdata/message2/more-functions.json b/testdata/message2/more-functions.json index 99640a4daed3..83a55151f786 100644 --- a/testdata/message2/more-functions.json +++ b/testdata/message2/more-functions.json @@ -1,5 +1,10 @@ { - "number": [ + "scenario": "Function tests 2", + "description": "More tests for ICU-specific formatting behavior.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "Format {123456789.9876 :number} number", "locale": "en-IN", @@ -19,54 +24,54 @@ "comment": "From ICU4J tests, with explicit formatter added" }, { - "srcs": [".local $dateStr = {$date :datetime}\n", + "src": [".local $dateStr = {$date :datetime}\n", "{{Testing date formatting: {$dateStr :datetime}.}}"], "exp": "Testing date formatting: 23.11.2022, 19:42.", "locale": "ro", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 }}] }, { "src": "Testing date formatting: {$date :date style=long}.", "exp": "Testing date formatting: November 23, 2022.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :date style=medium}.", "exp": "Testing date formatting: Nov 23, 2022.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :date style=short}.", "exp": "Testing date formatting: 11/23/22.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :time style=long}.", "exp": "Testing date formatting: 7:42:37\u202FPM PST.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :time style=medium}.", "exp": "Testing date formatting: 7:42:37\u202FPM.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :time style=short}.", "exp": "Testing date formatting: 7:42\u202FPM.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { - "srcs": [".local $num = {|42| :number}\n", + "src": [".local $num = {|42| :number}\n", "{{Testing date formatting: {$num :datetime}}}"], "exp": "Testing date formatting: {|42|}", - "errors": [{"type": "bad-input"}] + "expErrors": [{"type": "bad-operand"}] }, { "src": "From literal: {|123456789,531| :number}!", "exp": "From literal: {|123456789,531|}!", "locale": "en-IN", - "params": {"val": 1234567890.97531}, - "errors": [{"type": "bad-input"}], + "params": [{ "name": "val", "value": 1234567890.97531 }], + "expErrors": [{"type": "bad-operand"}], "comment": "Should fail because number literals are not treated as localized numbers", "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, @@ -74,32 +79,32 @@ "src": "From literal: {|123456789.531| :number}!", "exp": "From literal: \u1041\u1042\u1043,\u1044\u1045\u1046,\u1047\u1048\u1049.\u1045\u1043\u1041!", "locale": "my", - "params": {"val": 1234567890.97531} + "params": [{ "name": "val", "value": 1234567890.97531 }] }, { "src": "Default double: {$val}!", "exp": "Default double: 1,23,45,67,890.97531!", "locale": "en-IN", - "params": {"val": 1234567890.97531}, + "params": [{ "name": "val", "value": 1234567890.97531 }], "comment": "The next few tests check that numeric variables are formatted without specifying :number" }, { "src": "Default double: {$val}!", "exp": "Default double: 1.234.567.890,97531!", "locale": "ro", - "params": {"val": 1234567890.97531} + "params": [{ "name": "val", "value": 1234567890.97531 }] }, { "src": "Default float: {$val}!", "exp": "Default float: 3,141593!", "locale": "ro", - "params": {"val": 3.1415926535} + "params": [{"name": "val", "value": 3.1415926535}] }, { "src": "Default int64: {$val}!", "exp": "Default int64: 1.234.567.890.123.456.800!", "locale": "ro", - "params": {"val": 1234567890123456789}, + "params": [{ "name": "val", "value": 1234567890123456789 }], "comment": "Rounded due to JSON not supporting full 64-bit ints", "ignoreJava": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175932" }, @@ -107,7 +112,7 @@ "src": "Default int64: {$val}!", "exp": "Default int64: 1.234.567.890.123.456.770!", "locale": "ro", - "params": {"val": 1234567890123456789}, + "params": [{ "name": "val", "value": 1234567890123456789 }], "comment": "Rounded due to JSON not supporting full 64-bit ints", "ignoreCpp": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175932" }, @@ -115,7 +120,7 @@ "src": "Default number: {$val}!", "exp": "Default number: 1.234.567.890.123.456.789,987654!", "locale": "ro", - "params": {"val": {"decimal": "1234567890123456789.987654321"}} + "params": [{ "name": "val", "value": {"decimal": "1234567890123456789.987654321"} }] } ] } diff --git a/testdata/message2/more-syntax-errors.json b/testdata/message2/more-syntax-errors.json deleted file mode 100644 index 48f145b452c0..000000000000 --- a/testdata/message2/more-syntax-errors.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - ".input {$x :number}", - ".local $foo = {|1|}", - ".unsupported |statement| {$x :number}" -] diff --git a/testdata/message2/reserved-syntax.json b/testdata/message2/reserved-syntax.json deleted file mode 100644 index 9efd6fe6b9c9..000000000000 --- a/testdata/message2/reserved-syntax.json +++ /dev/null @@ -1,40 +0,0 @@ -[ - { "src": "hello {|4.2| %number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| %n|um|ber}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{+42}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| &num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| ^num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| +num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| ?num|be||r|s}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|foo| !number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|foo| *number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {?number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {$foo ~xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {$x xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{ !xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{~xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{ num x \\\\ abcde |aaa||3.14||42| r }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {$foo >num x \\\\ abcde |aaa||3.14| |42| r }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" } -] - diff --git a/testdata/message2/resolution-errors.json b/testdata/message2/resolution-errors.json index 077e8b0e9c3b..610588e0cf5e 100644 --- a/testdata/message2/resolution-errors.json +++ b/testdata/message2/resolution-errors.json @@ -1,14 +1,21 @@ -[ - { "src": "{$oops}", "exp": "{$oops}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "ICU4J doesn't signal unresolved variable errors?"}, - { "src": ".input {$x :number} {{{$x}}}", "exp": "{$x}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, - { "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", "exp": "other", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, - { "src": ".local $bar = {$none :number} .match {$foo :string} one {{one}} * {{{$bar}}}", "exp": "{$none}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, - { "src": "The value is {horse :func}.", "exp": "The value is {|horse|}.", "errors": [{ "type": "missing-func" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, - { "src": ".matc {-1} {{hello}}", - "errors": [{ "type": "unsupported-statement" }], +{ + "scenario": "Resolution errors", + "description": "Tests for unknown variables and functions", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { "src": "{$oops}", "exp": "{$oops}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "ICU4J doesn't signal unresolved variable errors?"}, + { "src": ".input {$x :number} {{{$x}}}", "exp": "{$x}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, + { "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", "exp": "other", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, + { "src": ".local $bar = {$none :number} .match {$foo :string} one {{one}} * {{{$bar}}}", "exp": "{$none}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, + { "src": "The value is {horse :func}.", "exp": "The value is {|horse|}.", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, + { "src": ".matc {-1} {{hello}}", + "expErrors": [{ "type": "unsupported-statement" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": ".m {-1} {{hello}}", - "errors": [{ "type": "unsupported-statement" }], + { "src": ".m {-1} {{hello}}", + "expErrors": [{ "type": "unsupported-statement" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" } -] + ] +} diff --git a/testdata/message2/runtime-errors.json b/testdata/message2/runtime-errors.json index 9c7bfbc58443..b1bb0cd491a0 100644 --- a/testdata/message2/runtime-errors.json +++ b/testdata/message2/runtime-errors.json @@ -1,26 +1,27 @@ -[ +{ + "scenario": "Runtime errors", + "description": "Tests for bad-selector and bad-operand errors", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": ".match {|horse| :date}\n 1 {{The value is one.}}\n * {{Formatter used as selector.}}", "exp": "Formatter used as selector.", - "errors": [{"type": "selector-error"}], - "ignoreJava": "ICU4J doesn't signal runtime errors?" - }, - { - "src": ".match {|horse| :string}\n 1 {{The value is one.}}\n * {{Selector used as formatter: {|horse| :string}}}", - "exp": "Selector used as formatter: {|horse|}", - "errors": [{"type": "formatting-error"}], + "expErrors": [{"type": "bad-selector"}], "ignoreJava": "ICU4J doesn't signal runtime errors?" }, { "src": ".match {|horse| :number}\n 1 {{The value is one.}}\n * {{horse is not a number.}}", "exp": "horse is not a number.", - "errors": [{"type": "selector-error"}], + "expErrors": [{"type": "bad-selector"}], "ignoreJava": "ICU4J doesn't signal runtime errors?" }, { "src": ".local $sel = {|horse| :number}\n .match {$sel}\n 1 {{The value is one.}}\n * {{horse is not a number.}}", "exp": "horse is not a number.", - "errors": [{"type": "selector-error"}], + "expErrors": [{"type": "bad-selector"}], "ignoreJava": "ICU4J doesn't signal runtime errors?" } -] + ] +} diff --git a/testdata/message2/spec/data-model-errors.json b/testdata/message2/spec/data-model-errors.json index 0a6bd67641b6..86a674c43961 100644 --- a/testdata/message2/spec/data-model-errors.json +++ b/testdata/message2/spec/data-model-errors.json @@ -1,32 +1,185 @@ { - "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}" - ] + "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json", + "scenario": "Data model errors", + "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" + } + ] + }, + { + "src": ".match {$var :string} * {{The first default}} * {{The second default}}", + "expErrors": [ + { + "type": "duplicate-variant" + } + ] + }, + { + "src": ".match {$x :string} {$y :string} * foo {{The first foo variant}} bar * {{The bar variant}} * |foo| {{The second foo variant}} * * {{The default variant}}", + "expErrors": [ + { + "type": "duplicate-variant" + } + ] + } + ] } diff --git a/testdata/message2/spec/functions/date.json b/testdata/message2/spec/functions/date.json new file mode 100644 index 000000000000..dd14e6785fb6 --- /dev/null +++ b/testdata/message2/spec/functions/date.json @@ -0,0 +1,46 @@ +{ + "scenario": "Date function", + "description": "The built-in formatter for dates.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [] + }, + "tests": [ + { + "src": "{:date}", + "exp": "{:date}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{horse :date}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "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}}}", + "ignoreJava": "ICU4J doesn't support this kind of composition" + } + ] +} diff --git a/testdata/message2/spec/functions/datetime.json b/testdata/message2/spec/functions/datetime.json new file mode 100644 index 000000000000..bdfea3096cda --- /dev/null +++ b/testdata/message2/spec/functions/datetime.json @@ -0,0 +1,68 @@ +{ + "scenario": "Datetime function", + "description": "The built-in formatter for datetimes.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [] + }, + "tests": [ + { + "src": "{:datetime}", + "exp": "{:datetime}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{$x :datetime}", + "exp": "{$x}", + "params": [ + { + "name": "x", + "value": true + } + ], + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{horse :datetime}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "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/testdata/message2/spec/functions/integer.json b/testdata/message2/spec/functions/integer.json new file mode 100644 index 000000000000..c8e75077a221 --- /dev/null +++ b/testdata/message2/spec/functions/integer.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json", + "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/testdata/message2/spec/functions/number.json b/testdata/message2/spec/functions/number.json new file mode 100644 index 000000000000..1b81c705622b --- /dev/null +++ b/testdata/message2/spec/functions/number.json @@ -0,0 +1,407 @@ +{ + "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": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|.1| :number}", + "exp": "invalid number literal {|.1|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|1.| :number}", + "exp": "invalid number literal {|1.|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|01| :number}", + "exp": "invalid number literal {|01|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|+1| :number}", + "exp": "invalid number literal {|+1|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|0x1| :number}", + "exp": "invalid number literal {|0x1|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "hello {:number}", + "exp": "hello {:number}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "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": "bad-option" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": "foo" + } + ], + "exp": "bar {$bar}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "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": "bad-option" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".input {$foo :number} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar {$foo}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "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 {$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" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{42 :number @foo @bar=13}", + "exp": "42", + "expParts": [ + { + "type": "number", + "source": "|42|", + "parts": [ + { + "type": "integer", + "value": "42" + } + ] + } + ] + } + ] +} diff --git a/testdata/message2/spec/functions/string.json b/testdata/message2/spec/functions/string.json new file mode 100644 index 000000000000..5858079b99a6 --- /dev/null +++ b/testdata/message2/spec/functions/string.json @@ -0,0 +1,51 @@ +{ + "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", + "ignoreJava": ":string doesn't stringify numbers?" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": null + } + ], + "exp": "other", + "ignoreCpp": "Can't handle null value for input variable" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "exp": "other", + "expErrors": [ + { + "type": "unresolved-variable" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + } + ] +} diff --git a/testdata/message2/spec/functions/time.json b/testdata/message2/spec/functions/time.json new file mode 100644 index 000000000000..845934a5e16a --- /dev/null +++ b/testdata/message2/spec/functions/time.json @@ -0,0 +1,43 @@ +{ + "scenario": "Time function", + "description": "The built-in formatter for times.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [] + }, + "tests": [ + { + "src": "{:time}", + "exp": "{:time}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{horse :time}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "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}}}", + "ignoreJava": "ICU4J doesn't support this kind of composition" + } + ] +} diff --git a/testdata/message2/spec/syntax-errors.json b/testdata/message2/spec/syntax-errors.json index fc4537131c83..34d9aa484572 100644 --- a/testdata/message2/spec/syntax-errors.json +++ b/testdata/message2/spec/syntax-errors.json @@ -1,56 +1,180 @@ -[ - ".", - "{", - "}", - "{}", - "{{", - "{{}", - "{{}}}", - "{|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}}" -] +{ + "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json", + "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": "{ @misplaced = attribute }" + }, + { + "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/testdata/message2/spec/syntax.json b/testdata/message2/spec/syntax.json new file mode 100644 index 000000000000..1b901eb639d7 --- /dev/null +++ b/testdata/message2/spec/syntax.json @@ -0,0 +1,970 @@ +{ + "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json", + "scenario": "Syntax", + "description": "Test cases that do not depend on any registry definitions.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "description": "message -> simple-message -> ", + "src": "", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> simple-start-char", + "src": "a", + "exp": "a" + }, + { + "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ...", + "src": "hello", + "exp": "hello" + }, + { + "description": "message -> simple-message -> simple-start pattern -> escaped-char", + "src": "\\\\", + "exp": "\\" + }, + { + "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ... -> simple-start-char *text-char placeholder", + "src": "hello {world}", + "exp": "hello world" + }, + { + "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ... -> simple-start-char *text-char placeholder", + "src": "hello {|world|}", + "exp": "hello world" + }, + { + "description": "message -> simple-message -> s simple-start pattern -> s simple-start-char pattern -> ...", + "src": "\n hello\t", + "exp": "\n hello\t" + }, + { + "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}", + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> expression -> literal-expression -> \"{\" literal \"}\"", + "src": "{a}", + "exp": "a" + }, + { + "description": "... -> literal-expression -> \"{\" literal s annotation \"}\" -> \"{\" literal s function \"}\" -> \"{\" literal s \":\" identifier \"}\" -> \"{\" literal s \":\" name \"}\"", + "src": "{a :f}", + "exp": "{|a|}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> \"{\" literal s \":\" namespace \":\" name \"}\"", + "src": "{a :u:f}", + "exp": "{|a|}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> expression -> variable-expression -> \"{\" variable \"}\"", + "src": "{$x}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> variable-expression -> \"{\" variable s annotation \"}\" -> \"{\" variable s function \"}\" -> \"{\" variable s \":\" identifier \"}\" -> \"{\" variable s \":\" name \"}\"", + "src": "{$x :f}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }, { "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> \"{\" variable s \":\" namespace \":\" name \"}\"", + "src": "{$x :u:f}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }, { "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> annotation-expression -> function -> \"{\" \":\" namespace \":\" name \"}\"", + "src": "{:u:f}", + "exp": "{:u:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> annotation-expression -> function -> \"{\" \":\" name \"}\"", + "src": "{:f}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> complex-message -> complex-body -> quoted-pattern -> \"{{\" pattern \"}}\" -> \"{{\"\"}}\"", + "src": "{{}}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier \"}\"", + "src": "{#tag}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag" + } + ] + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> input-declaration complex-body -> input variable-expression complex-body", + "src": ".input{$x}{{}}", + "exp": "" + }, + { + "description": "message -> complex-message -> s *(declaration [s]) complex-body s -> s declaration complex-body s -> s input-declaration complex-body s -> s input variable-expression complex-body s", + "src": "\t.input{$x}{{}}\n", + "exp": "" + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration declaration complex-body -> input-declaration input-declaration complex-body -> input variable-expression input variable-expression complex-body", + "src": ".input{$x}.input{$y}{{}}", + "exp": "" + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration s declaration complex-body -> input-declaration s input-declaration complex-body -> input variable-expression s input variable-expression complex-body", + "src": ".input{$x} .input{$y}{{}}", + "exp": "" + }, + { + "description": "message -> complex-message -> s *(declaration [s]) complex-body s -> s complex-body s", + "src": " {{}} ", + "exp": "" + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration declaration complex-body -> local-declaration input-declaration complex-body -> local s variable [s] \"=\" [s] expression input variable-expression complex-body", + "src": ".local $x ={a}.input{$y}{{}}", + "exp": "" + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> reserved-statement complex-body -> reserved-keyword expression -> \".\" name expression complex-body", + "src": ".n{a}{{}}", + "exp": "", + "expErrors": [ { "type": "unsupported-statement" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> complex-message -> complex-body -> matcher -> match-statement variant -> match selector key quoted-pattern -> \".match\" expression literal quoted-pattern", + "src": ".match{a :f}a{{}}*{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... input-declaration -> input s variable-expression ...", + "src": ".input {$x}{{}}", + "exp": "" + }, + { + "description": "... local-declaration -> local s variable s \"=\" expression ...", + "src": ".local $x ={a}{{}}", + "exp": "" + }, + { + "description": "... local-declaration -> local s variable \"=\" s expression ...", + "src": ".local $x= {a}{{}}", + "exp": "" + }, + { + "description": "... local-declaration -> local s variable s \"=\" expression ...", + "src": ".local $x = {a}{{}}", + "exp": "" + }, + { + "description": "... matcher -> match-statement [s] variant -> match 1*([s] selector) variant -> match selector selector variant -> match selector selector variant key s key quoted-pattern", + "src": ".match{a :f}{b :f}a b{{}}* *{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... matcher -> match-statement [s] variant -> match 1*([s] selector) variant -> match selector variant variant ...", + "src": ".match{a :f}a{{}}b{{}}*{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... variant -> key s quoted-pattern -> ...", + "src": ".match{a :f}a {{}}*{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... variant -> key s key s quoted-pattern -> ...", + "src": ".match{a :f}{b :f}a b {{}}* *{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... key -> \"*\" ...", + "src": ".match{a :f}*{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "simple-message -> simple-start pattern -> placeholder -> expression -> literal-expression -> \"{\" s literal \"}\"", + "src": "{ a}", + "exp": "a" + }, + { + "description": "... literal-expression -> \"{\" literal s attribute \"}\" -> \"{\" literal s \"@\" identifier \"}\"", + "src": "{a @c}", + "exp": "a" + }, + { + "description": "... -> literal-expression -> \"{\" literal s \"}\"", + "src": "{a }", + "exp": "a" + }, + { + "description": "simple-message -> simple-start pattern -> placeholder -> expression -> variable-expression -> \"{\" s variable \"}\"", + "src": "{ $x}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... variable-expression -> \"{\" variable s attribute \"}\" -> \"{\" variable s \"@\" identifier \"}\"", + "src": "{$x @c}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> variable-expression -> \"{\" variable s \"}\"", + "src": "{$x }", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "simple-message -> simple-start pattern -> placeholder -> expression -> annotation-expression -> \"{\" s annotation \"}\"", + "src": "{ :f}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... annotation-expression -> \"{\" annotation s attribute \"}\" -> \"{\" annotation s \"@\" identifier \"}\"", + "src": "{:f @c}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> annotation-expression -> \"{\" annotation s \"}\"", + "src": "{:f }", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... annotation -> private-use-annotation -> private-start", + "src": "{^}", + "exp": "{^}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... annotation -> reserved-annotation -> reserved-annotation-start", + "src": "{!}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"#\" identifier \"}\"", + "src": "{ #a}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier option \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"", + "src": "{#tag foo=bar}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag", + "options": { + "foo": "bar" + } + } + ] + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier attribute \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"", + "src": "{#a @c}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier s \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"", + "src": "{#a }", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier \"/\" \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"", + "src": "{#a/}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier \"}\"", + "src": "{/a}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"/\" identifier \"}\"", + "src": "{ /a}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier option \"}\"", + "src": "{/tag foo=bar}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "close", + "name": "tag", + "options": { + "foo": "bar" + } + } + ] + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier s \"}\"", + "src": "{/a }", + "exp": "" + }, + { + "description": "... annotation-expression -> function -> \":\" identifier option", + "src": "{:f k=v}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... option -> identifier s \"=\" literal", + "src": "{:f k =v}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... option -> identifier \"=\" s literal", + "src": "{:f k= v}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... option -> identifier s \"=\" s literal", + "src": "{:f k = v}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... attribute -> \"@\" identifier \"=\" literal ...", + "src": "{a @c=d}", + "exp": "a" + }, + { + "description": "... attribute -> \"@\" identifier s \"=\" literal ...", + "src": "{a @c =d}", + "exp": "a" + }, + { + "description": "... attribute -> \"@\" identifier \"=\" s literal ...", + "src": "{a @c= d}", + "exp": "a" + }, + { + "description": "... attribute -> \"@\" identifier s \"=\" s literal ...", + "src": "{a @c = d}", + "exp": "a" + }, + { + "description": "... attribute -> \"@\" identifier s \"=\" s variable ...", + "src": "{42 @foo=$bar}", + "exp": "42", + "expParts": [ + { + "type": "string", + "source": "|42|", + "value": "42" + } + ] + }, + { + "description": "... literal -> quoted-literal -> \"|\" \"|\" ...", + "src": "{||}", + "exp": "" + }, + { + "description": "... quoted-literal -> \"|\" quoted-char \"|\"", + "src": "{|a|}", + "exp": "a" + }, + { + "description": "... quoted-literal -> \"|\" escaped-char \"|\"", + "src": "{|\\\\|}", + "exp": "\\" + }, + { + "description": "... quoted-literal -> \"|\" quoted-char escaped-char \"|\"", + "src": "{|a\\\\|}", + "exp": "a\\" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30", + "src": "{0}", + "exp": "0" + }, + { + "description": "... unquoted-literal -> number-literal -> \"-\" %x30", + "src": "{-0}", + "exp": "-0" + }, + { + "description": "... unquoted-literal -> number-literal -> (%x31-39 *DIGIT) -> %x31", + "src": "{1}", + "exp": "1" + }, + { + "description": "... unquoted-literal -> number-literal -> (%x31-39 *DIGIT) -> %x31 DIGIT -> 11", + "src": "{11}", + "exp": "11" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 \".\" 1*DIGIT -> 0 \".\" 1", + "src": "{0.1}", + "exp": "0.1" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 \".\" 1*DIGIT -> %x30 \".\" DIGIT DIGIT -> 0 \".\" 1 2", + "src": "{0.12}", + "exp": "0.12" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" 1*DIGIT -> %x30 \"e\" DIGIT", + "src": "{0e1}", + "exp": "0e1" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" 1*DIGIT -> %x30 \"E\" DIGIT", + "src": "{0E1}", + "exp": "0E1" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" \"-\" 1*DIGIT ...", + "src": "{0E-1}", + "exp": "0E-1" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" \"+\" 1*DIGIT ...", + "src": "{0E-1}", + "exp": "0E-1" + }, + { + "description": "... reserved-statement -> reserved-keyword s reserved-body 1*([s] expression) -> reserved-keyword s reserved-body expression -> \".\" name s reserved-body-part expression -> \".\" name s reserved-char expression ...", + "src": ".n .{a}{{}}", + "exp": "", + "expErrors": [ { "type": "unsupported-statement" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-statement -> reserved-keyword reserved-body 1*([s] expression) -> reserved-keyword s reserved-body s expression -> \".\" name s reserved-body-part expression -> \".\" name s reserved-char expression ...", + "src": ".n. {a}{{}}", + "exp": "", + "expErrors": [ { "type": "unsupported-statement" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-statement -> reserved-keyword reserved-body 1*([s] expression) -> reserved-keyword reserved-body expression expression -> \".\" name reserved-body-part expression expression -> \".\" name s reserved-char expression expression ...", + "src": ".n.{a}{b}{{}}", + "exp": "", + "expErrors": [ { "type": "unsupported-statement" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part -> \"!\" reserved-char ...", + "src": "{!.}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start s reserved-body -> \"!\" s reserved-body-part -> \"!\" s reserved-char ...", + "src": "{! .}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{%}", + "exp": "{%}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{*}", + "exp": "{*}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{+}", + "exp": "{+}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{<}", + "exp": "{<}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{>}", + "exp": "{>}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{?}", + "exp": "{?}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{~}", + "exp": "{~}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... private-use-annotation -> private-start reserved-body -> \"^\" reserved-body-part -> \"^\" reserved-char ...", + "src": "{^.}", + "exp": "{^}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... private-use-annotation -> private-start s reserved-body -> \"^\" s reserved-body-part -> \"^\" s reserved-char ...", + "src": "{^ .}", + "exp": "{^}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... private-start ...", + "src": "{&}", + "exp": "{&}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part reserved-body-part -> \"!\" reserved-char escaped-char ...", + "src": "{!.\\{}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part s reserved-body-part -> \"!\" reserved-char s escaped-char ...", + "src": "{!. \\{}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part -> \"!\" quoted-literal ...", + "src": "{!|a|}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "hello { world\t\n}", + "exp": "hello world" + }, + { + "src": "hello {\u3000world\r}", + "exp": "hello world" + }, + { + "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}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": "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": "{42 @foo @bar=13}", + "exp": "42", + "expParts": [ + { + "type": "string", + "source": "|42|", + "value": "42" + } + ] + }, + { + "src": "foo {+reserved}", + "exp": "foo {+}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "+" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "foo {&private}", + "exp": "foo {&}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "&" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "foo {?reserved @a @b=$c}", + "exp": "foo {?}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "?" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".foo {42} {{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".foo{42}{{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".foo |}lit{| {42}{{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".l $y = {|bar|} {{}}", + "exp": "", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{{trailing whitespace}} \n", + "exp": "trailing whitespace" + } + ] +} diff --git a/testdata/message2/spec/test-core.json b/testdata/message2/spec/test-core.json deleted file mode 100644 index 2ee0786e62de..000000000000 --- a/testdata/message2/spec/test-core.json +++ /dev/null @@ -1,219 +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}", - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "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" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "foo {&private}", - "exp": "foo {&}", - "parts": [ - { "type": "literal", "value": "foo " }, - { "type": "fallback", "source": "&" } - ], - "errors": [{ "type": "unsupported-annotation" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "foo {?reserved @a @b=$c}", - "exp": "foo {?}", - "parts": [ - { "type": "literal", "value": "foo " }, - { "type": "fallback", "source": "?" } - ], - "errors": [{ "type": "unsupported-annotation" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": ".foo {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": ".foo{42}{{bar}}", - "cleanSrc": ".foo {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": ".foo |}lit{| {42}{{bar}}", - "cleanSrc": ".foo |}lit{| {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - } -] diff --git a/testdata/message2/spec/test-functions.json b/testdata/message2/spec/test-functions.json deleted file mode 100644 index a8979106947e..000000000000 --- a/testdata/message2/spec/test-functions.json +++ /dev/null @@ -1,343 +0,0 @@ -{ - "date": [ - { "src": "{:date}", - "exp": "{:date}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "{horse :date}", - "exp": "{|horse|}", - "errors": [{ "type": "bad-input" }], - "errorsJs": [{ "name": "RangeError" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { "src": "{|2006-01-02| :date}", "exp": "1/2/06" }, - { "src": "{|2006-01-02T15:04:06| :date}", "exp": "1/2/06" }, - { "src": "{|2006-01-02| :date style=long}", "exp": "January 2, 2006" }, - { - "src": ".local $d = {|2006-01-02| :date style=long} {{{$d :date}}}", - "exp": "January 2, 2006" - }, - { - "ignoreJava": "Can't chain :time and :date, they are different types", - "src": ".local $t = {|2006-01-02T15:04:06| :time} {{{$t :date}}}", - "exp": "1/2/06" - } - ], - "time": [ - { "src": "{:time}", "exp": "{:time}", "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "{horse :time}", - "exp": "{|horse|}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { "src": "{|2006-01-02T15:04:06| :time}", "expJs": "3:04 PM", "exp": "3:04 PM" }, - { - "src": "{|2006-01-02T15:04:06| :time style=medium}", - "expJs": "3:04:06 PM", - "exp": "3:04:06 PM" - }, - { - "src": ".local $t = {|2006-01-02T15:04:06| :time style=medium} {{{$t :time}}}", - "expJs": "3:04:06 PM", - "exp": "3:04:06 PM" - }, - { - "ignoreJava": "Can't chain :time and :date, they are different types", - "src": ".local $d = {|2006-01-02T15:04:06| :date} {{{$d :time}}}", - "exp": "3:04 PM" - } - ], - "datetime": [ - { - "src": "{:datetime}", - "exp": "{:datetime}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "{$x :datetime}", - "exp": "{$x}", - "params": { "x": true }, - "errors": [{ "type": "bad-input" }] - }, - { - "src": "{horse :datetime}", - "exp": "{|horse|}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { "src": "{|2006-01-02T15:04:06| :datetime}", "expJs": "1/2/06, 3:04 PM", "exp": "1/2/06, 3:04 PM" }, - { - "src": "{|2006-01-02T15:04:06| :datetime year=numeric month=|2-digit|}", - "exp": "01/2006" - }, - { - "src": "{|2006-01-02T15:04:06| :datetime dateStyle=long}", - "exp": "January 2, 2006" - }, - { - "src": "{|2006-01-02T15:04:06| :datetime timeStyle=medium}", - "expJs": "3:04:06 PM", - "exp": "3:04:06 PM" - }, - { - "src": "{$dt :datetime}", - "params": { "dt": "2006-01-02T15:04:06" }, - "expJs": "1/2/06, 3:04 PM", - "exp": "1/2/06, 3:04 PM" - }, - { - "ignoreJava": "Can't chain :time and :date, they are different types", - "ignoreCpp": "Same reason as Java", - "src": ".input {$dt :time style=medium} {{{$dt :datetime dateStyle=long}}}", - "params": { "dt": "2006-01-02T15:04:06" }, - "exp": "January 2, 2006 at 3:04:06 PM" - } - ], - "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" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "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 }, - "comment": "I think it is fine to ignore invalid options", - "expJs": "bar {$bar}", - "exp": "bar 4.2", - "errorsJs": [{ "type": "bad-option" }] - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar {$bar}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "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 }, - "comment": "I think it is fine to ignore invalid options", - "exp": "bar 4.2", - "expJs": "bar {$foo}", - "errorsJs": [{ "type": "bad-option" }] - }, - { - "src": ".input {$foo :number} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar {$foo}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "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" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "hello {42 :ordinal}", - "exp": "hello {|42|}", - "errors": [{ "type": "missing-func" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - } - ], - "plural": [ - { - "src": ".match {$foo :plural} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "other", - "errors": [{ "type": "missing-func" }, { "type": "not-selectable" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "hello {42 :plural}", - "exp": "hello {|42|}", - "errors": [{ "type": "missing-func" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - } - ], - "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", - "ignoreJava": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175933" - }, - { - "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" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - } - ] -} diff --git a/testdata/message2/spec/unsupported-expressions.json b/testdata/message2/spec/unsupported-expressions.json new file mode 100644 index 000000000000..f7d611509d23 --- /dev/null +++ b/testdata/message2/spec/unsupported-expressions.json @@ -0,0 +1,53 @@ +{ + "scenario": "Reserved and private annotations", + "description": "Tests for unsupported expressions (reserved/private)", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "unsupported-expression" + } + ] + }, + "tests": [ + { "src": "hello {|4.2| %number}" }, + { "src": "hello {|4.2| %n|um|ber}" }, + { "src": "{+42}" }, + { "src": "hello {|4.2| &num|be|r}" }, + { "src": "hello {|4.2| ^num|be|r}" }, + { "src": "hello {|4.2| +num|be|r}" }, + { "src": "hello {|4.2| ?num|be||r|s}" }, + { "src": "hello {|foo| !number}" }, + { "src": "hello {|foo| *number}" }, + { "src": "hello {?number}" }, + { "src": "{xyzz }" }, + { "src": "hello {$foo ~xyzz }" }, + { "src": "hello {$x xyzz }" }, + { "src": "{ !xyzz }" }, + { "src": "{~xyzz }" }, + { "src": "{ num x \\\\ abcde |aaa||3.14||42| r }" }, + { "src": "hello {$foo >num x \\\\ abcde |aaa||3.14| |42| r }" }, + { "src" : ".input{ $n ~ }{{{$n}}}" } + ] +} + diff --git a/testdata/message2/spec/unsupported-statements.json b/testdata/message2/spec/unsupported-statements.json new file mode 100644 index 000000000000..d944aa0f786d --- /dev/null +++ b/testdata/message2/spec/unsupported-statements.json @@ -0,0 +1,18 @@ +{ + "scenario": "Reserved statements", + "description": "Tests for unsupported statements", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "unsupported-statement" + } + ] + }, + "tests": [ + { "src" : ".i {1} {{}}" }, + { "src" : ".l $y = {|bar|} {{}}" }, + { "src" : ".l $x.y = {|bar|} {{}}" } + ] +} + diff --git a/testdata/message2/syntax-errors-diagnostics-multiline.json b/testdata/message2/syntax-errors-diagnostics-multiline.json index b7b87036f20a..facfadebab47 100644 --- a/testdata/message2/syntax-errors-diagnostics-multiline.json +++ b/testdata/message2/syntax-errors-diagnostics-multiline.json @@ -1,4 +1,15 @@ -[ +{ + "scenario": "Syntax errors with character and line offsets", + "description": "Syntax errors; for ICU4C, the character and line offsets in the parse error are checked", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ { "src": "{{hello wo\nrld", "char": 3, @@ -35,4 +46,5 @@ "line": 3, "comment": "Offset for end-of-input should be 0 here because the line begins after the '\n', but there is no character after the '\n'" } -] + ] +} diff --git a/testdata/message2/syntax-errors-diagnostics.json b/testdata/message2/syntax-errors-diagnostics.json index 2b0188f6b557..5b68ae80f31b 100644 --- a/testdata/message2/syntax-errors-diagnostics.json +++ b/testdata/message2/syntax-errors-diagnostics.json @@ -1,402 +1,348 @@ -[ - { "src": "}{|xyz|", "char": 0, "errors": [{"type": "parse-error"}] }, - { "src": "}", "char": 0, "errors": [{"type": "parse-error"}] }, +{ + "scenario": "Syntax errors with character offsets", + "description": "Syntax errors; for ICU4C, the character offset in the parse error is checked", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ + { "src": "}{|xyz|", "char": 0 }, + { "src": "}", "char": 0 }, { "src": "{{{%\\y{}}", "char": 5, - "comment": "Backslash followed by non-backslash followed by a '{' -- this should be an error immediately after the first backslash", - "errors": [{"type": "parse-error"}] + "comment": "Backslash followed by non-backslash followed by a '{' -- this should be an error immediately after the first backslash" }, { "src": "{%abc|\\z}}", "char": 7, - "comment": "Reserved chars followed by a '|' that doesn't begin a valid literal -- this should be an error at the first invalid char in the literal", - "errors": [{"type": "parse-error"}] + "comment": "Reserved chars followed by a '|' that doesn't begin a valid literal -- this should be an error at the first invalid char in the literal" }, { "src": "{%\\y{p}}", "char": 3, - "comment": "Same pattern, but with a valid reserved-char following the erroneous reserved-escape -- the offset should be the same as with the previous one", - "errors": [{"type": "parse-error"}] + "comment": "Same pattern, but with a valid reserved-char following the erroneous reserved-escape -- the offset should be the same as with the previous one" }, { "src": "{{{%ab|\\z|cd}}", "char": 8, - "comment": "Erroneous literal inside a reserved string -- the error should be at the first erroneous literal char", - "errors": [{"type": "parse-error"}] + "comment": "Erroneous literal inside a reserved string -- the error should be at the first erroneous literal char" }, { "src": "hello {|4.2| %num\\ber}}", "char": 18, - "comment": "Single backslash not allowed", - "errors": [{"type": "parse-error"}] + "comment": "Single backslash not allowed" }, { "src": "hello {|4.2| %num{be\\|r}}", "char": 17, - "comment": "Unescaped '{' not allowed", - "errors": [{"type": "parse-error"}] + "comment": "Unescaped '{' not allowed" }, { "src": "hello {|4.2| %num}be\\|r}}", - "char": 21, - "comment": "Unescaped '}' -- will be interpreted as the end of the reserved string, and the error will be reported at the index of '|', which is when the parser determines that the escaped '|' isn't a valid text-escape", - "errors": [{"type": "parse-error"}] - }, + "char": 23, + "comment": "Unescaped '}' -- will be interpreted as the end of the reserved string, and the error will be reported at the index of the next '}'" }, { "src": "hello {|4.2| %num\\{be|r}}", "char": 25, - "comment": "Unescaped '|' -- will be interpreted as the beginning of a literal. Error at end of input", - "errors": [{"type": "parse-error"}] + "comment": "Unescaped '|' -- will be interpreted as the beginning of a literal. Error at end of input" }, { "src": ".match{|y|}|y|{{{|||}}}", "char": 19, - "comment": "No spaces are required here. The error should be in the pattern, not before", - "errors": [{"type": "parse-error"}] + "comment": "No spaces are required here. The error should be in the pattern, not before" }, { "src": ".match {|y|}|foo|bar {{{a}}}", "char": 17, - "comment": "Missing spaces between keys", - "errors": [{"type": "parse-error"}] + "comment": "Missing spaces between keys" }, { "src": ".match {|y|} |quux| |foo|bar {{{a}}}", "char": 25, - "comment": "Missing spaces between keys", - "errors": [{"type": "parse-error"}] + "comment": "Missing spaces between keys" }, { "src": ".match {|y|} |quux| |foo||bar| {{{a}}}", "char": 26, - "comment": "Missing spaces between keys", - "errors": [{"type": "parse-error"}] + "comment": "Missing spaces between keys" }, { "src": ".match {|y|} |\\q| * %{! {z}", "char": 16, - "comment": "Error parsing the first key -- the error should be there, not in the also-erroneous third key", - "errors": [{"type": "parse-error"}] + "comment": "Error parsing the first key -- the error should be there, not in the also-erroneous third key" }, { "src": ".match {|y|} * %{! {z} |\\q|", "char": 16, - "comment": "Error parsing the second key -- the error should be there, not in the also-erroneous third key", - "errors": [{"type": "parse-error"}] + "comment": "Error parsing the second key -- the error should be there, not in the also-erroneous third key" }, { "src": ".match {|y|} * |\\q| {\\z}", "char": 18, - "comment": "Error parsing the last key -- the error should be there, not in the erroneous pattern", - "errors": [{"type": "parse-error"}] + "comment": "Error parsing the last key -- the error should be there, not in the erroneous pattern" }, { "src": ".match {|y|} {\\|} {@} * * * {{a}}", "char": 14, - "comment": "Non-expression as scrutinee in pattern -- error should be at the first non-expression, not the later non-expression", - "errors": [{"type": "parse-error"}] + "comment": "Non-expression as scrutinee in pattern -- error should be at the first non-expression, not the later non-expression" }, { "src": ".match {|y|} $foo * {{a}} when * :bar {{b}}", "char": 14, - "comment": "Non-key in variant -- error should be there, not in the next erroneous variant", - "errors": [{"type": "parse-error"}] + "comment": "Non-key in variant -- error should be there, not in the next erroneous variant" }, { "src": "{{ foo {|bar|} \\q baz ", "char": 16, - "comment": "Error should be within the first erroneous `text` or expression", - "errors": [{"type": "parse-error"}] + "comment": "Error should be within the first erroneous `text` or expression" }, { "src": "{{{: }}}", "char": 4, - "comment": "':' has to be followed by a function name -- the error should be at the first whitespace character", - "errors": [{"type": "parse-error"}] + "comment": "':' has to be followed by a function name -- the error should be at the first whitespace character" }, { "src": ".local $x = }|foo|}", "char": 12, - "comment": "Expression not starting with a '{'", - "errors": [{"type": "parse-error"}] - }, - { - "src": ".local $x = {|foo|} .l $y = {|bar|} .local $z {|quux|}", - "char": 22, - "comment": "Error should be at the first declaration not starting with a `.local`", - "errors": [{"type": "parse-error"}] + "comment": "Expression not starting with a '{'" }, { "src": ".local $bar {|foo|} {{$bar}}", "char": 12, - "comment": "Missing '=' in `.local` declaration", - "errors": [{"type": "parse-error"}] + "comment": "Missing '=' in `.local` declaration" }, { "src": ".local bar = {|foo|} {{$bar}}", "char": 7, - "comment": "LHS of declaration doesn't start with a '$'", - "errors": [{"type": "parse-error"}] + "comment": "LHS of declaration doesn't start with a '$'" }, { "src": ".local $bar = |foo| {{$bar}}", "char": 14, - "comment": "`.local` RHS isn't an expression", - "errors": [{"type": "parse-error"}] + "comment": "`.local` RHS isn't an expression" }, { "src": "{{extra}}content", "char": 9, - "comment": "Trailing characters that are not whitespace", - "errors": [{"type": "parse-error"}] + "comment": "Trailing characters that are not whitespace" }, { "src": ".match {|x|} * {{foo}}extra", "char": 28, - "comment": "Trailing characters that are not whitespace", - "errors": [{"type": "parse-error"}] + "comment": "Trailing characters that are not whitespace" }, { "src": ".match {$foo :string} {$bar :string} one * {{one}} * * {{other}} ", "char": 66, - "comment": "Trailing whitespace at end of message should not be accepted either", - "errors": [{"type": "parse-error"}] - }, - { - "src": "{{hi}} ", - "char": 6, - "comment": "Trailing whitespace at end of message should not be accepted either", - "errors": [{"type": "parse-error"}] + "comment": "Trailing whitespace at end of message should not be accepted either" }, { "src": "empty { }", "char": 8, - "comment": "Empty expression", - "errors": [{"type": "parse-error"}] + "comment": "Empty expression" }, { "src": ".match {} * {{foo}}", "char": 8, - "comment": "Empty expression", - "errors": [{"type": "parse-error"}] + "comment": "Empty expression" }, { "src": "bad {:}", "char": 6, - "comment": "':' not preceding a function name", - "errors": [{"type": "parse-error"}] + "comment": "':' not preceding a function name" }, { "src": "{{no-equal {|42| :number m }}}", "char": 27, - "comment": "Missing '=' after option name", - "errors": [{"type": "parse-error"}] + "comment": "Missing '=' after option name" }, { "src": "{{no-equal {|42| :number minimumFractionDigits 2}}}", "char": 47, - "comment": "Missing '=' after option name", - "errors": [{"type": "parse-error"}] + "comment": "Missing '=' after option name" }, { "src": "bad {:placeholder option value}", "char": 25, - "comment": "Missing '=' after option name", - "errors": [{"type": "parse-error"}] + "comment": "Missing '=' after option name" }, { "src": "hello {|4.2| :number min=2=3}", "char": 26, - "comment": "Extra '=' after option name", - "errors": [{"type": "parse-error"}] + "comment": "Extra '=' after option name" }, { "src": "hello {|4.2| :number min=2max=3}", "char": 26, - "comment": "Missing space between options", - "errors": [{"type": "parse-error"}] + "comment": "Missing space between options" }, { "src": "hello {|4.2| :number min=|a|max=3}", "char": 28, - "comment": "Missing whitespace between valid options", - "errors": [{"type": "parse-error"}] + "comment": "Missing whitespace between valid options" }, { "src": "hello {|4.2| :number min=|\\a|}", "char": 27, - "comment": "Ill-formed RHS of option -- the error should be within the RHS, not after parsing options", - "errors": [{"type": "parse-error"}] + "comment": "Ill-formed RHS of option -- the error should be within the RHS, not after parsing options" }, { "src": "no-equal {|42| :number {}", "char": 25, - "comment": "Junk after annotation", - "errors": [{"type": "parse-error"}] + "comment": "Junk after annotation" }, { "src": "bad {:placeholder option=}", "char": 25, - "comment": "Missing RHS of option", - "errors": [{"type": "parse-error"}] + "comment": "Missing RHS of option" }, { "src": "bad {:placeholder option}", "char": 24, - "comment": "Missing RHS of option", - "errors": [{"type": "parse-error"}] + "comment": "Missing RHS of option" }, { "src": "bad {$placeholder option}", "char": 18, - "comment": "Annotation is not a function or reserved text", - "errors": [{"type": "parse-error"}] + "comment": "Annotation is not a function or reserved text" }, { "src": "no {$placeholder end", "char": 17, - "comment": "Annotation is not a function or reserved text", - "errors": [{"type": "parse-error"}] + "comment": "Annotation is not a function or reserved text" }, { "src": ".match * {{foo}}", "char": 8, - "comment": "Missing expression in selectors", - "errors": [{"type": "parse-error"}] + "comment": "Missing expression in selectors" }, { "src": ".match |x| * {{foo}}", "char": 7, - "comment": "Non-expression in selectors", - "errors": [{"type": "parse-error"}] + "comment": "Non-expression in selectors" }, { "src": ".match {|x|} * foo", "char": 19, - "comment": "Missing RHS in variant", - "errors": [{"type": "parse-error"}] + "comment": "Missing RHS in variant" }, { "src": "{$:abc}", "char": 2, - "comment": "Variable names can't start with a : or -", - "errors": [{"type": "parse-error"}] + "comment": "Variable names can't start with a : or -" }, { "src": "{$-abc}", "char": 2, - "comment": "Variable names can't start with a : or -", - "errors": [{"type": "parse-error"}] + "comment": "Variable names can't start with a : or -" }, { "src": "{$bar+foo}", "char": 5, - "comment": "Missing space before annotation. Note that {{$bar:foo}} and {{$bar-foo}} are valid, because variable names can contain a ':' or a '-'", - "errors": [{"type": "parse-error"}] + "comment": "Missing space before annotation. Note that {{$bar:foo}} and {{$bar-foo}} are valid, because variable names can contain a ':' or a '-'" }, { "src": "{|3.14|:foo}", "char": 7, - "comment": "Missing space before annotation.", - "errors": [{"type": "parse-error"}] + "comment": "Missing space before annotation." }, { "src": "{|3.14|-foo}", "char": 7, - "comment": "Missing space before annotation.", - "errors": [{"type": "parse-error"}] + "comment": "Missing space before annotation." }, { "src": "{|3.14|+foo}", "char": 7, - "comment": "Missing space before annotation.", - "errors": [{"type": "parse-error"}] + "comment": "Missing space before annotation." }, { "src": ".local $foo = {$bar} .match {$foo} :one {one} * {other}", "char": 36, - "comment": "Unquoted literals can't begin with a ':'", - "errors": [{"type": "parse-error"}] + "comment": "Unquoted literals can't begin with a ':'" }, { "src": ".local $foo = {$bar :fun option=:a} {{bar {$foo}}}", "char": 32, - "comment": "Unquoted literals can't begin with a ':'", - "errors": [{"type": "parse-error"}] - }, - { "src": "{|foo| {#markup}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] }, - { "src": "{|foo| #markup}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] }, - { "src": "{|foo| {#markup/}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] }, - { "src": "{|foo| {/markup}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] }, - { "src": ".input $x = {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".input $x = {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".input {|1| :number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".input {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".input {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".", "char": 1, "errors": [{"type": "parse-error"}]}, - { "src": "{", "char": 1, "errors": [{"type": "parse-error"}]}, - { "src": "}", "char": 0, "errors": [{"type": "parse-error"}]}, - { "src": "{}", "char": 1, "errors": [{"type": "parse-error"}]}, - { "src": "{{", "char": 2, "errors": [{"type": "parse-error"}]}, - { "src": "{{}", "char": 3, "errors": [{"type": "parse-error"}]}, - { "src": "{{}}}", "char": 4, "errors": [{"type": "parse-error"}]}, - { "src": "{|foo| #markup}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": "{{missing end brace}", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "{{missing end braces", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "{{missing end {$braces", "char": 22, "errors": [{"type": "parse-error"}]}, - { "src": "{{extra}} content", "char": 9, "errors": [{"type": "parse-error"}]}, - { "src": "empty { } placeholder", "char": 8, "errors": [{"type": "parse-error"}]}, - { "src": "missing space {42:func}", "char": 17, "errors": [{"type": "parse-error"}]}, - { "src": "missing space {|foo|:func}", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "missing space {|foo|@bar}", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "missing space {:func@bar}", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "{:func @bar@baz}", "char": 11, "errors": [{"type": "parse-error"}]}, - { "src": "{:func @bar=42@baz}", "char": 14, "errors": [{"type": "parse-error"}]}, - { "src": "{+reserved@bar}", "char": 10, "errors": [{"type": "parse-error"}]}, - { "src": "{&private@bar}", "char": 9, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:} placeholder", "char": 6, "errors": [{"type": "parse-error"}]}, - { "src": "bad {\\u0000placeholder}", "char": 5, "errors": [{"type": "parse-error"}]}, - { "src": "no-equal {|42| :number minimumFractionDigits 2}", "char": 45, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option=}", "char": 25, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option value}", "char": 25, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option:value}", "char": 30, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option}", "char": 24, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder:}", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "bad {::placeholder}", "char": 6, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder::foo}", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option:=x}", "char": 25, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder :option=x}", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option::x=y}", "char": 25, "errors": [{"type": "parse-error"}]}, - { "src": "bad {$placeholder option}", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder @attribute=}", "char": 29, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder @attribute=@foo}", "char": 29, "errors": [{"type": "parse-error"}]}, - { "src": "no {placeholder end", "char": 16, "errors": [{"type": "parse-error"}]}, - { "src": "no {$placeholder end", "char": 17, "errors": [{"type": "parse-error"}]}, - { "src": "no {:placeholder end", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "no {|placeholder| end", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "no {|literal} end", "char": 17, "errors": [{"type": "parse-error"}]}, - { "src": "no {|literal or placeholder end", "char": 31, "errors": [{"type": "parse-error"}]}, - { "src": ".local bar = {|foo|} {{_}}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": ".local #bar = {|foo|} {{_}}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": ".local $bar {|foo|} {{_}}", "char": 12, "errors": [{"type": "parse-error"}]}, - { "src": ".local $bar = |foo| {{_}}", "char": 14, "errors": [{"type": "parse-error"}]}, - { "src": ".match {#foo} * {{foo}}", "char": 8, "errors": [{"type": "parse-error"}]}, - { "src": ".match {} * {{foo}}", "char": 8, "errors": [{"type": "parse-error"}]}, - { "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}", "char": 30, "errors": [{"type": "parse-error"}]}, - { "src": ".match * {{foo}}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": ".match {|x| :x} * foo", "char": 21, "errors": [{"type": "parse-error"}]}, - { "src": ".match {|x| :x} * {{foo}} extra", "char": 31, "errors": [{"type": "parse-error"}]}, - { "src": ".match |x| * {{foo}}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": ".match {|foo| :string} o:ne {{one}} * {{other}}", "char": 24, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]}, - { "src": ".match {|foo| :string} one: {{one}} * {{other}}", "char": 26, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]}, - { "src": ".local $foo = {|42| :number option=a:b} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]}, - { "src": ".local $foo = {|42| :number option=a:b:c} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]}, - { "src": "{$bar:foo}", "char": 5, "comment" : "tests for ':' in unquoted literals (not allowed)", "errors": [{"type": "parse-error"}]}, + "comment": "Unquoted literals can't begin with a ':'" + }, + { "src": "{|foo| {#markup}}", "char": 7, "comment": "Markup in wrong place" }, + { "src": "{|foo| #markup}", "char": 7, "comment": "Markup in wrong place" }, + { "src": "{|foo| {#markup/}}", "char": 7, "comment": "Markup in wrong place" }, + { "src": "{|foo| {/markup}}", "char": 7, "comment": "Markup in wrong place" }, + { "src": ".input $x = {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".input $x = {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".input {|1| :number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".input {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".input {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".", "char": 1}, + { "src": "{", "char": 1}, + { "src": "}", "char": 0}, + { "src": "{}", "char": 1}, + { "src": "{{", "char": 2}, + { "src": "{{}", "char": 3}, + { "src": "{{}}}", "char": 4}, + { "src": "{|foo| #markup}", "char": 7}, + { "src": "{{missing end brace}", "char": 20}, + { "src": "{{missing end braces", "char": 20}, + { "src": "{{missing end {$braces", "char": 22}, + { "src": "{{extra}} content", "char": 10}, + { "src": "empty { } placeholder", "char": 8}, + { "src": "missing space {42:func}", "char": 17}, + { "src": "missing space {|foo|:func}", "char": 20}, + { "src": "missing space {|foo|@bar}", "char": 20}, + { "src": "missing space {:func@bar}", "char": 20}, + { "src": "{:func @bar@baz}", "char": 11}, + { "src": "{:func @bar=42@baz}", "char": 14}, + { "src": "{+reserved@bar}", "char": 10}, + { "src": "{&private@bar}", "char": 9}, + { "src": "bad {:} placeholder", "char": 6}, + { "src": "bad {\\u0000placeholder}", "char": 5}, + { "src": "no-equal {|42| :number minimumFractionDigits 2}", "char": 45}, + { "src": "bad {:placeholder option=}", "char": 25}, + { "src": "bad {:placeholder option value}", "char": 25}, + { "src": "bad {:placeholder option:value}", "char": 30}, + { "src": "bad {:placeholder option}", "char": 24}, + { "src": "bad {:placeholder:}", "char": 18}, + { "src": "bad {::placeholder}", "char": 6}, + { "src": "bad {:placeholder::foo}", "char": 18}, + { "src": "bad {:placeholder option:=x}", "char": 25}, + { "src": "bad {:placeholder :option=x}", "char": 18}, + { "src": "bad {:placeholder option::x=y}", "char": 25}, + { "src": "bad {$placeholder option}", "char": 18}, + { "src": "bad {:placeholder @attribute=}", "char": 29}, + { "src": "bad {:placeholder @attribute=@foo}", "char": 29}, + { "src": "no {placeholder end", "char": 16}, + { "src": "no {$placeholder end", "char": 17}, + { "src": "no {:placeholder end", "char": 20}, + { "src": "no {|placeholder| end", "char": 18}, + { "src": "no {|literal} end", "char": 17}, + { "src": "no {|literal or placeholder end", "char": 31}, + { "src": ".local bar = {|foo|} {{_}}", "char": 7}, + { "src": ".local #bar = {|foo|} {{_}}", "char": 7}, + { "src": ".local $bar {|foo|} {{_}}", "char": 12}, + { "src": ".local $bar = |foo| {{_}}", "char": 14}, + { "src": ".match {#foo} * {{foo}}", "char": 8}, + { "src": ".match {} * {{foo}}", "char": 8}, + { "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}", "char": 30}, + { "src": ".match * {{foo}}", "char": 7}, + { "src": ".match {|x| :x} * foo", "char": 21}, + { "src": ".match {|x| :x} * {{foo}} extra", "char": 31}, + { "src": ".match |x| * {{foo}}", "char": 7}, + { "src": ".match {|foo| :string} o:ne {{one}} * {{other}}", "char": 24, "comment" : "tests for ':' in unquoted literals (not allowed)" }, + { "src": ".match {|foo| :string} one: {{one}} * {{other}}", "char": 26, "comment" : "tests for ':' in unquoted literals (not allowed)" }, + { "src": ".local $foo = {|42| :number option=a:b} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" }, + { "src": ".local $foo = {|42| :number option=a:b:c} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" }, + { "src": "{$bar:foo}", "char": 5, "comment" : "tests for ':' in unquoted literals (not allowed)"}, { "src": ".match {1} {{_}}", "char": 12, - "comment": "Disambiguating a wrong .match from an unsupported statement", - "errors": [{"type": "parse-error"}] + "comment": "Disambiguating a wrong .match from an unsupported statement" } -] + ] +} diff --git a/testdata/message2/syntax-errors-end-of-input.json b/testdata/message2/syntax-errors-end-of-input.json index 69fcc04d0cf3..d97dcb24ac82 100644 --- a/testdata/message2/syntax-errors-end-of-input.json +++ b/testdata/message2/syntax-errors-end-of-input.json @@ -1,4 +1,15 @@ -[ +{ + "scenario": "Syntax errors for unexpected end-of-input", + "description": "Syntax errors; for ICU4C, the character offset is expected to be the last character", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ { "src": ".local ", "char": 9 }, { "src": ".lo", "char": 3 }, { "src": ".local $foo", "char": 11 }, @@ -13,4 +24,6 @@ { "src": "{{missing end {$brace}", "char": 22 }, { "src": "{{missing end {$brace}}", "char": 23 }, { "src": "{{%xyz", "char": 6 } -] + ] +} + diff --git a/testdata/message2/tricky-declarations.json b/testdata/message2/tricky-declarations.json index b340c46e90bf..3fded666e633 100644 --- a/testdata/message2/tricky-declarations.json +++ b/testdata/message2/tricky-declarations.json @@ -1,17 +1,19 @@ -[ +{ + "scenario": "Declarations tests", + "description": "Tests for interesting combinations of .local and .input", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": ".input {$var :number minimumFractionDigits=$var2} .input {$var2 :number minimumFractionDigits=5} {{{$var} {$var2}}}", "exp": "1.000 3.00000", - "params": { "var": 1, "var2": 3 }, - "errors": [{ "type": "duplicate-declaration" }] + "params": [{ "name": "var", "value": 1}, {"name": "var2", "value": 3 }], + "expErrors": [{ "type": "duplicate-declaration" }] }, { "src": ".local $var = {$var2} .local $var2 = {1} {{{$var} {$var2}}}", "exp": "5 1", - "params": { "var2": 5 }, - "errors": [{ "type": "duplicate-declaration" }] - }, - { - "src": ".local $var2 = {1} {{{$var2}}}", - "exp": "1", - "params": { "var2": 5 } + "params": [{ "name": "var2", "value": 5 }], + "expErrors": [{ "type": "duplicate-declaration" }] } -] + ] +} diff --git a/testdata/message2/valid-tests.json b/testdata/message2/valid-tests.json index f037c49d4b48..5f1292e00896 100644 --- a/testdata/message2/valid-tests.json +++ b/testdata/message2/valid-tests.json @@ -1,5 +1,10 @@ -[ - { "src": "hello {|4.2| :number}", "exp": "hello 4.2"}, +{ + "scenario": "Valid tests", + "description": "Additional valid tests", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "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= 2}", "exp": "hello 4.20" }, @@ -63,63 +68,61 @@ { "src": "{42e369}", "exp": "42e369", "ignoreJava": "See ICU-22810"}, { "src": "hello {|3| :number }", "exp": "hello 3" }, - { "src": "{:foo}", "errors": [{ "type": "missing-func" }], + { "src": "{:foo}", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo }", "errors": [{ "type": "missing-func" }], + { "src": "{:foo }", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo }", "errors": [{ "type": "missing-func" }], + { "src": "{:foo }", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo k=v}", "errors": [{ "type": "missing-func" }], + { "src": "{:foo k=v}", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo k=v }", "errors": [{ "type": "missing-func" }], + { "src": "{:foo k=v }", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo k1=v1 k2=v2}", "errors": [{ "type": "missing-func" }], + { "src": "{:foo k1=v1 k2=v2}", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo k1=v1 k2=v2 }", "errors": [{ "type": "missing-func" }], + { "src": "{:foo k1=v1 k2=v2 }", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, { "src": "{|3.14| }", "exp": "3.14" }, { "src": "{|3.14| }", "exp": "3.14" }, { "src": "{|3.14| :number}", "exp": "3.14" }, { "src": "{|3.14| :number }", "exp": "3.14" }, - { "src": "{$bar }", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar }", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{$bar }", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar }", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{$bar :foo}", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar :foo}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{$bar :foo }", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar :foo }", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{$bar-foo}", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar-foo}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, { "src": ".local $foo = {|hello|} .local $foo = {$foo} {{{$foo}}}", - "errors": [{ "type": "duplicate-declaration" }] }, + "expErrors": [{ "type": "duplicate-declaration" }] }, { "src": "good {placeholder}", "exp": "good placeholder" }, { "src": "a\\\\qbc", "exp": "a\\qbc", "comment": "pattern -> escaped-char -> backslash backslash" }, - { - "src": "{$one} and {$two}", - "params": { "one": "1.3", "two": "4.2" }, - "exp": "1.3 and 4.2" + "comment": "message -> simple-message -> simple-start pattern -> escaped-char", + "src": "\\\\", + "exp": "\\" }, { - "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "baz": "foo" }, - "exp": "bar foo" + "comment": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"#\" identifier \"}\"", + "src": "{ #a}", + "exp": "" }, { - "src": ".local $foo = {$foo} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar foo", - "errors": [{ "type": "duplicate-declaration" }] + "comment": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"/\" identifier \"}\"", + "src": "{ /a}", + "exp": "" }, { - "src": ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}", - "params": { "baz": "foo" }, - "exp": "bar {$bar}", - "errors": [{ "type": "duplicate-declaration" }] + "comment": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> input-declaration complex-body -> input variable-expression complex-body", + "src": ".input{$x}{{}}", + "exp": "" } -] + ] +}