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