summaryrefslogtreecommitdiffstats
path: root/tests/auto
diff options
context:
space:
mode:
authorVolker Hilsheimer <volker.hilsheimer@qt.io>2023-04-10 17:35:42 +0200
committerVolker Hilsheimer <volker.hilsheimer@qt.io>2023-04-17 17:27:55 +0200
commit339d321c8f51da29100cbacdaeccf0b6def6718e (patch)
tree6371ab847b9d6545b5fa4eb6fd34312ab366b5f9 /tests/auto
parentb6882d8f0c8c002b70fc9f5db6e11021b51739b6 (diff)
Add QTextToSpeech::findVoices
QVoice is not a type that can be constructed by client code, it can only be constructed by the engine (as the QVoice instance carries the engine specific identifier, which an application cannot know). Applications that want to select a voice that matches a certain (set of) criteria - for instance "a male English voice" - have to go through some length to first get the list of all voices (availableVoices returns only the voices for the current locale, so one first has to go through all locales, sets each of them, and combine the lists of voices for each locale), and then postprocess that list to find a voice that matches. For C++, a variadic template function returns the list of voices that matches an arbitrary set of criteria, in an arbitrary order. Each argument is matched against the corresponding property of a voice based on its type. It's possible to match voices against only language or territory as well as a fully defined QLocale object. For QML, the function takes a map from voice's property name to value. Since QML cannot construct a locale with an "Any" territory or language, a "language" property is added to the voice type so that only the language attribute of the provided locale is compared. Make this testable by providing the mock engine with support for a parameter "voices". That parameter takes a list of std::tuple, as otherwise we'd have to create a VoiceData struct type of sorts that the test can use. Extend the list of built-in voices, and improve QVoice's debug output by printing the full locale, not just the language. Change-Id: I5266e65932ea3db52fae92a6f50caa14dbe1f2f6 Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
Diffstat (limited to 'tests/auto')
-rw-r--r--tests/auto/qtexttospeech/tst_qtexttospeech.cpp160
-rw-r--r--tests/auto/qtexttospeech_qml/tst_texttospeech.qml25
2 files changed, 184 insertions, 1 deletions
diff --git a/tests/auto/qtexttospeech/tst_qtexttospeech.cpp b/tests/auto/qtexttospeech/tst_qtexttospeech.cpp
index a7c2a3c..017eef5 100644
--- a/tests/auto/qtexttospeech/tst_qtexttospeech.cpp
+++ b/tests/auto/qtexttospeech/tst_qtexttospeech.cpp
@@ -35,6 +35,8 @@ private slots:
void availableVoices();
void availableLocales();
+ void findVoices_data();
+ void findVoices();
void locale();
void voice();
@@ -66,6 +68,10 @@ private slots:
void synthesizeCallback_data();
void synthesizeCallback();
+public:
+ using Selector = QList<QVoice>(*)(const QTextToSpeech *);
+ using VoiceData = typename std::tuple<QString, QLocale, QVoice::Gender, QVoice::Age>;
+
private:
static bool hasDefaultAudioOutput()
{
@@ -96,6 +102,37 @@ private:
QTextToSpeech::ErrorReason errorReason = QTextToSpeech::ErrorReason::NoError;
};
+// enable QCOMPARE and debug output in case of failure
+QT_BEGIN_NAMESPACE
+bool operator==(const QList<QVoice> &lhs, const QList<tst_QTextToSpeech::VoiceData> &rhs)
+{
+ if (lhs.size() != rhs.size())
+ return false;
+ // we can't assume any stable sorting, so we have to check that all
+ // entries from lhs are also in rhs.
+ for (const auto &voice : lhs) {
+ const tst_QTextToSpeech::VoiceData data{voice.name(), voice.locale(),
+ voice.gender(), voice.age()};
+ if (!rhs.contains(data))
+ return false;
+ }
+ return true;
+}
+
+QDebug &operator<<(QDebug &dbg, const tst_QTextToSpeech::VoiceData &data)
+{
+ QDebugStateSaver rollback(dbg);
+ const auto [name, locale, gender, age] = data;
+ dbg.nospace().noquote();
+ dbg << "VoiceData(name: " << name
+ << ", locale: " << locale
+ << ", gender: " << QVoice::genderName(gender)
+ << ", age: " << QVoice::ageName(age)
+ << ")";
+ return dbg;
+}
+QT_END_NAMESPACE
+
void tst_QTextToSpeech::initTestCase_data()
{
qInfo("Available text-to-speech engines:");
@@ -186,6 +223,129 @@ void tst_QTextToSpeech::availableLocales()
qInfo().noquote() << "-" << locale;
}
+void tst_QTextToSpeech::findVoices_data()
+{
+#define SELECTOR(...) Selector([](const QTextToSpeech *tts) -> QList<QVoice> \
+ { return tts->findVoices(__VA_ARGS__); })
+
+ QTest::addColumn<QList<VoiceData>>("voicesData");
+ QTest::addColumn<Selector>("selector");
+ QTest::addColumn<QList<VoiceData>>("expectedVoices");
+
+ const VoiceData bob = {u"Bob"_s, QLocale(QLocale::English, QLocale::UnitedKingdom),
+ QVoice::Male, QVoice::Senior};
+ const VoiceData alice = {u"Alice"_s, QLocale(QLocale::English, QLocale::UnitedStates),
+ QVoice::Female, QVoice::Senior};
+ const VoiceData maleSenior = {u"Bob"_s, QLocale(QLocale::English),
+ QVoice::Male, QVoice::Senior};
+ const VoiceData maleChild = {u"Thomas"_s, QLocale(QLocale::German),
+ QVoice::Male, QVoice::Child};
+ const VoiceData femaleTeen = {u"Anne"_s, QLocale(QLocale::English, QLocale::Australia),
+ QVoice::Female, QVoice::Teenager};
+ const VoiceData femaleAdult = {u"Mary"_s, QLocale(QLocale::English),
+ QVoice::Female, QVoice::Adult};
+ const VoiceData diverseTeen = {u"Charly"_s, QLocale(QLocale::French),
+ QVoice::Unknown, QVoice::Teenager};
+ const VoiceData diverseAgeless = {u"Sam"_s, QLocale(QLocale::Chinese),
+ QVoice::Unknown, QVoice::Other};
+ const VoiceData fromOslo = {u"Julia"_s, QLocale(QLocale::NorwegianBokmal, QLocale::Norway),
+ QVoice::Female, QVoice::Adult};
+ const VoiceData fromWestcoast = {u"Morten"_s, QLocale(QLocale::NorwegianNynorsk, QLocale::Norway),
+ QVoice::Male, QVoice::Teenager};
+
+ const QList<VoiceData> allVoices{bob, alice,
+ maleSenior, maleChild,
+ femaleTeen, femaleAdult,
+ diverseTeen, diverseAgeless,
+ fromOslo, fromWestcoast};
+
+ QTest::addRow("one-of-one") << QList<VoiceData>{bob}
+ << SELECTOR(u"Bob"_s)
+ << QList<VoiceData>{bob};
+ QTest::addRow("none") << allVoices
+ << SELECTOR(u"Francis"_s)
+ << QList<VoiceData>{};
+ QTest::addRow("all") << QList<VoiceData>{bob, alice}
+ << SELECTOR()
+ << QList<VoiceData>{bob, alice};
+ QTest::addRow("bobs") << allVoices
+ << SELECTOR(u"Bob"_s)
+ << QList<VoiceData>{bob, maleSenior};
+ QTest::addRow("male") << allVoices
+ << SELECTOR(QVoice::Male)
+ << QList<VoiceData>{fromWestcoast, bob, maleSenior, maleChild};
+ QTest::addRow("senior") << allVoices
+ << SELECTOR(QVoice::Senior)
+ << QList<VoiceData>{bob, alice, maleSenior};
+ QTest::addRow("Chinese") << allVoices
+ << SELECTOR(QLocale::Chinese)
+ << QList<VoiceData>{diverseAgeless};
+ QTest::addRow("from Norway") << allVoices
+ << SELECTOR(QLocale::Norway)
+ << QList<VoiceData>{fromOslo, fromWestcoast};
+
+ // multiple of same type - not supported as it would always yield an empty result,
+ // so we generate a compile-time error. Ideally we could logically OR those criteria,
+ // but this is not a SQL database.
+/*
+ QTest::addRow("impossible") << allVoices
+ << SELECTOR(QLocale::Norway, QLocale::Sweden)
+ << QList<VoiceData>{};
+*/
+
+ // mostly compile tests
+ QTest::addRow("QLatin1String") << allVoices
+ << SELECTOR(QLatin1String("Alice"))
+ << QList<VoiceData>{alice};
+ QTest::addRow("QLatin1StringView") << allVoices
+ << SELECTOR(QLatin1StringView("Alice"))
+ << QList<VoiceData>{alice};
+ QTest::addRow("QStringView") << allVoices
+ << SELECTOR(QStringView(u"Alice"))
+ << QList<VoiceData>{alice};
+ QTest::addRow("QRegularExpression") << allVoices
+ << SELECTOR(QRegularExpression("A(.*)"))
+ << QList<VoiceData>{alice, femaleTeen};
+
+#ifndef QT_NO_CAST_FROM_ASCII
+ // compile tests for types that are implicitly convertible to QString
+ QTest::addRow("const char*") << allVoices
+ << SELECTOR("Alice")
+ << QList<VoiceData>{alice};
+ QTest::addRow("QByteArray") << allVoices
+ << SELECTOR(QByteArray("Alice"))
+ << QList<VoiceData>{alice};
+#endif
+
+#undef SELECTOR
+}
+
+void tst_QTextToSpeech::findVoices()
+{
+ QFETCH_GLOBAL(const QString, engine);
+ // Testing once with mock engine is enough, no need to generate QSKIP noise
+ if (engine != "mock")
+ return;
+
+ QFETCH(const QList<VoiceData>, voicesData);
+ QFETCH(const Selector, selector);
+ QFETCH(const QList<VoiceData>, expectedVoices);
+ QVariantMap parameters;
+ parameters["voices"] = QVariant::fromValue(voicesData);
+ QTextToSpeech tts(engine, parameters);
+ QSignalSpy localeChangedSpy(&tts, &QTextToSpeech::localeChanged);
+ QSignalSpy voiceChangedSpy(&tts, &QTextToSpeech::voiceChanged);
+
+ const QList<QVoice> result = selector(&tts);
+
+ QCOMPARE(result, expectedVoices);
+ QCOMPARE(localeChangedSpy.count(), 0);
+ QCOMPARE(voiceChangedSpy.count(), 0);
+
+ // compile test
+ QCOMPARE(tts.findVoices(tts.locale()), tts.availableVoices());
+}
+
/*
Testing the locale property, and its dependency on the voice
property.
diff --git a/tests/auto/qtexttospeech_qml/tst_texttospeech.qml b/tests/auto/qtexttospeech_qml/tst_texttospeech.qml
index 09338be..d01c86d 100644
--- a/tests/auto/qtexttospeech_qml/tst_texttospeech.qml
+++ b/tests/auto/qtexttospeech_qml/tst_texttospeech.qml
@@ -19,10 +19,33 @@ TestCase {
}
function test_availableLocales() {
- compare(tts.availableLocales().length, 3)
+ compare(tts.availableLocales().length, 5)
}
function test_availableVoices() {
compare(tts.availableVoices().length, 2)
}
+
+ function test_findVoices() {
+ let bob = tts.findVoices({"name":"Bob"})
+ compare(bob.length, 1)
+ let women = tts.findVoices({"gender":Voice.Female})
+ compare(women.length, 5)
+ let children = tts.findVoices({"age":Voice.Child})
+ compare(children.length, 1)
+ // includes all english speakers, no matter where they're from
+ let english = tts.findVoices({"language":Qt.locale("en")})
+ compare(english.length, 4)
+ let bokmalers = tts.findVoices({"locale":Qt.locale("NO")})
+ compare(bokmalers.length, 2)
+ let nynorskers = tts.findVoices({"locale":Qt.locale("nn-NO")})
+ compare(nynorskers.length, 2)
+
+ let englishWomen = tts.findVoices({
+ "language": Qt.locale("en"),
+ "gender": Voice.Female,
+ "age": Voice.Adult
+ });
+ compare(englishWomen.length, 1)
+ }
}