1 // © 2024 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 4 #ifndef _TESTMESSAGEFORMAT2_UTILS 5 #define _TESTMESSAGEFORMAT2_UTILS 6 7 #include "unicode/utypes.h" 8 9 #if !UCONFIG_NO_FORMATTING 10 11 #if !UCONFIG_NO_MF2 12 13 #include "unicode/locid.h" 14 #include "unicode/messageformat2_formattable.h" 15 #include "unicode/messageformat2.h" 16 #include "intltest.h" 17 #include "messageformat2_macros.h" 18 #include "messageformat2_serializer.h" 19 20 U_NAMESPACE_BEGIN namespace message2 { 21 22 class TestCase : public UMemory { 23 private: 24 /* const */ UnicodeString testName; 25 /* const */ UnicodeString pattern; 26 /* const */ Locale locale; 27 /* const */ std::map<UnicodeString, Formattable> arguments; 28 /* const */ UErrorCode expectedError; 29 /* const */ bool expectedNoSyntaxError; 30 /* const */ bool hasExpectedOutput; 31 /* const */ UnicodeString expected; 32 /* const */ bool hasLineNumberAndOffset; 33 /* const */ uint32_t lineNumber; 34 /* const */ uint32_t offset; 35 /* const */ bool ignoreError; 36 37 // Function registry is not owned by the TestCase object 38 const MFFunctionRegistry* functionRegistry = nullptr; 39 40 public: getPattern()41 const UnicodeString& getPattern() const { return pattern; } getLocale()42 const Locale& getLocale() const { return locale; } getArguments()43 std::map<UnicodeString, Formattable> getArguments() const { return std::move(arguments); } getTestName()44 const UnicodeString& getTestName() const { return testName; } expectSuccess()45 bool expectSuccess() const { 46 return (!ignoreError && U_SUCCESS(expectedError)); 47 } expectFailure()48 bool expectFailure() const { 49 return (!ignoreError && U_FAILURE(expectedError)); 50 } expectNoSyntaxError()51 bool expectNoSyntaxError() const { 52 return expectedNoSyntaxError; 53 } expectedErrorCode()54 UErrorCode expectedErrorCode() const { 55 U_ASSERT(!expectSuccess()); 56 return expectedError; 57 } lineNumberAndOffsetMatch(uint32_t actualLine,uint32_t actualOffset)58 bool lineNumberAndOffsetMatch(uint32_t actualLine, uint32_t actualOffset) const { 59 return (!hasLineNumberAndOffset || 60 ((actualLine == lineNumber) && actualOffset == offset)); 61 } outputMatches(const UnicodeString & result)62 bool outputMatches(const UnicodeString& result) const { 63 return (!hasExpectedOutput || (expected == result)); 64 } expectedOutput()65 const UnicodeString& expectedOutput() const { 66 U_ASSERT(hasExpectedOutput); 67 return expected; 68 } getLineNumber()69 uint32_t getLineNumber() const { 70 U_ASSERT(hasLineNumberAndOffset); 71 return lineNumber; 72 } getOffset()73 uint32_t getOffset() const { 74 U_ASSERT(hasLineNumberAndOffset); 75 return offset; 76 } hasCustomRegistry()77 bool hasCustomRegistry() const { return functionRegistry != nullptr; } getCustomRegistry()78 const MFFunctionRegistry* getCustomRegistry() const { 79 U_ASSERT(hasCustomRegistry()); 80 return functionRegistry; 81 } 82 TestCase(const TestCase&); 83 TestCase& operator=(TestCase&& other) noexcept = default; 84 virtual ~TestCase(); 85 86 class Builder : public UObject { 87 friend class TestCase; 88 89 public: setName(UnicodeString name)90 Builder& setName(UnicodeString name) { testName = name; return *this; } setPattern(UnicodeString pat)91 Builder& setPattern(UnicodeString pat) { pattern = pat; return *this; } setArgument(const UnicodeString & k,const UnicodeString & val)92 Builder& setArgument(const UnicodeString& k, const UnicodeString& val) { 93 arguments[k] = Formattable(val); 94 return *this; 95 } setArgument(const UnicodeString & k,const Formattable * val,int32_t count)96 Builder& setArgument(const UnicodeString& k, const Formattable* val, int32_t count) { 97 U_ASSERT(val != nullptr); 98 arguments[k] = Formattable(val, count); 99 return *this; 100 } setArgument(const UnicodeString & k,double val)101 Builder& setArgument(const UnicodeString& k, double val) { 102 arguments[k] = Formattable(val); 103 return *this; 104 } setArgument(const UnicodeString & k,int64_t val)105 Builder& setArgument(const UnicodeString& k, int64_t val) { 106 arguments[k] = Formattable(val); 107 return *this; 108 } setDateArgument(const UnicodeString & k,UDate date)109 Builder& setDateArgument(const UnicodeString& k, UDate date) { 110 arguments[k] = Formattable::forDate(date); 111 return *this; 112 } setDecimalArgument(const UnicodeString & k,std::string_view decimal,UErrorCode & errorCode)113 Builder& setDecimalArgument(const UnicodeString& k, std::string_view decimal, UErrorCode& errorCode) { 114 THIS_ON_ERROR(errorCode); 115 arguments[k] = Formattable::forDecimal(decimal, errorCode); 116 return *this; 117 } setArgument(const UnicodeString & k,const FormattableObject * val)118 Builder& setArgument(const UnicodeString& k, const FormattableObject* val) { 119 U_ASSERT(val != nullptr); 120 arguments[k] = Formattable(val); 121 return *this; 122 } clearArguments()123 Builder& clearArguments() { 124 arguments.clear(); 125 return *this; 126 } setExpected(UnicodeString e)127 Builder& setExpected(UnicodeString e) { 128 hasExpectedOutput = true; 129 expected = e; 130 return *this; 131 } clearExpected()132 Builder& clearExpected() { 133 hasExpectedOutput = false; 134 return *this; 135 } setExpectedError(UErrorCode errorCode)136 Builder& setExpectedError(UErrorCode errorCode) { 137 expectedError = U_SUCCESS(errorCode) ? U_ZERO_ERROR : errorCode; 138 return *this; 139 } setNoSyntaxError()140 Builder& setNoSyntaxError() { 141 expectNoSyntaxError = true; 142 return *this; 143 } setExpectSuccess()144 Builder& setExpectSuccess() { 145 return setExpectedError(U_ZERO_ERROR); 146 } setLocale(Locale && loc)147 Builder& setLocale(Locale&& loc) { 148 locale = loc; 149 return *this; 150 } setExpectedLineNumberAndOffset(uint32_t line,uint32_t o)151 Builder& setExpectedLineNumberAndOffset(uint32_t line, uint32_t o) { 152 hasLineNumberAndOffset = true; 153 lineNumber = line; 154 offset = o; 155 return *this; 156 } setIgnoreError()157 Builder& setIgnoreError() { 158 ignoreError = true; 159 return *this; 160 } clearIgnoreError()161 Builder& clearIgnoreError() { 162 ignoreError = false; 163 return *this; 164 } setFunctionRegistry(const MFFunctionRegistry * reg)165 Builder& setFunctionRegistry(const MFFunctionRegistry* reg) { 166 U_ASSERT(reg != nullptr); 167 functionRegistry = reg; 168 return *this; 169 } build()170 TestCase build() const { 171 return TestCase(*this); 172 } 173 virtual ~Builder(); 174 175 private: 176 UnicodeString testName; 177 UnicodeString pattern; 178 Locale locale; 179 std::map<UnicodeString, Formattable> arguments; 180 bool hasExpectedOutput; 181 UnicodeString expected; 182 UErrorCode expectedError; 183 bool expectNoSyntaxError; 184 bool hasLineNumberAndOffset; 185 uint32_t lineNumber; 186 uint32_t offset; 187 bool ignoreError; 188 const MFFunctionRegistry* functionRegistry = nullptr; // Not owned 189 190 public: Builder()191 Builder() : pattern(""), locale(Locale::getDefault()), hasExpectedOutput(false), expected(""), expectedError(U_ZERO_ERROR), expectNoSyntaxError(false), hasLineNumberAndOffset(false), ignoreError(false) {} 192 }; 193 194 private: TestCase(const Builder & builder)195 TestCase(const Builder& builder) : 196 testName(builder.testName), 197 pattern(builder.pattern), 198 locale(builder.locale), 199 arguments(builder.arguments), 200 expectedError(builder.expectedError), 201 expectedNoSyntaxError(builder.expectNoSyntaxError), 202 hasExpectedOutput(builder.hasExpectedOutput), 203 expected(builder.expected), 204 hasLineNumberAndOffset(builder.hasLineNumberAndOffset), 205 lineNumber(builder.hasLineNumberAndOffset ? builder.lineNumber : 0), 206 offset(builder.hasLineNumberAndOffset ? builder.offset : 0), 207 ignoreError(builder.ignoreError), 208 functionRegistry(builder.functionRegistry) { 209 // If an error is not expected, then the expected 210 // output should be present 211 U_ASSERT(expectFailure() || expectNoSyntaxError() || hasExpectedOutput); 212 } 213 }; // class TestCase 214 215 class TestUtils { 216 public: 217 218 // Runs a single test case runTestCase(IntlTest & tmsg,const TestCase & testCase,IcuTestErrorCode & errorCode)219 static void runTestCase(IntlTest& tmsg, 220 const TestCase& testCase, 221 IcuTestErrorCode& errorCode) { 222 CHECK_ERROR(errorCode); 223 224 UParseError parseError; 225 MessageFormatter::Builder mfBuilder(errorCode); 226 mfBuilder.setPattern(testCase.getPattern(), parseError, errorCode).setLocale(testCase.getLocale()); 227 228 if (testCase.hasCustomRegistry()) { 229 mfBuilder.setFunctionRegistry(*testCase.getCustomRegistry()); 230 } 231 // Initially, set error behavior to strict. 232 // We'll re-run to check for errors. 233 mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT); 234 MessageFormatter mf = mfBuilder.build(errorCode); 235 UnicodeString result; 236 237 // Builder should fail if a syntax error was expected 238 if (!testCase.expectSuccess() && testCase.expectedErrorCode() == U_MF_SYNTAX_ERROR) { 239 if (errorCode != testCase.expectedErrorCode()) { 240 failExpectedFailure(tmsg, testCase, errorCode); 241 } 242 errorCode.reset(); 243 return; 244 } 245 246 if (U_SUCCESS(errorCode)) { 247 result = mf.formatToString(MessageArguments(testCase.getArguments(), errorCode), errorCode); 248 } 249 250 const UnicodeString& in = mf.getNormalizedPattern(); 251 UnicodeString out; 252 if (!roundTrip(in, mf.getDataModel(), out) 253 // For now, don't round-trip messages with these errors, 254 // since duplicate options are dropped 255 && testCase.expectedErrorCode() != U_MF_DUPLICATE_OPTION_NAME_ERROR) { 256 failRoundTrip(tmsg, testCase, in, out); 257 } 258 259 if (testCase.expectNoSyntaxError()) { 260 if (errorCode == U_MF_SYNTAX_ERROR) { 261 failSyntaxError(tmsg, testCase); 262 } 263 errorCode.reset(); 264 return; 265 } 266 if (testCase.expectSuccess() && U_FAILURE(errorCode)) { 267 failExpectedSuccess(tmsg, testCase, errorCode, parseError.line, parseError.offset); 268 return; 269 } 270 if (testCase.expectFailure() && errorCode != testCase.expectedErrorCode()) { 271 failExpectedFailure(tmsg, testCase, errorCode); 272 return; 273 } 274 if (!testCase.lineNumberAndOffsetMatch(parseError.line, parseError.offset)) { 275 failWrongOffset(tmsg, testCase, parseError.line, parseError.offset); 276 } 277 if (U_FAILURE(errorCode) && !testCase.expectSuccess() 278 && testCase.expectedErrorCode() != U_MF_SYNTAX_ERROR) { 279 // Re-run the formatter if there was an error, 280 // in order to get best-effort output 281 errorCode.reset(); 282 mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_BEST_EFFORT); 283 mf = mfBuilder.build(errorCode); 284 if (U_SUCCESS(errorCode)) { 285 result = mf.formatToString(MessageArguments(testCase.getArguments(), errorCode), errorCode); 286 } 287 if (U_FAILURE(errorCode)) { 288 // Must be a non-MF2 error code 289 U_ASSERT(!(errorCode >= U_MF_UNRESOLVED_VARIABLE_ERROR 290 && errorCode <= U_FMT_PARSE_ERROR_LIMIT)); 291 } 292 // Re-run the formatter 293 result = mf.formatToString(MessageArguments(testCase.getArguments(), errorCode), errorCode); 294 if (!testCase.outputMatches(result)) { 295 failWrongOutput(tmsg, testCase, result); 296 return; 297 } 298 } 299 errorCode.reset(); 300 } 301 roundTrip(const UnicodeString & normalizedInput,const MFDataModel & dataModel,UnicodeString & result)302 static bool roundTrip(const UnicodeString& normalizedInput, const MFDataModel& dataModel, UnicodeString& result) { 303 Serializer(dataModel, result).serialize(); 304 return (normalizedInput == result); 305 } 306 failSyntaxError(IntlTest & tmsg,const TestCase & testCase)307 static void failSyntaxError(IntlTest& tmsg, const TestCase& testCase) { 308 tmsg.dataerrln(testCase.getTestName()); 309 tmsg.logln(testCase.getTestName() + " failed test with pattern: " + testCase.getPattern() + " and error code U_MF_SYNTAX_ERROR; expected no syntax error"); 310 } 311 failExpectedSuccess(IntlTest & tmsg,const TestCase & testCase,IcuTestErrorCode & errorCode,int32_t line,int32_t offset)312 static void failExpectedSuccess(IntlTest& tmsg, const TestCase& testCase, IcuTestErrorCode& errorCode, int32_t line, int32_t offset) { 313 tmsg.dataerrln(testCase.getTestName()); 314 tmsg.logln(testCase.getTestName() + " failed test with pattern: " + testCase.getPattern() + " and error code " + UnicodeString(u_errorName(errorCode))); 315 tmsg.dataerrln("line = %d offset = %d", line, offset); 316 errorCode.reset(); 317 } failExpectedFailure(IntlTest & tmsg,const TestCase & testCase,IcuTestErrorCode & errorCode)318 static void failExpectedFailure(IntlTest& tmsg, const TestCase& testCase, IcuTestErrorCode& errorCode) { 319 tmsg.dataerrln(testCase.getTestName()); 320 tmsg.errln(testCase.getTestName() + " failed test with wrong error code; pattern: " + testCase.getPattern() + " and error code " + UnicodeString(u_errorName(errorCode)) + " and expected error code: " + UnicodeString(u_errorName(testCase.expectedErrorCode()))); 321 errorCode.reset(); 322 } failWrongOutput(IntlTest & tmsg,const TestCase & testCase,const UnicodeString & result)323 static void failWrongOutput(IntlTest& tmsg, const TestCase& testCase, const UnicodeString& result) { 324 tmsg.dataerrln(testCase.getTestName()); 325 tmsg.logln(testCase.getTestName() + " failed test with wrong output; pattern: " + testCase.getPattern() + " and expected output = " + testCase.expectedOutput() + " and actual output = " + result); 326 } 327 failRoundTrip(IntlTest & tmsg,const TestCase & testCase,const UnicodeString & in,const UnicodeString & output)328 static void failRoundTrip(IntlTest& tmsg, const TestCase& testCase, const UnicodeString& in, const UnicodeString& output) { 329 tmsg.dataerrln(testCase.getTestName()); 330 tmsg.logln(testCase.getTestName() + " failed test with wrong output; normalized input = " + in + " serialized data model = " + output); 331 } 332 failWrongOffset(IntlTest & tmsg,const TestCase & testCase,uint32_t actualLine,uint32_t actualOffset)333 static void failWrongOffset(IntlTest& tmsg, const TestCase& testCase, uint32_t actualLine, uint32_t actualOffset) { 334 tmsg.dataerrln("Test failed with wrong line or character offset in parse error; expected (line %d, offset %d), got (line %d, offset %d)", testCase.getLineNumber(), testCase.getOffset(), 335 actualLine, actualOffset); 336 tmsg.logln(UnicodeString(testCase.getTestName()) + " pattern = " + testCase.getPattern() + " - failed by returning the wrong line number or offset in the parse error"); 337 } 338 }; // class TestUtils 339 340 } // namespace message2 341 U_NAMESPACE_END 342 343 #endif /* #if !UCONFIG_NO_MF2 */ 344 345 #endif /* #if !UCONFIG_NO_FORMATTING */ 346 347 #endif 348