localematchertest.cpp   [plain text]


// © 2019 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License

// localematchertest.cpp
// created: 2019jul04 Markus W. Scherer

#include <string>
#include <vector>

#include "unicode/utypes.h"
#include "unicode/localematcher.h"
#include "unicode/locid.h"
#include "charstr.h"
#include "cmemory.h"
#include "intltest.h"
#include "localeprioritylist.h"
#include "ucbuf.h"

#define ARRAY_RANGE(array) (array), ((array) + UPRV_LENGTHOF(array))

namespace {

const char *locString(const Locale *loc) {
    return loc != nullptr ? loc->getName() : "(null)";
}

struct TestCase {
    int32_t lineNr = 0;

    CharString supported;
    CharString def;
    UnicodeString favor;
    UnicodeString threshold;
    CharString desired;
    CharString expMatch;
    CharString expDesired;
    CharString expCombined;

    void reset() {
        supported.clear();
        def.clear();
        favor.remove();
        threshold.remove();
    }
};

}  // namespace

class LocaleMatcherTest : public IntlTest {
public:
    LocaleMatcherTest() {}

    void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par=NULL);

    void testEmpty();
    void testCopyErrorTo();
    void testBasics();
    void testSupportedDefault();
    void testUnsupportedDefault();
    void testDemotion();
    void testMatch();
    void testResolvedLocale();
    void testDataDriven();

private:
    UBool dataDriven(const TestCase &test, IcuTestErrorCode &errorCode);
};

extern IntlTest *createLocaleMatcherTest() {
    return new LocaleMatcherTest();
}

void LocaleMatcherTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char * /*par*/) {
    if(exec) {
        logln("TestSuite LocaleMatcherTest: ");
    }
    TESTCASE_AUTO_BEGIN;
    TESTCASE_AUTO(testEmpty);
    TESTCASE_AUTO(testCopyErrorTo);
    TESTCASE_AUTO(testBasics);
    TESTCASE_AUTO(testSupportedDefault);
    TESTCASE_AUTO(testUnsupportedDefault);
    TESTCASE_AUTO(testDemotion);
    TESTCASE_AUTO(testMatch);
    TESTCASE_AUTO(testResolvedLocale);
    TESTCASE_AUTO(testDataDriven);
    TESTCASE_AUTO_END;
}

void LocaleMatcherTest::testEmpty() {
    IcuTestErrorCode errorCode(*this, "testEmpty");
    LocaleMatcher matcher = LocaleMatcher::Builder().build(errorCode);
    const Locale *best = matcher.getBestMatch(Locale::getFrench(), errorCode);
    assertEquals("getBestMatch(fr)", "(null)", locString(best));
    LocaleMatcher::Result result = matcher.getBestMatchResult("fr", errorCode);
    assertEquals("getBestMatchResult(fr).des", "(null)", locString(result.getDesiredLocale()));
    assertEquals("getBestMatchResult(fr).desIndex", -1, result.getDesiredIndex());
    assertEquals("getBestMatchResult(fr).supp",
                 "(null)", locString(result.getSupportedLocale()));
    assertEquals("getBestMatchResult(fr).suppIndex",
                 -1, result.getSupportedIndex());
}

void LocaleMatcherTest::testCopyErrorTo() {
    IcuTestErrorCode errorCode(*this, "testCopyErrorTo");
    // The builder does not set any errors except out-of-memory.
    // Test what we can.
    LocaleMatcher::Builder builder;
    UErrorCode success = U_ZERO_ERROR;
    assertFalse("no error", builder.copyErrorTo(success));
    assertTrue("still success", U_SUCCESS(success));
    UErrorCode failure = U_INVALID_FORMAT_ERROR;
    assertTrue("failure passed in", builder.copyErrorTo(failure));
    assertEquals("same failure", U_INVALID_FORMAT_ERROR, failure);
}

void LocaleMatcherTest::testBasics() {
    IcuTestErrorCode errorCode(*this, "testBasics");
    Locale locales[] = { "fr", "en_GB", "en" };
    {
        LocaleMatcher matcher = LocaleMatcher::Builder().
            setSupportedLocales(ARRAY_RANGE(locales)).build(errorCode);
        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
        assertEquals("fromRange.getBestMatch(en_GB)", "en_GB", locString(best));
        best = matcher.getBestMatch("en_US", errorCode);
        assertEquals("fromRange.getBestMatch(en_US)", "en", locString(best));
        best = matcher.getBestMatch("fr_FR", errorCode);
        assertEquals("fromRange.getBestMatch(fr_FR)", "fr", locString(best));
        best = matcher.getBestMatch("ja_JP", errorCode);
        assertEquals("fromRange.getBestMatch(ja_JP)", "fr", locString(best));
    }
    // Code coverage: Variations of setting supported locales.
    {
        std::vector<Locale> locales{ "fr", "en_GB", "en" };
        LocaleMatcher matcher = LocaleMatcher::Builder().
            setSupportedLocales(locales.begin(), locales.end()).build(errorCode);
        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
        assertEquals("fromRange.getBestMatch(en_GB)", "en_GB", locString(best));
        best = matcher.getBestMatch("en_US", errorCode);
        assertEquals("fromRange.getBestMatch(en_US)", "en", locString(best));
        best = matcher.getBestMatch("fr_FR", errorCode);
        assertEquals("fromRange.getBestMatch(fr_FR)", "fr", locString(best));
        best = matcher.getBestMatch("ja_JP", errorCode);
        assertEquals("fromRange.getBestMatch(ja_JP)", "fr", locString(best));
    }
    {
        Locale::RangeIterator<Locale *> iter(ARRAY_RANGE(locales));
        LocaleMatcher matcher = LocaleMatcher::Builder().
            setSupportedLocales(iter).build(errorCode);
        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
        assertEquals("fromIter.getBestMatch(en_GB)", "en_GB", locString(best));
        best = matcher.getBestMatch("en_US", errorCode);
        assertEquals("fromIter.getBestMatch(en_US)", "en", locString(best));
        best = matcher.getBestMatch("fr_FR", errorCode);
        assertEquals("fromIter.getBestMatch(fr_FR)", "fr", locString(best));
        best = matcher.getBestMatch("ja_JP", errorCode);
        assertEquals("fromIter.getBestMatch(ja_JP)", "fr", locString(best));
    }
    {
        Locale *pointers[] = { locales, locales + 1, locales + 2 };
        // Lambda with explicit reference return type to prevent copy-constructing a temporary
        // which would be destructed right away.
        LocaleMatcher matcher = LocaleMatcher::Builder().
            setSupportedLocalesViaConverter(
                ARRAY_RANGE(pointers), [](const Locale *p) -> const Locale & { return *p; }).
            build(errorCode);
        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
        assertEquals("viaConverter.getBestMatch(en_GB)", "en_GB", locString(best));
        best = matcher.getBestMatch("en_US", errorCode);
        assertEquals("viaConverter.getBestMatch(en_US)", "en", locString(best));
        best = matcher.getBestMatch("fr_FR", errorCode);
        assertEquals("viaConverter.getBestMatch(fr_FR)", "fr", locString(best));
        best = matcher.getBestMatch("ja_JP", errorCode);
        assertEquals("viaConverter.getBestMatch(ja_JP)", "fr", locString(best));
    }
    {
        LocaleMatcher matcher = LocaleMatcher::Builder().
            addSupportedLocale(locales[0]).
            addSupportedLocale(locales[1]).
            addSupportedLocale(locales[2]).
            build(errorCode);
        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
        assertEquals("added.getBestMatch(en_GB)", "en_GB", locString(best));
        best = matcher.getBestMatch("en_US", errorCode);
        assertEquals("added.getBestMatch(en_US)", "en", locString(best));
        best = matcher.getBestMatch("fr_FR", errorCode);
        assertEquals("added.getBestMatch(fr_FR)", "fr", locString(best));
        best = matcher.getBestMatch("ja_JP", errorCode);
        assertEquals("added.getBestMatch(ja_JP)", "fr", locString(best));
    }
    {
        LocaleMatcher matcher = LocaleMatcher::Builder().
            setSupportedLocalesFromListString(
                " el, fr;q=0.555555, en-GB ; q = 0.88  , el; q =0, en;q=0.88 , fr ").
            build(errorCode);
        const Locale *best = matcher.getBestMatchForListString("el, fr, fr;q=0, en-GB", errorCode);
        assertEquals("fromList.getBestMatch(en_GB)", "en_GB", locString(best));
        best = matcher.getBestMatch("en_US", errorCode);
        assertEquals("fromList.getBestMatch(en_US)", "en", locString(best));
        best = matcher.getBestMatch("fr_FR", errorCode);
        assertEquals("fromList.getBestMatch(fr_FR)", "fr", locString(best));
        best = matcher.getBestMatch("ja_JP", errorCode);
        assertEquals("fromList.getBestMatch(ja_JP)", "fr", locString(best));
    }
    // more API coverage
    {
        LocalePriorityList list("fr, en-GB", errorCode);
        LocalePriorityList::Iterator iter(list.iterator());
        LocaleMatcher matcher = LocaleMatcher::Builder().
            setSupportedLocales(iter).
            addSupportedLocale(Locale::getEnglish()).
            setDefaultLocale(&Locale::getGerman()).
            build(errorCode);
        const Locale *best = matcher.getBestMatch("en_GB", errorCode);
        assertEquals("withDefault.getBestMatch(en_GB)", "en_GB", locString(best));
        best = matcher.getBestMatch("en_US", errorCode);
        assertEquals("withDefault.getBestMatch(en_US)", "en", locString(best));
        best = matcher.getBestMatch("fr_FR", errorCode);
        assertEquals("withDefault.getBestMatch(fr_FR)", "fr", locString(best));
        best = matcher.getBestMatch("ja_JP", errorCode);
        assertEquals("withDefault.getBestMatch(ja_JP)", "de", locString(best));

        Locale desired("en_GB");  // distinct object from Locale.UK
        LocaleMatcher::Result result = matcher.getBestMatchResult(desired, errorCode);
        assertTrue("withDefault: exactly desired en-GB object",
                   &desired == result.getDesiredLocale());
        assertEquals("withDefault: en-GB desired index", 0, result.getDesiredIndex());
        assertEquals("withDefault: en-GB supported",
                     "en_GB", locString(result.getSupportedLocale()));
        assertEquals("withDefault: en-GB supported index", 1, result.getSupportedIndex());

        LocalePriorityList list2("ja-JP, en-US", errorCode);
        LocalePriorityList::Iterator iter2(list2.iterator());
        result = matcher.getBestMatchResult(iter2, errorCode);
        assertEquals("withDefault: ja-JP, en-US desired index", 1, result.getDesiredIndex());
        assertEquals("withDefault: ja-JP, en-US desired",
                     "en_US", locString(result.getDesiredLocale()));

        desired = Locale("en", "US");  // distinct object from Locale.US
        result = matcher.getBestMatchResult(desired, errorCode);
        assertTrue("withDefault: exactly desired en-US object",
                   &desired == result.getDesiredLocale());
        assertEquals("withDefault: en-US desired index", 0, result.getDesiredIndex());
        assertEquals("withDefault: en-US supported", "en", locString(result.getSupportedLocale()));
        assertEquals("withDefault: en-US supported index", 2, result.getSupportedIndex());

        result = matcher.getBestMatchResult("ja_JP", errorCode);
        assertEquals("withDefault: ja-JP desired", "(null)", locString(result.getDesiredLocale()));
        assertEquals("withDefault: ja-JP desired index", -1, result.getDesiredIndex());
        assertEquals("withDefault: ja-JP supported", "de", locString(result.getSupportedLocale()));
        assertEquals("withDefault: ja-JP supported index", -1, result.getSupportedIndex());
    }
}

void LocaleMatcherTest::testSupportedDefault() {
    // The default locale is one of the supported locales.
    IcuTestErrorCode errorCode(*this, "testSupportedDefault");
    Locale locales[] = { "fr", "en_GB", "en" };
    LocaleMatcher matcher = LocaleMatcher::Builder().
        setSupportedLocales(ARRAY_RANGE(locales)).
        setDefaultLocale(&locales[1]).
        build(errorCode);
    const Locale *best = matcher.getBestMatch("en_GB", errorCode);
    assertEquals("getBestMatch(en_GB)", "en_GB", locString(best));
    best = matcher.getBestMatch("en_US", errorCode);
    assertEquals("getBestMatch(en_US)", "en", locString(best));
    best = matcher.getBestMatch("fr_FR", errorCode);
    assertEquals("getBestMatch(fr_FR)", "fr", locString(best));
    best = matcher.getBestMatch("ja_JP", errorCode);
    assertEquals("getBestMatch(ja_JP)", "en_GB", locString(best));
    LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode);
    assertEquals("getBestMatchResult(ja_JP).supp",
                 "en_GB", locString(result.getSupportedLocale()));
    assertEquals("getBestMatchResult(ja_JP).suppIndex",
                 1, result.getSupportedIndex());
}

void LocaleMatcherTest::testUnsupportedDefault() {
    // The default locale does not match any of the supported locales.
    IcuTestErrorCode errorCode(*this, "testUnsupportedDefault");
    Locale locales[] = { "fr", "en_GB", "en" };
    Locale def("de");
    LocaleMatcher matcher = LocaleMatcher::Builder().
        setSupportedLocales(ARRAY_RANGE(locales)).
        setDefaultLocale(&def).
        build(errorCode);
    const Locale *best = matcher.getBestMatch("en_GB", errorCode);
    assertEquals("getBestMatch(en_GB)", "en_GB", locString(best));
    best = matcher.getBestMatch("en_US", errorCode);
    assertEquals("getBestMatch(en_US)", "en", locString(best));
    best = matcher.getBestMatch("fr_FR", errorCode);
    assertEquals("getBestMatch(fr_FR)", "fr", locString(best));
    best = matcher.getBestMatch("ja_JP", errorCode);
    assertEquals("getBestMatch(ja_JP)", "de", locString(best));
    LocaleMatcher::Result result = matcher.getBestMatchResult("ja_JP", errorCode);
    assertEquals("getBestMatchResult(ja_JP).supp",
                 "de", locString(result.getSupportedLocale()));
    assertEquals("getBestMatchResult(ja_JP).suppIndex",
                 -1, result.getSupportedIndex());
}

void LocaleMatcherTest::testDemotion() {
    IcuTestErrorCode errorCode(*this, "testDemotion");
    Locale supported[] = { "fr", "de-CH", "it" };
    Locale desired[] = { "fr-CH", "de-CH", "it" };
    {
        LocaleMatcher noDemotion = LocaleMatcher::Builder().
            setSupportedLocales(ARRAY_RANGE(supported)).
            setDemotionPerDesiredLocale(ULOCMATCH_DEMOTION_NONE).build(errorCode);
        Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired));
        assertEquals("no demotion",
                     "de_CH", locString(noDemotion.getBestMatch(desiredIter, errorCode)));
    }

    {
        LocaleMatcher regionDemotion = LocaleMatcher::Builder().
            setSupportedLocales(ARRAY_RANGE(supported)).
            setDemotionPerDesiredLocale(ULOCMATCH_DEMOTION_REGION).build(errorCode);
        Locale::RangeIterator<Locale *> desiredIter(ARRAY_RANGE(desired));
        assertEquals("region demotion",
                     "fr", locString(regionDemotion.getBestMatch(desiredIter, errorCode)));
    }
}

void LocaleMatcherTest::testMatch() {
    IcuTestErrorCode errorCode(*this, "testMatch");
    LocaleMatcher matcher = LocaleMatcher::Builder().build(errorCode);

    // Java test function testMatch_exact()
    Locale en_CA("en_CA");
    assertEquals("exact match", 1.0, matcher.internalMatch(en_CA, en_CA, errorCode));

    // testMatch_none
    Locale ar_MK("ar_MK");
    double match = matcher.internalMatch(ar_MK, en_CA, errorCode);
    assertTrue("mismatch: 0<=match<0.2", 0 <= match && match < 0.2);

    // testMatch_matchOnMaximized
    Locale und_TW("und_TW");
    Locale zh("zh");
    Locale zh_Hant("zh_Hant");
    double matchZh = matcher.internalMatch(und_TW, zh, errorCode);
    double matchZhHant = matcher.internalMatch(und_TW, zh_Hant, errorCode);
    assertTrue("und_TW should be closer to zh_Hant than to zh",
               matchZh < matchZhHant);
    Locale en_Hant_TW("en_Hant_TW");
    double matchEnHantTw = matcher.internalMatch(en_Hant_TW, zh_Hant, errorCode);
    assertTrue("zh_Hant should be closer to und_TW than to en_Hant_TW",
               matchEnHantTw < matchZhHant);
    assertTrue("zh should be closer to und_TW than to en_Hant_TW",
               matchEnHantTw < matchZh);
}

void LocaleMatcherTest::testResolvedLocale() {
    IcuTestErrorCode errorCode(*this, "testResolvedLocale");
    LocaleMatcher matcher = LocaleMatcher::Builder().
        addSupportedLocale("ar-EG").
        build(errorCode);
    Locale desired("ar-SA-u-nu-latn");
    LocaleMatcher::Result result = matcher.getBestMatchResult(desired, errorCode);
    assertEquals("best", "ar_EG", locString(result.getSupportedLocale()));
    Locale resolved = result.makeResolvedLocale(errorCode);
    assertEquals("ar-EG + ar-SA-u-nu-latn = ar-SA-u-nu-latn",
                 "ar-SA-u-nu-latn",
                 resolved.toLanguageTag<std::string>(errorCode).data());
}

namespace {

bool toInvariant(const UnicodeString &s, CharString &inv, ErrorCode &errorCode) {
    if (errorCode.isSuccess()) {
        inv.clear().appendInvariantChars(s, errorCode);
        return errorCode.isSuccess();
    }
    return false;
}

bool getSuffixAfterPrefix(const UnicodeString &s, int32_t limit,
                          const UnicodeString &prefix, UnicodeString &suffix) {
    if (prefix.length() <= limit && s.startsWith(prefix)) {
        suffix.setTo(s, prefix.length(), limit - prefix.length());
        return true;
    } else {
        return false;
    }
}

bool getInvariantSuffixAfterPrefix(const UnicodeString &s, int32_t limit,
                                   const UnicodeString &prefix, CharString &suffix,
                                   ErrorCode &errorCode) {
    UnicodeString u_suffix;
    return getSuffixAfterPrefix(s, limit, prefix, u_suffix) &&
        toInvariant(u_suffix, suffix, errorCode);
}

bool readTestCase(const UnicodeString &line, TestCase &test, IcuTestErrorCode &errorCode) {
    if (errorCode.isFailure()) { return false; }
    ++test.lineNr;
    // Start of comment, or end of line, minus trailing spaces.
    int32_t limit = line.indexOf(u'#');
    if (limit < 0) {
        limit = line.length();
        // Remove trailing CR LF.
        char16_t c;
        while (limit > 0 && ((c = line.charAt(limit - 1)) == u'\n' || c == u'\r')) {
            --limit;
        }
    }
    // Remove spaces before comment or at the end of the line.
    char16_t c;
    while (limit > 0 && ((c = line.charAt(limit - 1)) == u' ' || c == u'\t')) {
        --limit;
    }
    if (limit == 0) {  // empty line
        return false;
    }
    if (line.startsWith(u"** test: ")) {
        test.reset();
    } else if (getInvariantSuffixAfterPrefix(line, limit, u"@supported=",
                                             test.supported, errorCode)) {
    } else if (getInvariantSuffixAfterPrefix(line, limit, u"@default=",
                                             test.def, errorCode)) {
    } else if (getSuffixAfterPrefix(line, limit, u"@favor=", test.favor)) {
    } else if (getSuffixAfterPrefix(line, limit, u"@threshold=", test.threshold)) {
    } else {
        int32_t matchSep = line.indexOf(u">>");
        // >> before an inline comment, and followed by more than white space.
        if (0 <= matchSep && (matchSep + 2) < limit) {
            toInvariant(line.tempSubStringBetween(0, matchSep).trim(), test.desired, errorCode);
            test.expDesired.clear();
            test.expCombined.clear();
            int32_t start = matchSep + 2;
            int32_t expLimit = line.indexOf(u'|', start);
            if (expLimit < 0) {
                toInvariant(line.tempSubStringBetween(start, limit).trim(),
                            test.expMatch, errorCode);
            } else {
                toInvariant(line.tempSubStringBetween(start, expLimit).trim(),
                            test.expMatch, errorCode);
                start = expLimit + 1;
                expLimit = line.indexOf(u'|', start);
                if (expLimit < 0) {
                    toInvariant(line.tempSubStringBetween(start, limit).trim(),
                                test.expDesired, errorCode);
                } else {
                    toInvariant(line.tempSubStringBetween(start, expLimit).trim(),
                                test.expDesired, errorCode);
                    toInvariant(line.tempSubStringBetween(expLimit + 1, limit).trim(),
                                test.expCombined, errorCode);
                }
            }
            return errorCode.isSuccess();
        } else {
            errorCode.set(U_INVALID_FORMAT_ERROR);
        }
    }
    return false;
}

Locale *getLocaleOrNull(const CharString &s, Locale &locale) {
    if (s == "null") {
        return nullptr;
    } else {
        return &(locale = Locale(s.data()));
    }
}

}  // namespace

UBool LocaleMatcherTest::dataDriven(const TestCase &test, IcuTestErrorCode &errorCode) {
    LocaleMatcher::Builder builder;
    builder.setSupportedLocalesFromListString(test.supported.toStringPiece());
    if (!test.def.isEmpty()) {
        Locale defaultLocale(test.def.data());
        builder.setDefaultLocale(&defaultLocale);
    }
    if (!test.favor.isEmpty()) {
        ULocMatchFavorSubtag favor;
        if (test.favor == u"normal") {
            favor = ULOCMATCH_FAVOR_LANGUAGE;
        } else if (test.favor == u"script") {
            favor = ULOCMATCH_FAVOR_SCRIPT;
        } else {
            errln(UnicodeString(u"unsupported FavorSubtag value ") + test.favor);
            return FALSE;
        }
        builder.setFavorSubtag(favor);
    }
    if (!test.threshold.isEmpty()) {
        infoln("skipping test case on line %d with non-default threshold: not exposed via API",
               (int)test.lineNr);
        return TRUE;
        // int32_t threshold = Integer.valueOf(test.threshold);
        // builder.internalSetThresholdDistance(threshold);
    }
    LocaleMatcher matcher = builder.build(errorCode);
    if (errorCode.errIfFailureAndReset("LocaleMatcher::Builder::build()")) {
        return FALSE;
    }

    Locale expMatchLocale("");
    Locale *expMatch = getLocaleOrNull(test.expMatch, expMatchLocale);
    if (test.expDesired.isEmpty() && test.expCombined.isEmpty()) {
        StringPiece desiredSP = test.desired.toStringPiece();
        const Locale *bestSupported = matcher.getBestMatchForListString(desiredSP, errorCode);
        if (!assertEquals("bestSupported from string",
                          locString(expMatch), locString(bestSupported))) {
            return FALSE;
        }
        LocalePriorityList desired(test.desired.toStringPiece(), errorCode);
        LocalePriorityList::Iterator desiredIter = desired.iterator();
        if (desired.getLength() == 1) {
            const Locale &desiredLocale = desiredIter.next();
            bestSupported = matcher.getBestMatch(desiredLocale, errorCode);
            UBool ok = assertEquals("bestSupported from Locale",
                                    locString(expMatch), locString(bestSupported));

            LocaleMatcher::Result result = matcher.getBestMatchResult(desiredLocale, errorCode);
            return ok & assertEquals("result.getSupportedLocale from Locale",
                                     locString(expMatch), locString(result.getSupportedLocale()));
        } else {
            bestSupported = matcher.getBestMatch(desiredIter, errorCode);
            return assertEquals("bestSupported from Locale iterator",
                                locString(expMatch), locString(bestSupported));
        }
    } else {
        LocalePriorityList desired(test.desired.toStringPiece(), errorCode);
        LocalePriorityList::Iterator desiredIter = desired.iterator();
        LocaleMatcher::Result result = matcher.getBestMatchResult(desiredIter, errorCode);
        UBool ok = assertEquals("result.getSupportedLocale from Locales",
                                locString(expMatch), locString(result.getSupportedLocale()));
        if (!test.expDesired.isEmpty()) {
            Locale expDesiredLocale("");
            Locale *expDesired = getLocaleOrNull(test.expDesired, expDesiredLocale);
            ok &= assertEquals("result.getDesiredLocale from Locales",
                               locString(expDesired), locString(result.getDesiredLocale()));
        }
        if (!test.expCombined.isEmpty()) {
            if (test.expMatch.contains("-u-")) {
                logKnownIssue("20727",
                              UnicodeString(u"ignoring makeResolvedLocale() line ") + test.lineNr);
                return ok;
            }
            Locale expCombinedLocale("");
            Locale *expCombined = getLocaleOrNull(test.expCombined, expCombinedLocale);
            Locale combined = result.makeResolvedLocale(errorCode);
            ok &= assertEquals("combined Locale from Locales",
                               locString(expCombined), locString(&combined));
        }
        return ok;
    }
}

void LocaleMatcherTest::testDataDriven() {
    IcuTestErrorCode errorCode(*this, "testDataDriven");
    CharString path(getSourceTestData(errorCode), errorCode);
    path.appendPathPart("localeMatcherTest.txt", errorCode);
    const char *codePage = "UTF-8";
    LocalUCHARBUFPointer f(ucbuf_open(path.data(), &codePage, TRUE, FALSE, errorCode));
    if(errorCode.errIfFailureAndReset("ucbuf_open(localeMatcherTest.txt)")) {
        return;
    }
    int32_t lineLength;
    const UChar *p;
    UnicodeString line;
    TestCase test;
    int32_t numPassed = 0;
    while ((p = ucbuf_readline(f.getAlias(), &lineLength, errorCode)) != nullptr &&
            errorCode.isSuccess()) {
        line.setTo(FALSE, p, lineLength);
        if (!readTestCase(line, test, errorCode)) {
            if (errorCode.errIfFailureAndReset(
                    "test data syntax error on line %d", (int)test.lineNr)) {
                infoln(line);
            }
            continue;
        }
        UBool ok = dataDriven(test, errorCode);
        if (errorCode.errIfFailureAndReset("test error on line %d", (int)test.lineNr)) {
            infoln(line);
        } else if (!ok) {
            infoln("test failure on line %d", (int)test.lineNr);
            infoln(line);
        } else {
            ++numPassed;
        }
    }
    infoln("number of passing test cases: %d", (int)numPassed);
}