1// © 2019 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html
3
4// localematchertest.cpp
5// created: 2019jul04 Markus W. Scherer
6
7#include <string>
8#include <vector>
9#include <utility>
10
11#include "unicode/utypes.h"
12#include "unicode/localematcher.h"
13#include "unicode/locid.h"
14#include "charstr.h"
15#include "cmemory.h"
16#include "intltest.h"
17#include "localeprioritylist.h"
18#include "ucbuf.h"
19
20#define ARRAY_RANGE(array) (array), ((array) + UPRV_LENGTHOF(array))
21
22namespace {
23
24const char *locString(const Locale *loc) {
25    return loc != nullptr ? loc->getName() : "(null)";
26}
27
28struct TestCase {
29    int32_t lineNr = 0;
30
31    CharString supported;
32    CharString def;
33    UnicodeString favor;
34    UnicodeString threshold;
35    CharString desired;
36    CharString expMatch;
37    CharString expDesired;
38    CharString expCombined;
39
40    void reset() {
41        supported.clear();
42        def.clear();
43        favor.remove();
44        threshold.remove();
45    }
46};
47
48}  // namespace
49
50class LocaleMatcherTest : public IntlTest {
51public:
52    LocaleMatcherTest() {}
53
54    void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par=NULL) override;
55
56    void testEmpty();
57    void testCopyErrorTo();
58    void testBasics();
59    void testSupportedDefault();
60    void testUnsupportedDefault();
61    void testNoDefault();
62    void testDemotion();
63    void testDirection();
64    void testMaxDistanceAndIsMatch();
65    void testMatch();
66    void testResolvedLocale();
67    void testDataDriven();
68
69private:
70    UBool dataDriven(const TestCase &test, IcuTestErrorCode &errorCode);
71};
72
73extern IntlTest *createLocaleMatcherTest() {
74    return new LocaleMatcherTest();
75}
76
77void LocaleMatcherTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) {
78    if(exec) {
79        logln("TestSuite LocaleMatcherTest: ");
80    }
81    TESTCASE_AUTO_BEGIN;
82    TESTCASE_AUTO(testEmpty);
83    TESTCASE_AUTO(testCopyErrorTo);
84    TESTCASE_AUTO(testBasics);
85    TESTCASE_AUTO(testSupportedDefault);
86    TESTCASE_AUTO(testUnsupportedDefault);
87    TESTCASE_AUTO(testNoDefault);
88    TESTCASE_AUTO(testDemotion);
89    TESTCASE_AUTO(testDirection);
90    TESTCASE_AUTO(testMaxDistanceAndIsMatch);
91    TESTCASE_AUTO(testMatch);
92    TESTCASE_AUTO(testResolvedLocale);
93    TESTCASE_AUTO(testDataDriven);
94    TESTCASE_AUTO_END;
95}
96
97void LocaleMatcherTest::testEmpty() {
98    IcuTestErrorCode errorCode(*this, "testEmpty");
99    LocaleMatcher matcher = LocaleMatcher::Builder().build(errorCode);
100    const Locale *best = matcher.getBestMatch(Locale::getFrench(), errorCode);
101    assertEquals("getBestMatch(fr)", "(null)", locString(best));
102    LocaleMatcher::Result result = matcher.getBestMatchResult("fr", errorCode);
103    assertEquals("getBestMatchResult(fr).des", "(null)", locString(result.getDesiredLocale()));
104    assertEquals("getBestMatchResult(fr).desIndex", -1, result.getDesiredIndex());
105    assertEquals("getBestMatchResult(fr).supp",
106                 "(null)", locString(result.getSupportedLocale()));
107    assertEquals("getBestMatchResult(fr).suppIndex",
108                 -1, result.getSupportedIndex());
109}
110
111void LocaleMatcherTest::testCopyErrorTo() {
112    IcuTestErrorCode errorCode(*this, "testCopyErrorTo");
113    // The builder does not set any errors except out-of-memory.
114    // Test what we can.
115    LocaleMatcher::Builder builder;
116    UErrorCode success = U_ZERO_ERROR;
117    assertFalse("no error", builder.copyErrorTo(success));
118    assertTrue("still success", U_SUCCESS(success));
119    UErrorCode failure = U_INVALID_FORMAT_ERROR;
120    assertTrue("failure passed in", builder.copyErrorTo(failure));
121    assertEquals("same failure", U_INVALID_FORMAT_ERROR, failure);
122}
123
124void LocaleMatcherTest::testBasics() {
125    IcuTestErrorCode errorCode(*this, "testBasics");
126    Locale locales[] = { "fr", "en_GB", "en" };
127    {
128        LocaleMatcher matcher = LocaleMatcher::Builder().
129            setSupportedLocales(ARRAY_RANGE(locales)).build(errorCode);
130        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
131        assertEquals("fromRange.getBestMatch(en_GB)", "en_GB", locString(best));
132        best = matcher.getBestMatch("en_US", errorCode);
133        assertEquals("fromRange.getBestMatch(en_US)", "en", locString(best));
134        best = matcher.getBestMatch("fr_FR", errorCode);
135        assertEquals("fromRange.getBestMatch(fr_FR)", "fr", locString(best));
136        best = matcher.getBestMatch("ja_JP", errorCode);
137        assertEquals("fromRange.getBestMatch(ja_JP)", "fr", locString(best));
138    }
139    // Code coverage: Variations of setting supported locales.
140    {
141        std::vector<Locale> locales{ "fr", "en_GB", "en" };
142        LocaleMatcher matcher = LocaleMatcher::Builder().
143            setSupportedLocales(locales.begin(), locales.end()).build(errorCode);
144        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
145        assertEquals("fromRange.getBestMatch(en_GB)", "en_GB", locString(best));
146        best = matcher.getBestMatch("en_US", errorCode);
147        assertEquals("fromRange.getBestMatch(en_US)", "en", locString(best));
148        best = matcher.getBestMatch("fr_FR", errorCode);
149        assertEquals("fromRange.getBestMatch(fr_FR)", "fr", locString(best));
150        best = matcher.getBestMatch("ja_JP", errorCode);
151        assertEquals("fromRange.getBestMatch(ja_JP)", "fr", locString(best));
152    }
153    {
154        Locale::RangeIterator<Locale *> iter(ARRAY_RANGE(locales));
155        LocaleMatcher matcher = LocaleMatcher::Builder().
156            setSupportedLocales(iter).build(errorCode);
157        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
158        assertEquals("fromIter.getBestMatch(en_GB)", "en_GB", locString(best));
159        best = matcher.getBestMatch("en_US", errorCode);
160        assertEquals("fromIter.getBestMatch(en_US)", "en", locString(best));
161        best = matcher.getBestMatch("fr_FR", errorCode);
162        assertEquals("fromIter.getBestMatch(fr_FR)", "fr", locString(best));
163        best = matcher.getBestMatch("ja_JP", errorCode);
164        assertEquals("fromIter.getBestMatch(ja_JP)", "fr", locString(best));
165    }
166    {
167        Locale *pointers[] = { locales, locales + 1, locales + 2 };
168        // Lambda with explicit reference return type to prevent copy-constructing a temporary
169        // which would be destructed right away.
170        LocaleMatcher matcher = LocaleMatcher::Builder().
171            setSupportedLocalesViaConverter(
172                ARRAY_RANGE(pointers), [](const Locale *p) -> const Locale & { return *p; }).
173            build(errorCode);
174        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
175        assertEquals("viaConverter.getBestMatch(en_GB)", "en_GB", locString(best));
176        best = matcher.getBestMatch("en_US", errorCode);
177        assertEquals("viaConverter.getBestMatch(en_US)", "en", locString(best));
178        best = matcher.getBestMatch("fr_FR", errorCode);
179        assertEquals("viaConverter.getBestMatch(fr_FR)", "fr", locString(best));
180        best = matcher.getBestMatch("ja_JP", errorCode);
181        assertEquals("viaConverter.getBestMatch(ja_JP)", "fr", locString(best));
182    }
183    {
184        LocaleMatcher matcher = LocaleMatcher::Builder().
185            addSupportedLocale(locales[0]).
186            addSupportedLocale(locales[1]).
187            addSupportedLocale(locales[2]).
188            build(errorCode);
189        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
190        assertEquals("added.getBestMatch(en_GB)", "en_GB", locString(best));
191        best = matcher.getBestMatch("en_US", errorCode);
192        assertEquals("added.getBestMatch(en_US)", "en", locString(best));
193        best = matcher.getBestMatch("fr_FR", errorCode);
194        assertEquals("added.getBestMatch(fr_FR)", "fr", locString(best));
195        best = matcher.getBestMatch("ja_JP", errorCode);
196        assertEquals("added.getBestMatch(ja_JP)", "fr", locString(best));
197    }
198    {
199        LocaleMatcher matcher = LocaleMatcher::Builder().
200            setSupportedLocalesFromListString(
201                " el, fr;q=0.555555, en-GB ; q = 0.88  , el; q =0, en;q=0.88 , fr ").
202            build(errorCode);
203        const Locale *best = matcher.getBestMatchForListString("el, fr, fr;q=0, en-GB", errorCode);
204        assertEquals("fromList.getBestMatch(en_GB)", "en_GB", locString(best));
205        best = matcher.getBestMatch("en_US", errorCode);
206        assertEquals("fromList.getBestMatch(en_US)", "en", locString(best));
207        best = matcher.getBestMatch("fr_FR", errorCode);
208        assertEquals("fromList.getBestMatch(fr_FR)", "fr", locString(best));
209        best = matcher.getBestMatch("ja_JP", errorCode);
210        assertEquals("fromList.getBestMatch(ja_JP)", "fr", locString(best));
211    }
212    // more API coverage
213    {
214        LocalePriorityList list("fr, en-GB", errorCode);
215        LocalePriorityList::Iterator iter(list.iterator());
216        LocaleMatcher matcher = LocaleMatcher::Builder().
217            setSupportedLocales(iter).
218            addSupportedLocale(Locale::getEnglish()).
219            setDefaultLocale(&Locale::getGerman()).
220            build(errorCode);
221        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
222        assertEquals("withDefault.getBestMatch(en_GB)", "en_GB", locString(best));
223        best = matcher.getBestMatch("en_US", errorCode);
224        assertEquals("withDefault.getBestMatch(en_US)", "en", locString(best));
225        best = matcher.getBestMatch("fr_FR", errorCode);
226        assertEquals("withDefault.getBestMatch(fr_FR)", "fr", locString(best));
227        best = matcher.getBestMatch("ja_JP", errorCode);
228        assertEquals("withDefault.getBestMatch(ja_JP)", "de", locString(best));
229
230        Locale desired("en_GB");  // distinct object from Locale.UK
231        LocaleMatcher::Result result = matcher.getBestMatchResult(desired, errorCode);
232        assertTrue("withDefault: exactly desired en-GB object",
233                   &desired == result.getDesiredLocale());
234        assertEquals("withDefault: en-GB desired index", 0, result.getDesiredIndex());
235        assertEquals("withDefault: en-GB supported",
236                     "en_GB", locString(result.getSupportedLocale()));
237        assertEquals("withDefault: en-GB supported index", 1, result.getSupportedIndex());
238
239        LocalePriorityList list2("ja-JP, en-US", errorCode);
240        LocalePriorityList::Iterator iter2(list2.iterator());
241        result = matcher.getBestMatchResult(iter2, errorCode);
242        assertEquals("withDefault: ja-JP, en-US desired index", 1, result.getDesiredIndex());
243        assertEquals("withDefault: ja-JP, en-US desired",
244                     "en_US", locString(result.getDesiredLocale()));
245
246        desired = Locale("en", "US");  // distinct object from Locale.US
247        result = matcher.getBestMatchResult(desired, errorCode);
248        assertTrue("withDefault: exactly desired en-US object",
249                   &desired == result.getDesiredLocale());
250        assertEquals("withDefault: en-US desired index", 0, result.getDesiredIndex());
251        assertEquals("withDefault: en-US supported", "en", locString(result.getSupportedLocale()));
252        assertEquals("withDefault: en-US supported index", 2, result.getSupportedIndex());
253
254        result = matcher.getBestMatchResult("ja_JP", errorCode);
255        assertEquals("withDefault: ja-JP desired", "(null)", locString(result.getDesiredLocale()));
256        assertEquals("withDefault: ja-JP desired index", -1, result.getDesiredIndex());
257        assertEquals("withDefault: ja-JP supported", "de", locString(result.getSupportedLocale()));
258        assertEquals("withDefault: ja-JP supported index", -1, result.getSupportedIndex());
259    }
260}
261
262void LocaleMatcherTest::testSupportedDefault() {
263    // The default locale is one of the supported locales.
264    IcuTestErrorCode errorCode(*this, "testSupportedDefault");
265    Locale locales[] = { "fr", "en_GB", "en" };
266    LocaleMatcher matcher = LocaleMatcher::Builder().
267        setSupportedLocales(ARRAY_RANGE(locales)).
268        setDefaultLocale(&locales[1]).
269        build(errorCode);
270    const Locale *best = matcher.getBestMatch("en_GB", errorCode);
271    assertEquals("getBestMatch(en_GB)", "en_GB", locString(best));
272    best = matcher.getBestMatch("en_US", errorCode);
273    assertEquals("getBestMatch(en_US)", "en", locString(best));
274    best = matcher.getBestMatch("fr_FR", errorCode);
275    assertEquals("getBestMatch(fr_FR)", "fr", locString(best));
276    best = matcher.getBestMatch("ja_JP", errorCode);
277    assertEquals("getBestMatch(ja_JP)", "en_GB", locString(best));
278    LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode);
279    assertEquals("getBestMatchResult(ja_JP).supp",
280                 "en_GB", locString(result.getSupportedLocale()));
281    assertEquals("getBestMatchResult(ja_JP).suppIndex",
282                 -1, result.getSupportedIndex());
283}
284
285void LocaleMatcherTest::testUnsupportedDefault() {
286    // The default locale does not match any of the supported locales.
287    IcuTestErrorCode errorCode(*this, "testUnsupportedDefault");
288    Locale locales[] = { "fr", "en_GB", "en" };
289    Locale def("de");
290    LocaleMatcher matcher = LocaleMatcher::Builder().
291        setSupportedLocales(ARRAY_RANGE(locales)).
292        setDefaultLocale(&def).
293        build(errorCode);
294    const Locale *best = matcher.getBestMatch("en_GB", errorCode);
295    assertEquals("getBestMatch(en_GB)", "en_GB", locString(best));
296    best = matcher.getBestMatch("en_US", errorCode);
297    assertEquals("getBestMatch(en_US)", "en", locString(best));
298    best = matcher.getBestMatch("fr_FR", errorCode);
299    assertEquals("getBestMatch(fr_FR)", "fr", locString(best));
300    best = matcher.getBestMatch("ja_JP", errorCode);
301    assertEquals("getBestMatch(ja_JP)", "de", locString(best));
302    LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode);
303    assertEquals("getBestMatchResult(ja_JP).supp",
304                 "de", locString(result.getSupportedLocale()));
305    assertEquals("getBestMatchResult(ja_JP).suppIndex",
306                 -1, result.getSupportedIndex());
307}
308
309void LocaleMatcherTest::testNoDefault() {
310    // We want nullptr instead of any default locale.
311    IcuTestErrorCode errorCode(*this, "testNoDefault");
312    Locale locales[] = { "fr", "en_GB", "en" };
313    LocaleMatcher matcher = LocaleMatcher::Builder().
314        setSupportedLocales(ARRAY_RANGE(locales)).
315        setNoDefaultLocale().
316        build(errorCode);
317    const Locale *best = matcher.getBestMatch("en_GB", errorCode);
318    assertEquals("getBestMatch(en_GB)", "en_GB", locString(best));
319    best = matcher.getBestMatch("en_US", errorCode);
320    assertEquals("getBestMatch(en_US)", "en", locString(best));
321    best = matcher.getBestMatch("fr_FR", errorCode);
322    assertEquals("getBestMatch(fr_FR)", "fr", locString(best));
323    best = matcher.getBestMatch("ja_JP", errorCode);
324    assertEquals("getBestMatch(ja_JP)", "(null)", locString(best));
325    LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode);
326    assertEquals("getBestMatchResult(ja_JP).supp",
327                 "(null)", locString(result.getSupportedLocale()));
328    assertEquals("getBestMatchResult(ja_JP).suppIndex",
329                 -1, result.getSupportedIndex());
330}
331
332void LocaleMatcherTest::testDemotion() {
333    IcuTestErrorCode errorCode(*this, "testDemotion");
334    Locale supported[] = { "fr", "de-CH", "it" };
335    Locale desired[] = { "fr-CH", "de-CH", "it" };
336    {
337        LocaleMatcher noDemotion = LocaleMatcher::Builder().
338            setSupportedLocales(ARRAY_RANGE(supported)).
339            setDemotionPerDesiredLocale(ULOCMATCH_DEMOTION_NONE).build(errorCode);
340        Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired));
341        assertEquals("no demotion",
342                     "de_CH", locString(noDemotion.getBestMatch(desiredIter, errorCode)));
343    }
344
345    {
346        LocaleMatcher regionDemotion = LocaleMatcher::Builder().
347            setSupportedLocales(ARRAY_RANGE(supported)).
348            setDemotionPerDesiredLocale(ULOCMATCH_DEMOTION_REGION).build(errorCode);
349        Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired));
350        assertEquals("region demotion",
351                     "fr", locString(regionDemotion.getBestMatch(desiredIter, errorCode)));
352    }
353}
354
355void LocaleMatcherTest::testDirection() {
356    IcuTestErrorCode errorCode(*this, "testDirection");
357    Locale supported[] = { "ar", "nn" };
358    Locale desired[] = { "arz-EG", "nb-DK" };
359    LocaleMatcher::Builder builder;
360    builder.setSupportedLocales(ARRAY_RANGE(supported));
361    {
362        // arz is a close one-way match to ar, and the region matches.
363        // (Egyptian Arabic vs. Arabic)
364        // Also explicitly exercise the move copy constructor.
365        LocaleMatcher built = builder.build(errorCode);
366        LocaleMatcher withOneWay(std::move(built));
367        Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired));
368        assertEquals("with one-way", "ar",
369                     locString(withOneWay.getBestMatch(desiredIter, errorCode)));
370    }
371    {
372        // nb is a less close two-way match to nn, and the regions differ.
373        // (Norwegian Bokmal vs. Nynorsk)
374        // Also explicitly exercise the move assignment operator.
375        LocaleMatcher onlyTwoWay = builder.build(errorCode);
376        LocaleMatcher built =
377            builder.setDirection(ULOCMATCH_DIRECTION_ONLY_TWO_WAY).build(errorCode);
378        onlyTwoWay = std::move(built);
379        Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired));
380        assertEquals("only two-way", "nn",
381                     locString(onlyTwoWay.getBestMatch(desiredIter, errorCode)));
382    }
383}
384
385void LocaleMatcherTest::testMaxDistanceAndIsMatch() {
386    IcuTestErrorCode errorCode(*this, "testMaxDistanceAndIsMatch");
387    LocaleMatcher::Builder builder;
388    LocaleMatcher standard = builder.build(errorCode);
389    Locale germanLux("de-LU");
390    Locale germanPhoenician("de-Phnx-AT");
391    Locale greek("el");
392    assertTrue("standard de-LU / de", standard.isMatch(germanLux, Locale::getGerman(), errorCode));
393    assertFalse("standard de-Phnx-AT / de",
394                standard.isMatch(germanPhoenician, Locale::getGerman(), errorCode));
395
396    // Allow a script difference to still match.
397    LocaleMatcher loose =
398        builder.setMaxDistance(germanPhoenician, Locale::getGerman()).build(errorCode);
399    assertTrue("loose de-LU / de", loose.isMatch(germanLux, Locale::getGerman(), errorCode));
400    assertTrue("loose de-Phnx-AT / de",
401               loose.isMatch(germanPhoenician, Locale::getGerman(), errorCode));
402    assertFalse("loose el / de", loose.isMatch(greek, Locale::getGerman(), errorCode));
403
404    // Allow at most a regional difference.
405    LocaleMatcher regional =
406        builder.setMaxDistance(Locale("de-AT"), Locale::getGerman()).build(errorCode);
407    assertTrue("regional de-LU / de",
408               regional.isMatch(Locale("de-LU"), Locale::getGerman(), errorCode));
409    assertFalse("regional da / no", regional.isMatch(Locale("da"), Locale("no"), errorCode));
410    assertFalse("regional zh-Hant / zh",
411                regional.isMatch(Locale::getChinese(), Locale::getTraditionalChinese(), errorCode));
412}
413
414
415void LocaleMatcherTest::testMatch() {
416    IcuTestErrorCode errorCode(*this, "testMatch");
417    LocaleMatcher matcher = LocaleMatcher::Builder().build(errorCode);
418
419    // Java test function testMatch_exact()
420    Locale en_CA("en_CA");
421    assertEquals("exact match", 1.0, matcher.internalMatch(en_CA, en_CA, errorCode));
422
423    // testMatch_none
424    Locale ar_MK("ar_MK");
425    double match = matcher.internalMatch(ar_MK, en_CA, errorCode);
426    assertTrue("mismatch: 0<=match<0.2", 0 <= match && match < 0.2);
427
428    // testMatch_matchOnMaximized
429    Locale und_TW("und_TW");
430    Locale zh("zh");
431    Locale zh_Hant("zh_Hant");
432    double matchZh = matcher.internalMatch(und_TW, zh, errorCode);
433    double matchZhHant = matcher.internalMatch(und_TW, zh_Hant, errorCode);
434    assertTrue("und_TW should be closer to zh_Hant than to zh",
435               matchZh < matchZhHant);
436    Locale en_Hant_TW("en_Hant_TW");
437    double matchEnHantTw = matcher.internalMatch(en_Hant_TW, zh_Hant, errorCode);
438    assertTrue("zh_Hant should be closer to und_TW than to en_Hant_TW",
439               matchEnHantTw < matchZhHant);
440    assertTrue("zh should not match und_TW or en_Hant_TW",
441               matchZh == 0.0 && matchEnHantTw == 0.0); // with changes in CLDR-1435
442}
443
444void LocaleMatcherTest::testResolvedLocale() {
445    IcuTestErrorCode errorCode(*this, "testResolvedLocale");
446    LocaleMatcher matcher = LocaleMatcher::Builder().
447        addSupportedLocale("ar-EG").
448        build(errorCode);
449    Locale desired("ar-SA-u-nu-latn");
450    LocaleMatcher::Result result = matcher.getBestMatchResult(desired, errorCode);
451    assertEquals("best", "ar_EG", locString(result.getSupportedLocale()));
452    Locale resolved = result.makeResolvedLocale(errorCode);
453    assertEquals("ar-EG + ar-SA-u-nu-latn = ar-SA-u-nu-latn",
454                 "ar-SA-u-nu-latn",
455                 resolved.toLanguageTag<std::string>(errorCode).data());
456}
457
458namespace {
459
460bool toInvariant(const UnicodeString &s, CharString &inv, ErrorCode &errorCode) {
461    if (errorCode.isSuccess()) {
462        inv.clear().appendInvariantChars(s, errorCode);
463        return errorCode.isSuccess();
464    }
465    return false;
466}
467
468bool getSuffixAfterPrefix(const UnicodeString &s, int32_t limit,
469                          const UnicodeString &prefix, UnicodeString &suffix) {
470    if (prefix.length() <= limit && s.startsWith(prefix)) {
471        suffix.setTo(s, prefix.length(), limit - prefix.length());
472        return true;
473    } else {
474        return false;
475    }
476}
477
478bool getInvariantSuffixAfterPrefix(const UnicodeString &s, int32_t limit,
479                                   const UnicodeString &prefix, CharString &suffix,
480                                   ErrorCode &errorCode) {
481    UnicodeString u_suffix;
482    return getSuffixAfterPrefix(s, limit, prefix, u_suffix) &&
483        toInvariant(u_suffix, suffix, errorCode);
484}
485
486bool readTestCase(const UnicodeString &line, TestCase &test, IcuTestErrorCode &errorCode) {
487    if (errorCode.isFailure()) { return false; }
488    ++test.lineNr;
489    // Start of comment, or end of line, minus trailing spaces.
490    int32_t limit = line.indexOf(u'#');
491    if (limit < 0) {
492        limit = line.length();
493        // Remove trailing CR LF.
494        char16_t c;
495        while (limit > 0 && ((c = line.charAt(limit - 1)) == u'\n' || c == u'\r')) {
496            --limit;
497        }
498    }
499    // Remove spaces before comment or at the end of the line.
500    char16_t c;
501    while (limit > 0 && ((c = line.charAt(limit - 1)) == u' ' || c == u'\t')) {
502        --limit;
503    }
504    if (limit == 0) {  // empty line
505        return false;
506    }
507    if (line.startsWith(u"** test: ")) {
508        test.reset();
509    } else if (getInvariantSuffixAfterPrefix(line, limit, u"@supported=",
510                                             test.supported, errorCode)) {
511    } else if (getInvariantSuffixAfterPrefix(line, limit, u"@default=",
512                                             test.def, errorCode)) {
513    } else if (getSuffixAfterPrefix(line, limit, u"@favor=", test.favor)) {
514    } else if (getSuffixAfterPrefix(line, limit, u"@threshold=", test.threshold)) {
515    } else {
516        int32_t matchSep = line.indexOf(u">>");
517        // >> before an inline comment, and followed by more than white space.
518        if (0 <= matchSep && (matchSep + 2) < limit) {
519            toInvariant(line.tempSubStringBetween(0, matchSep).trim(), test.desired, errorCode);
520            test.expDesired.clear();
521            test.expCombined.clear();
522            int32_t start = matchSep + 2;
523            int32_t expLimit = line.indexOf(u'|', start);
524            if (expLimit < 0) {
525                toInvariant(line.tempSubStringBetween(start, limit).trim(),
526                            test.expMatch, errorCode);
527            } else {
528                toInvariant(line.tempSubStringBetween(start, expLimit).trim(),
529                            test.expMatch, errorCode);
530                start = expLimit + 1;
531                expLimit = line.indexOf(u'|', start);
532                if (expLimit < 0) {
533                    toInvariant(line.tempSubStringBetween(start, limit).trim(),
534                                test.expDesired, errorCode);
535                } else {
536                    toInvariant(line.tempSubStringBetween(start, expLimit).trim(),
537                                test.expDesired, errorCode);
538                    toInvariant(line.tempSubStringBetween(expLimit + 1, limit).trim(),
539                                test.expCombined, errorCode);
540                }
541            }
542            return errorCode.isSuccess();
543        } else {
544            errorCode.set(U_INVALID_FORMAT_ERROR);
545        }
546    }
547    return false;
548}
549
550Locale *getLocaleOrNull(const CharString &s, Locale &locale) {
551    if (s == "null") {
552        return nullptr;
553    } else {
554        return &(locale = Locale(s.data()));
555    }
556}
557
558}  // namespace
559
560UBool LocaleMatcherTest::dataDriven(const TestCase &test, IcuTestErrorCode &errorCode) {
561    LocaleMatcher::Builder builder;
562    builder.setSupportedLocalesFromListString(test.supported.toStringPiece());
563    if (!test.def.isEmpty()) {
564        Locale defaultLocale(test.def.data());
565        builder.setDefaultLocale(&defaultLocale);
566    }
567    if (!test.favor.isEmpty()) {
568        ULocMatchFavorSubtag favor;
569        if (test.favor == u"normal") {
570            favor = ULOCMATCH_FAVOR_LANGUAGE;
571        } else if (test.favor == u"script") {
572            favor = ULOCMATCH_FAVOR_SCRIPT;
573        } else {
574            errln(UnicodeString(u"unsupported FavorSubtag value ") + test.favor);
575            return false;
576        }
577        builder.setFavorSubtag(favor);
578    }
579    if (!test.threshold.isEmpty()) {
580        infoln("skipping test case on line %d with non-default threshold: not exposed via API",
581               (int)test.lineNr);
582        return true;
583        // int32_t threshold = Integer.valueOf(test.threshold);
584        // builder.internalSetThresholdDistance(threshold);
585    }
586    LocaleMatcher matcher = builder.build(errorCode);
587    if (errorCode.errIfFailureAndReset("LocaleMatcher::Builder::build()")) {
588        return false;
589    }
590
591    Locale expMatchLocale("");
592    Locale *expMatch = getLocaleOrNull(test.expMatch, expMatchLocale);
593    if (test.expDesired.isEmpty() && test.expCombined.isEmpty()) {
594        StringPiece desiredSP = test.desired.toStringPiece();
595        const Locale *bestSupported = matcher.getBestMatchForListString(desiredSP, errorCode);
596        if (!assertEquals("bestSupported from string",
597                          locString(expMatch), locString(bestSupported))) {
598            return false;
599        }
600        LocalePriorityList desired(test.desired.toStringPiece(), errorCode);
601        LocalePriorityList::Iterator desiredIter = desired.iterator();
602        if (desired.getLength() == 1) {
603            const Locale &desiredLocale = desiredIter.next();
604            bestSupported = matcher.getBestMatch(desiredLocale, errorCode);
605            UBool ok = assertEquals("bestSupported from Locale",
606                                    locString(expMatch), locString(bestSupported));
607
608            LocaleMatcher::Result result = matcher.getBestMatchResult(desiredLocale, errorCode);
609            return ok & assertEquals("result.getSupportedLocale from Locale",
610                                     locString(expMatch), locString(result.getSupportedLocale()));
611        } else {
612            bestSupported = matcher.getBestMatch(desiredIter, errorCode);
613            return assertEquals("bestSupported from Locale iterator",
614                                locString(expMatch), locString(bestSupported));
615        }
616    } else {
617        LocalePriorityList desired(test.desired.toStringPiece(), errorCode);
618        LocalePriorityList::Iterator desiredIter = desired.iterator();
619        LocaleMatcher::Result result = matcher.getBestMatchResult(desiredIter, errorCode);
620        UBool ok = assertEquals("result.getSupportedLocale from Locales",
621                                locString(expMatch), locString(result.getSupportedLocale()));
622        if (!test.expDesired.isEmpty()) {
623            Locale expDesiredLocale("");
624            Locale *expDesired = getLocaleOrNull(test.expDesired, expDesiredLocale);
625            ok &= assertEquals("result.getDesiredLocale from Locales",
626                               locString(expDesired), locString(result.getDesiredLocale()));
627        }
628        if (!test.expCombined.isEmpty()) {
629            if (test.expMatch.contains("-u-")) {
630                logKnownIssue("20727",
631                              UnicodeString(u"ignoring makeResolvedLocale() line ") + test.lineNr);
632                return ok;
633            }
634            Locale expCombinedLocale("");
635            Locale *expCombined = getLocaleOrNull(test.expCombined, expCombinedLocale);
636            Locale combined = result.makeResolvedLocale(errorCode);
637            ok &= assertEquals("combined Locale from Locales",
638                               locString(expCombined), locString(&combined));
639        }
640        return ok;
641    }
642}
643
644void LocaleMatcherTest::testDataDriven() {
645    IcuTestErrorCode errorCode(*this, "testDataDriven");
646    CharString path(getSourceTestData(errorCode), errorCode);
647    path.appendPathPart("localeMatcherTest.txt", errorCode);
648    const char *codePage = "UTF-8";
649    LocalUCHARBUFPointer f(ucbuf_open(path.data(), &codePage, true, false, errorCode));
650    if(errorCode.errIfFailureAndReset("ucbuf_open(localeMatcherTest.txt)")) {
651        return;
652    }
653    int32_t lineLength;
654    const UChar *p;
655    UnicodeString line;
656    TestCase test;
657    int32_t numPassed = 0;
658    while ((p = ucbuf_readline(f.getAlias(), &lineLength, errorCode)) != nullptr &&
659            errorCode.isSuccess()) {
660        line.setTo(false, p, lineLength);
661        if (!readTestCase(line, test, errorCode)) {
662            if (errorCode.errIfFailureAndReset(
663                    "test data syntax error on line %d", (int)test.lineNr)) {
664                infoln(line);
665            }
666            continue;
667        }
668        UBool ok = dataDriven(test, errorCode);
669        if (errorCode.errIfFailureAndReset("test error on line %d", (int)test.lineNr)) {
670            infoln(line);
671        } else if (!ok) {
672            infoln("test failure on line %d", (int)test.lineNr);
673            infoln(line);
674        } else {
675            ++numPassed;
676        }
677    }
678    infoln("number of passing test cases: %d", (int)numPassed);
679}
680