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 MessageFormatter mf = mfBuilder.build(errorCode); 232 UnicodeString result; 233 234 if (U_SUCCESS(errorCode)) { 235 result = mf.formatToString(MessageArguments(testCase.getArguments(), errorCode), errorCode); 236 } 237 238 if (testCase.expectSuccess() || (testCase.expectedErrorCode() != U_MF_SYNTAX_ERROR 239 // For now, don't round-trip messages with these errors, 240 // since duplicate options are dropped 241 && testCase.expectedErrorCode() != U_MF_DUPLICATE_OPTION_NAME_ERROR)) { 242 const UnicodeString& in = mf.getNormalizedPattern(); 243 UnicodeString out; 244 if (!roundTrip(in, mf.getDataModel(), out)) { 245 failRoundTrip(tmsg, testCase, in, out); 246 } 247 } 248 249 if (testCase.expectNoSyntaxError()) { 250 if (errorCode == U_MF_SYNTAX_ERROR) { 251 failSyntaxError(tmsg, testCase); 252 } 253 errorCode.reset(); 254 return; 255 } 256 if (testCase.expectSuccess() && U_FAILURE(errorCode)) { 257 failExpectedSuccess(tmsg, testCase, errorCode); 258 return; 259 } 260 if (testCase.expectFailure() && errorCode != testCase.expectedErrorCode()) { 261 failExpectedFailure(tmsg, testCase, errorCode); 262 return; 263 } 264 if (!testCase.lineNumberAndOffsetMatch(parseError.line, parseError.offset)) { 265 failWrongOffset(tmsg, testCase, parseError.line, parseError.offset); 266 } 267 if (!testCase.outputMatches(result)) { 268 failWrongOutput(tmsg, testCase, result); 269 return; 270 } 271 errorCode.reset(); 272 } 273 roundTrip(const UnicodeString & normalizedInput,const MFDataModel & dataModel,UnicodeString & result)274 static bool roundTrip(const UnicodeString& normalizedInput, const MFDataModel& dataModel, UnicodeString& result) { 275 Serializer(dataModel, result).serialize(); 276 return (normalizedInput == result); 277 } 278 failSyntaxError(IntlTest & tmsg,const TestCase & testCase)279 static void failSyntaxError(IntlTest& tmsg, const TestCase& testCase) { 280 tmsg.dataerrln(testCase.getTestName()); 281 tmsg.logln(testCase.getTestName() + " failed test with pattern: " + testCase.getPattern() + " and error code U_MF_SYNTAX_WARNING; expected no syntax error"); 282 } 283 failExpectedSuccess(IntlTest & tmsg,const TestCase & testCase,IcuTestErrorCode & errorCode)284 static void failExpectedSuccess(IntlTest& tmsg, const TestCase& testCase, IcuTestErrorCode& errorCode) { 285 tmsg.dataerrln(testCase.getTestName()); 286 tmsg.logln(testCase.getTestName() + " failed test with pattern: " + testCase.getPattern() + " and error code " + ((int32_t) errorCode)); 287 errorCode.reset(); 288 } failExpectedFailure(IntlTest & tmsg,const TestCase & testCase,IcuTestErrorCode & errorCode)289 static void failExpectedFailure(IntlTest& tmsg, const TestCase& testCase, IcuTestErrorCode& errorCode) { 290 tmsg.dataerrln(testCase.getTestName()); 291 tmsg.logln(testCase.getTestName() + " failed test with wrong error code; pattern: " + testCase.getPattern() + " and error code " + ((int32_t) errorCode) + "(expected error code: " + ((int32_t) testCase.expectedErrorCode()) + " )"); 292 errorCode.reset(); 293 } failWrongOutput(IntlTest & tmsg,const TestCase & testCase,const UnicodeString & result)294 static void failWrongOutput(IntlTest& tmsg, const TestCase& testCase, const UnicodeString& result) { 295 tmsg.dataerrln(testCase.getTestName()); 296 tmsg.logln(testCase.getTestName() + " failed test with wrong output; pattern: " + testCase.getPattern() + " and expected output = " + testCase.expectedOutput() + " and actual output = " + result); 297 } 298 failRoundTrip(IntlTest & tmsg,const TestCase & testCase,const UnicodeString & in,const UnicodeString & output)299 static void failRoundTrip(IntlTest& tmsg, const TestCase& testCase, const UnicodeString& in, const UnicodeString& output) { 300 tmsg.dataerrln(testCase.getTestName()); 301 tmsg.logln(testCase.getTestName() + " failed test with wrong output; normalized input = " + in + " serialized data model = " + output); 302 } 303 failWrongOffset(IntlTest & tmsg,const TestCase & testCase,uint32_t actualLine,uint32_t actualOffset)304 static void failWrongOffset(IntlTest& tmsg, const TestCase& testCase, uint32_t actualLine, uint32_t actualOffset) { 305 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(), 306 actualLine, actualOffset); 307 tmsg.logln(UnicodeString(testCase.getTestName()) + " pattern = " + testCase.getPattern() + " - failed by returning the wrong line number or offset in the parse error"); 308 } 309 }; // class TestUtils 310 311 } // namespace message2 312 U_NAMESPACE_END 313 314 #endif /* #if !UCONFIG_NO_MF2 */ 315 316 #endif /* #if !UCONFIG_NO_FORMATTING */ 317 318 #endif 319