• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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