1 // © 2024 and later: Unicode, Inc. and others.
2
3 #include "unicode/utypes.h"
4
5 #if !UCONFIG_NO_FORMATTING
6
7 #if !UCONFIG_NO_MF2
8
9 #include "unicode/calendar.h"
10 #include "messageformat2test.h"
11
12 using namespace icu::message2;
13
14 /*
15 TODO: Tests need to be unified in a single format that
16 both ICU4C and ICU4J can use, rather than being embedded in code.
17
18 Tests are included in their current state to give a sense of
19 how much test coverage has been achieved. Most of the testing is
20 of the parser/serializer; the formatter needs to be tested more
21 thoroughly.
22 */
23
24 void
runIndexedTest(int32_t index,UBool exec,const char * & name,char *)25 TestMessageFormat2::runIndexedTest(int32_t index, UBool exec,
26 const char* &name, char* /*par*/) {
27 TESTCASE_AUTO_BEGIN;
28 TESTCASE_AUTO(testAPICustomFunctions);
29 TESTCASE_AUTO(testCustomFunctions);
30 TESTCASE_AUTO(testAPI);
31 TESTCASE_AUTO(testAPISimple);
32 TESTCASE_AUTO(testDataModelAPI);
33 TESTCASE_AUTO(testFormatterAPI);
34 TESTCASE_AUTO(testHighLoneSurrogate);
35 TESTCASE_AUTO(testLowLoneSurrogate);
36 TESTCASE_AUTO(dataDrivenTests);
37 TESTCASE_AUTO_END;
38 }
39
40 // Needs more tests
testDataModelAPI()41 void TestMessageFormat2::testDataModelAPI() {
42 IcuTestErrorCode errorCode1(*this, "testAPI");
43 UErrorCode errorCode = (UErrorCode) errorCode1;
44
45 using Pattern = data_model::Pattern;
46
47 Pattern::Builder builder(errorCode);
48
49 builder.add("a", errorCode);
50 builder.add("b", errorCode);
51 builder.add("c", errorCode);
52
53 Pattern p = builder.build(errorCode);
54 int32_t i = 0;
55 for (auto iter = p.begin(); iter != p.end(); ++iter) {
56 std::variant<UnicodeString, Expression, Markup> part = *iter;
57 UnicodeString val = *std::get_if<UnicodeString>(&part);
58 if (i == 0) {
59 assertEquals("testDataModelAPI", val, "a");
60 } else if (i == 1) {
61 assertEquals("testDataModelAPI", val, "b");
62 } else if (i == 2) {
63 assertEquals("testDataModelAPI", val, "c");
64 }
65 i++;
66 }
67 assertEquals("testDataModelAPI", i, 3);
68 }
69
70 // Needs more tests
testFormatterAPI()71 void TestMessageFormat2::testFormatterAPI() {
72 IcuTestErrorCode errorCode(*this, "testFormatterAPI");
73 UnicodeString result;
74 UParseError parseError;
75
76 // Check that constructing the formatter fails
77 // if there's a syntax error
78 UnicodeString pattern = "{{}";
79 MessageFormatter::Builder mfBuilder(errorCode);
80 mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_BEST_EFFORT); // This shouldn't matter, since there's a syntax error
81 mfBuilder.setPattern(pattern, parseError, errorCode);
82 MessageFormatter mf = mfBuilder.build(errorCode);
83 errorCode.expectErrorAndReset(U_MF_SYNTAX_ERROR,
84 "testFormatterAPI: expected syntax error, best-effort error handling");
85
86 // Parsing is done when setPattern() is called,
87 // so setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT) or setSuppressErrors must be called
88 // _before_ setPattern() to get the right behavior,
89 // and if either method is called after setting a pattern,
90 // setPattern() has to be called again.
91
92 // Should get the same behavior with strict errors
93 mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT);
94 // Force re-parsing, as above comment
95 mfBuilder.setPattern(pattern, parseError, errorCode);
96 mf = mfBuilder.build(errorCode);
97 errorCode.expectErrorAndReset(U_MF_SYNTAX_ERROR,
98 "testFormatterAPI: expected syntax error, strict error handling");
99
100 // Try the same thing for a pattern with a resolution error
101 pattern = "{{{$x}}}";
102 // Check that a pattern with a resolution error gives fallback output
103 mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_BEST_EFFORT);
104 mfBuilder.setPattern(pattern, parseError, errorCode);
105 mf = mfBuilder.build(errorCode);
106 errorCode.errIfFailureAndReset("testFormatterAPI: expected success from builder, best-effort error handling");
107 result = mf.formatToString(MessageArguments(), errorCode);
108 errorCode.errIfFailureAndReset("testFormatterAPI: expected success from formatter, best-effort error handling");
109 assertEquals("testFormatterAPI: fallback for message with unresolved variable",
110 result, "{$x}");
111
112 // Check that we do get an error with strict errors
113 mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT);
114 mf = mfBuilder.build(errorCode);
115 errorCode.errIfFailureAndReset("testFormatterAPI: builder should succeed with resolution error");
116 result = mf.formatToString(MessageArguments(), errorCode);
117 errorCode.expectErrorAndReset(U_MF_UNRESOLVED_VARIABLE_ERROR,
118 "testFormatterAPI: formatting should fail with resolution error and strict error handling");
119
120 // Finally, check a valid pattern
121 pattern = "hello";
122 mfBuilder.setPattern(pattern, parseError, errorCode);
123 mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_BEST_EFFORT);
124 mf = mfBuilder.build(errorCode);
125 errorCode.errIfFailureAndReset("testFormatterAPI: expected success from builder with valid pattern, best-effort error handling");
126 result = mf.formatToString(MessageArguments(), errorCode);
127 errorCode.errIfFailureAndReset("testFormatterAPI: expected success from formatter with valid pattern, best-effort error handling");
128 assertEquals("testFormatterAPI: wrong output with valid pattern, best-effort error handling",
129 result, "hello");
130
131 // Check that behavior is the same with strict errors
132 mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT);
133 mf = mfBuilder.build(errorCode);
134 errorCode.errIfFailureAndReset("testFormatterAPI: expected success from builder with valid pattern, strict error handling");
135 result = mf.formatToString(MessageArguments(), errorCode);
136 errorCode.errIfFailureAndReset("testFormatterAPI: expected success from formatter with valid pattern, strict error handling");
137 assertEquals("testFormatterAPI: wrong output with valid pattern, strict error handling",
138 result, "hello");
139 }
140
141 // Example for design doc -- version without null and error checks
testAPISimple()142 void TestMessageFormat2::testAPISimple() {
143 IcuTestErrorCode errorCode1(*this, "testAPI");
144 UErrorCode errorCode = (UErrorCode) errorCode1;
145 UParseError parseError;
146 Locale locale = "en_US";
147
148 // Since this is the example used in the
149 // design doc, it elides null checks and error checks.
150 // To be used in the test suite, it should include those checks
151 // Null checks and error checks elided
152 MessageFormatter::Builder builder(errorCode);
153 MessageFormatter mf = builder.setPattern(u"Hello, {$userName}!", parseError, errorCode)
154 .build(errorCode);
155
156 std::map<UnicodeString, message2::Formattable> argsBuilder;
157 argsBuilder["userName"] = message2::Formattable("John");
158 MessageArguments args(argsBuilder, errorCode);
159
160 UnicodeString result;
161 result = mf.formatToString(args, errorCode);
162 assertEquals("testAPI", result, "Hello, John!");
163
164 mf = builder.setPattern("Today is {$today :date style=full}.", parseError, errorCode)
165 .setLocale(locale)
166 .build(errorCode);
167
168 Calendar* cal(Calendar::createInstance(errorCode));
169 // Sunday, October 28, 2136 8:39:12 AM PST
170 cal->set(2136, Calendar::OCTOBER, 28, 8, 39, 12);
171 UDate date = cal->getTime(errorCode);
172
173 argsBuilder.clear();
174 argsBuilder["today"] = message2::Formattable::forDate(date);
175 args = MessageArguments(argsBuilder, errorCode);
176 result = mf.formatToString(args, errorCode);
177 assertEquals("testAPI", "Today is Sunday, October 28, 2136.", result);
178
179 argsBuilder.clear();
180 argsBuilder["photoCount"] = message2::Formattable(static_cast<int64_t>(12));
181 argsBuilder["userGender"] = message2::Formattable("feminine");
182 argsBuilder["userName"] = message2::Formattable("Maria");
183 args = MessageArguments(argsBuilder, errorCode);
184
185 mf = builder.setPattern(".match {$photoCount :number} {$userGender :string}\n\
186 1 masculine {{{$userName} added a new photo to his album.}}\n \
187 1 feminine {{{$userName} added a new photo to her album.}}\n \
188 1 * {{{$userName} added a new photo to their album.}}\n \
189 * masculine {{{$userName} added {$photoCount} photos to his album.}}\n \
190 * feminine {{{$userName} added {$photoCount} photos to her album.}}\n \
191 * * {{{$userName} added {$photoCount} photos to their album.}}", parseError, errorCode)
192 .setLocale(locale)
193 .build(errorCode);
194 result = mf.formatToString(args, errorCode);
195 assertEquals("testAPI", "Maria added 12 photos to her album.", result);
196
197 delete cal;
198 }
199
200 // Design doc example, with more details
testAPI()201 void TestMessageFormat2::testAPI() {
202 IcuTestErrorCode errorCode(*this, "testAPI");
203 TestCase::Builder testBuilder;
204
205 // Pattern: "Hello, {$userName}!"
206 TestCase test(testBuilder.setName("testAPI")
207 .setPattern("Hello, {$userName}!")
208 .setArgument("userName", "John")
209 .setExpected("Hello, John!")
210 .setLocale("en_US")
211 .build());
212 TestUtils::runTestCase(*this, test, errorCode);
213
214 // Pattern: "{Today is {$today ..."
215 LocalPointer<Calendar> cal(Calendar::createInstance(errorCode));
216 // Sunday, October 28, 2136 8:39:12 AM PST
217 cal->set(2136, Calendar::OCTOBER, 28, 8, 39, 12);
218 UDate date = cal->getTime(errorCode);
219
220 test = testBuilder.setName("testAPI")
221 .setPattern("Today is {$today :date style=full}.")
222 .setDateArgument("today", date)
223 .setExpected("Today is Sunday, October 28, 2136.")
224 .setLocale("en_US")
225 .build();
226 TestUtils::runTestCase(*this, test, errorCode);
227
228 // Pattern matching - plural
229 UnicodeString pattern = ".match {$photoCount :string} {$userGender :string}\n\
230 1 masculine {{{$userName} added a new photo to his album.}}\n \
231 1 feminine {{{$userName} added a new photo to her album.}}\n \
232 1 * {{{$userName} added a new photo to their album.}}\n \
233 * masculine {{{$userName} added {$photoCount} photos to his album.}}\n \
234 * feminine {{{$userName} added {$photoCount} photos to her album.}}\n \
235 * * {{{$userName} added {$photoCount} photos to their album.}}";
236
237
238 int64_t photoCount = 12;
239 test = testBuilder.setName("testAPI")
240 .setPattern(pattern)
241 .setArgument("photoCount", photoCount)
242 .setArgument("userGender", "feminine")
243 .setArgument("userName", "Maria")
244 .setExpected("Maria added 12 photos to her album.")
245 .setLocale("en_US")
246 .build();
247 TestUtils::runTestCase(*this, test, errorCode);
248
249 // Built-in functions
250 pattern = ".match {$photoCount :number} {$userGender :string}\n\
251 1 masculine {{{$userName} added a new photo to his album.}}\n \
252 1 feminine {{{$userName} added a new photo to her album.}}\n \
253 1 * {{{$userName} added a new photo to their album.}}\n \
254 * masculine {{{$userName} added {$photoCount} photos to his album.}}\n \
255 * feminine {{{$userName} added {$photoCount} photos to her album.}}\n \
256 * * {{{$userName} added {$photoCount} photos to their album.}}";
257
258 photoCount = 1;
259 test = testBuilder.setName("testAPI")
260 .setPattern(pattern)
261 .setArgument("photoCount", photoCount)
262 .setArgument("userGender", "feminine")
263 .setArgument("userName", "Maria")
264 .setExpected("Maria added a new photo to her album.")
265 .setLocale("en_US")
266 .build();
267 TestUtils::runTestCase(*this, test, errorCode);
268 }
269
270 // Custom functions example from the ICU4C API design doc
271 // Note: error/null checks are omitted
testAPICustomFunctions()272 void TestMessageFormat2::testAPICustomFunctions() {
273 IcuTestErrorCode errorCode1(*this, "testAPICustomFunctions");
274 UErrorCode errorCode = (UErrorCode) errorCode1;
275 UParseError parseError;
276 Locale locale = "en_US";
277
278 // Set up custom function registry
279 MFFunctionRegistry::Builder builder(errorCode);
280 MFFunctionRegistry functionRegistry =
281 builder.adoptFormatter(data_model::FunctionName("person"), new PersonNameFormatterFactory(), errorCode)
282 .build();
283
284 Person* person = new Person(UnicodeString("Mr."), UnicodeString("John"), UnicodeString("Doe"));
285
286 std::map<UnicodeString, message2::Formattable> argsBuilder;
287 argsBuilder["name"] = message2::Formattable(person);
288 MessageArguments arguments(argsBuilder, errorCode);
289
290 MessageFormatter::Builder mfBuilder(errorCode);
291 UnicodeString result;
292 // This fails, because we did not provide a function registry:
293 MessageFormatter mf = mfBuilder.setErrorHandlingBehavior(MessageFormatter::U_MF_STRICT)
294 .setPattern("Hello {$name :person formality=informal}",
295 parseError, errorCode)
296 .setLocale(locale)
297 .build(errorCode);
298 result = mf.formatToString(arguments, errorCode);
299 assertEquals("testAPICustomFunctions", U_MF_UNKNOWN_FUNCTION_ERROR, errorCode);
300
301 errorCode = U_ZERO_ERROR;
302 mfBuilder.setFunctionRegistry(functionRegistry).setLocale(locale);
303
304 mf = mfBuilder.setPattern("Hello {$name :person formality=informal}", parseError, errorCode)
305 .build(errorCode);
306 result = mf.formatToString(arguments, errorCode);
307 assertEquals("testAPICustomFunctions", "Hello John", result);
308
309 mf = mfBuilder.setPattern("Hello {$name :person formality=formal}", parseError, errorCode)
310 .build(errorCode);
311 result = mf.formatToString(arguments, errorCode);
312 assertEquals("testAPICustomFunctions", "Hello Mr. Doe", result);
313
314 mf = mfBuilder.setPattern("Hello {$name :person formality=formal length=long}", parseError, errorCode)
315 .build(errorCode);
316 result = mf.formatToString(arguments, errorCode);
317 assertEquals("testAPICustomFunctions", "Hello Mr. John Doe", result);
318
319 // By type
320 MFFunctionRegistry::Builder builderByType(errorCode);
321 FunctionName personFormatterName("person");
322 MFFunctionRegistry functionRegistryByType =
323 builderByType.adoptFormatter(personFormatterName,
324 new PersonNameFormatterFactory(),
325 errorCode)
326 .setDefaultFormatterNameByType("person",
327 personFormatterName,
328 errorCode)
329 .build();
330 mfBuilder.setFunctionRegistry(functionRegistryByType);
331 mf = mfBuilder.setPattern("Hello {$name}", parseError, errorCode)
332 .setLocale(locale)
333 .build(errorCode);
334 result = mf.formatToString(arguments, errorCode);
335 assertEquals("testAPICustomFunctions", U_ZERO_ERROR, errorCode);
336 // Expect "Hello John" because in the custom function we registered,
337 // "informal" is the default formality and "length" is the default length
338 assertEquals("testAPICustomFunctions", "Hello John", result);
339 delete person;
340 }
341
342 // ICU-22890 lone surrogate cause infinity loop
testHighLoneSurrogate()343 void TestMessageFormat2::testHighLoneSurrogate() {
344 IcuTestErrorCode errorCode(*this, "testHighLoneSurrogate");
345 UParseError pe = { 0, 0, {0}, {0} };
346 // Lone surrogate with only high surrogate
347 UnicodeString loneSurrogate({0xda02, 0});
348 icu::message2::MessageFormatter msgfmt1 =
349 icu::message2::MessageFormatter::Builder(errorCode)
350 .setPattern(loneSurrogate, pe, errorCode)
351 .build(errorCode);
352 UnicodeString result = msgfmt1.formatToString({}, errorCode);
353 errorCode.expectErrorAndReset(U_MF_SYNTAX_ERROR, "testHighLoneSurrogate");
354 }
355
356 // ICU-22890 lone surrogate cause infinity loop
testLowLoneSurrogate()357 void TestMessageFormat2::testLowLoneSurrogate() {
358 IcuTestErrorCode errorCode(*this, "testLowLoneSurrogate");
359 UParseError pe = { 0, 0, {0}, {0} };
360 // Lone surrogate with only low surrogate
361 UnicodeString loneSurrogate({0xdc02, 0});
362 icu::message2::MessageFormatter msgfmt2 =
363 icu::message2::MessageFormatter::Builder(errorCode)
364 .setPattern(loneSurrogate, pe, errorCode)
365 .build(errorCode);
366 UnicodeString result = msgfmt2.formatToString({}, errorCode);
367 errorCode.expectErrorAndReset(U_MF_SYNTAX_ERROR, "testLowLoneSurrogate");
368 }
369
dataDrivenTests()370 void TestMessageFormat2::dataDrivenTests() {
371 IcuTestErrorCode errorCode(*this, "jsonTests");
372
373 jsonTestsFromFiles(errorCode);
374 }
375
~TestCase()376 TestCase::~TestCase() {}
~Builder()377 TestCase::Builder::~Builder() {}
378
379 #endif /* #if !UCONFIG_NO_MF2 */
380
381 #endif /* #if !UCONFIG_NO_FORMATTING */
382
383