MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 92.8% 77 / 0 / 83
Functions: 90.9% 10 / 0 / 11
Branches: 74.4% 125 / 0 / 168

presentation/SpellService.cpp
Line Branch Exec Source
1 #include "SpellService.h"
2 #include "ComposeLog.h"
3 #include "data/SpellDictionaryPaths.h"
4
5 #include <QLocale>
6 #include <QRegularExpression>
7 #include <QStandardPaths>
8 #include <QVariantMap>
9
10 #ifdef MAILJD_HAVE_SONNET
11 #include <Sonnet/GuessLanguage>
12 #include <Sonnet/Speller>
13 #endif
14
15 795 SpellService::SpellService(QObject *parent) : QObject(parent) {}
16
17 6222 QStringList SpellService::languages() const {
18
2/2
✓ Branch 2 → 3 taken 6120 times.
✓ Branch 2 → 4 taken 102 times.
6222 if (m_loaded) return m_languages;
19 102 m_loaded = true;
20 #ifdef MAILJD_HAVE_SONNET
21 // F108-17: the dictionaries other programs bring along, before Sonnet
22 // loads its Hunspell plugin (it reads the data directories then).
23
1/1
✓ Branch 4 → 5 taken 102 times.
102 const QStringList roots = SpellDictionaryPaths::bundledRoots();
24 const QStringList linked = SpellDictionaryPaths::expose(
25
1/1
✓ Branch 10 → 11 taken 102 times.
204 SpellDictionaryPaths::findDictionaries(roots),
26
3/3
✓ Branch 8 → 9 taken 102 times.
✓ Branch 9 → 10 taken 102 times.
✓ Branch 11 → 12 taken 102 times.
306 QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QStringLiteral("/spelling"));
27
1/10
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 39 taken 102 times.
✗ Branch 19 → 20 not taken.
✗ Branch 23 → 24 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 26 → 27 not taken.
✗ Branch 27 → 28 not taken.
✗ Branch 29 → 30 not taken.
✗ Branch 37 → 21 not taken.
✗ Branch 37 → 38 not taken.
102 if (!linked.isEmpty()) qCInfo(lcCompose) << "dictionaries of other programs" << linked << "from" << roots;
28
2/2
✓ Branch 40 → 41 taken 102 times.
✓ Branch 41 → 42 taken 102 times.
102 m_languages = Sonnet::Speller().availableLanguages();
29
1/1
✓ Branch 46 → 47 taken 102 times.
102 m_languages.sort();
30 #endif
31
6/6
✓ Branch 47 → 48 taken 102 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 54 → 55 taken 1 time.
✓ Branch 58 → 49 taken 1 time.
✓ Branch 58 → 59 taken 102 times.
103 qCInfo(lcCompose) << "spell checking languages" << m_languages.size();
32 102 return m_languages;
33 102 }
34
35 795 SpellService::~SpellService() = default;
36
37 2128 QVariantList SpellService::languageChoices() const {
38 // Each language in its own words ("Deutsch", "American English"); the
39 // country only when two dictionaries would read the same.
40 2128 QList<std::pair<QString, QString>> named;
41 2128 QHash<QString, int> uses;
42
5/5
✓ Branch 2 → 3 taken 2128 times.
✓ Branch 3 → 4 taken 2128 times.
✓ Branch 4 → 5 taken 2128 times.
✓ Branch 26 → 6 taken 8512 times.
✓ Branch 26 → 27 taken 2128 times.
10640 for (const QString &code : languages()) {
43
1/1
✓ Branch 7 → 8 taken 8512 times.
8512 const QLocale locale(code);
44
3/4
✓ Branch 8 → 9 taken 8512 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 8512 times.
✓ Branch 11 → 12 taken 8512 times.
8512 QString name = locale.language() == QLocale::C ? code : locale.nativeLanguageName();
45
3/4
✓ Branch 13 → 14 taken 8512 times.
✗ Branch 13 → 18 not taken.
✓ Branch 14 → 15 taken 8512 times.
✓ Branch 16 → 17 taken 8512 times.
8512 if (!name.isEmpty()) name[0] = name[0].toUpper();
46
1/1
✓ Branch 19 → 20 taken 8512 times.
8512 named.append({code, name});
47
1/1
✓ Branch 21 → 22 taken 8512 times.
8512 ++uses[name];
48 10640 }
49 2128 QVariantList out;
50
4/4
✓ Branch 28 → 29 taken 2128 times.
✓ Branch 29 → 30 taken 2128 times.
✓ Branch 84 → 31 taken 8512 times.
✓ Branch 84 → 85 taken 2128 times.
10640 for (const auto &[code, name] : named) {
51
1/1
✓ Branch 34 → 35 taken 8512 times.
8512 const QLocale locale(code);
52
2/3
✓ Branch 37 → 38 taken 6384 times.
✓ Branch 38 → 39 taken 6384 times.
✗ Branch 38 → 49 not taken.
6384 const QString shown = uses.value(name) > 1 && locale.language() != QLocale::C
53
15/25
✓ Branch 36 → 37 taken 6384 times.
✓ Branch 36 → 49 taken 2128 times.
✓ Branch 41 → 42 taken 6384 times.
✓ Branch 45 → 46 taken 6384 times.
✓ Branch 46 → 47 taken 6384 times.
✓ Branch 50 → 51 taken 6384 times.
✓ Branch 50 → 52 taken 2128 times.
✓ Branch 52 → 53 taken 6384 times.
✓ Branch 52 → 54 taken 2128 times.
✓ Branch 54 → 55 taken 6384 times.
✓ Branch 54 → 56 taken 2128 times.
✓ Branch 56 → 57 taken 6384 times.
✓ Branch 56 → 58 taken 2128 times.
✓ Branch 58 → 59 taken 6384 times.
✓ Branch 58 → 60 taken 2128 times.
✗ Branch 100 → 101 not taken.
✗ Branch 100 → 102 not taken.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 112 → 114 not taken.
✗ Branch 116 → 117 not taken.
✗ Branch 116 → 118 not taken.
29792 ? name + QStringLiteral(" (") + locale.nativeTerritoryName() + QLatin1Char(')')
54
1/1
✓ Branch 47 → 48 taken 6384 times.
17024 : name;
55
3/5
✓ Branch 70 → 71 taken 8512 times.
✓ Branch 74 → 75 taken 17024 times.
✓ Branch 74 → 76 taken 8512 times.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
42560 out.append(QVariantMap{{QStringLiteral("code"), code}, {QStringLiteral("name"), shown}});
56 8512 }
57 2128 return out;
58 27664 }
59
60 1911 QString SpellService::bestLanguage(const QString &preferred) const {
61
2/3
✓ Branch 2 → 3 taken 1911 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1911 times.
1911 if (languages().isEmpty()) return {};
62 1911 QString wanted = preferred;
63
1/1
✓ Branch 12 → 13 taken 1911 times.
1911 wanted.replace(QLatin1Char('-'), QLatin1Char('_'));
64
2/2
✓ Branch 14 → 15 taken 169 times.
✓ Branch 14 → 16 taken 1742 times.
1911 if (m_languages.contains(wanted)) return wanted;
65
2/2
✓ Branch 19 → 20 taken 1742 times.
✓ Branch 20 → 21 taken 1742 times.
1742 const QString base = wanted.section(QLatin1Char('_'), 0, 0).toLower();
66 // The base language with its usual country first (de → de_DE, en → en_US).
67
7/9
✓ Branch 24 → 25 taken 871 times.
✓ Branch 24 → 28 taken 871 times.
✓ Branch 28 → 29 taken 871 times.
✓ Branch 31 → 32 taken 1742 times.
✓ Branch 32 → 33 taken 1742 times.
✓ Branch 35 → 36 taken 871 times.
✓ Branch 35 → 37 taken 871 times.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
2613 const QString usual = base + QLatin1Char('_') + (base == QLatin1String("en") ? QStringLiteral("US") : base.toUpper());
68
2/2
✓ Branch 38 → 39 taken 919 times.
✓ Branch 38 → 40 taken 823 times.
1742 if (m_languages.contains(usual)) return usual;
69
4/4
✓ Branch 40 → 41 taken 823 times.
✓ Branch 41 → 42 taken 823 times.
✓ Branch 56 → 43 taken 3292 times.
✓ Branch 56 → 57 taken 823 times.
4115 for (const QString &code : m_languages)
70
3/4
✓ Branch 47 → 48 taken 3292 times.
✓ Branch 48 → 49 taken 3292 times.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 3292 times.
3292 if (code.section(QLatin1Char('_'), 0, 0).toLower() == base) return code;
71 823 return {};
72 1911 }
73
74 493 Sonnet::Speller *SpellService::speller(const QString &language) {
75 #ifdef MAILJD_HAVE_SONNET
76
2/3
✓ Branch 2 → 3 taken 493 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 493 times.
493 if (!languages().contains(language)) return nullptr;
77 493 auto &slot = m_spellers[language];
78
3/3
✓ Branch 9 → 10 taken 62 times.
✓ Branch 9 → 14 taken 431 times.
✓ Branch 10 → 11 taken 62 times.
493 if (!slot) slot = std::make_shared<Sonnet::Speller>(language);
79
1/2
✓ Branch 16 → 17 taken 493 times.
✗ Branch 16 → 18 not taken.
493 return slot->isValid() ? slot.get() : nullptr;
80 #else
81 Q_UNUSED(language)
82 return nullptr;
83 #endif
84 }
85
86 3038 bool SpellService::isMisspelled(const QString &word, const QString &language) {
87 #ifdef MAILJD_HAVE_SONNET
88 // G11: text repeats its words; a Hunspell lookup costs about 3 µs, a known
89 // word a hash lookup. Bounded: the cache of a language starts afresh at
90 // KnownWordsLimit words.
91
1/1
✓ Branch 2 → 3 taken 3038 times.
3038 auto &known = m_known[language];
92
2/2
✓ Branch 6 → 7 taken 2565 times.
✓ Branch 6 → 9 taken 473 times.
3038 if (const auto it = known.constFind(word); it != known.constEnd()) return it.value();
93
1/1
✓ Branch 9 → 11 taken 473 times.
473 auto *checker = speller(language);
94
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 473 times.
473 if (!checker) return false;
95
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 473 times.
473 if (known.size() >= KnownWordsLimit) known.clear();
96
1/1
✓ Branch 16 → 17 taken 473 times.
473 const bool misspelled = checker->isMisspelled(word);
97
1/1
✓ Branch 17 → 18 taken 473 times.
473 known.insert(word, misspelled);
98 473 return misspelled;
99 #else
100 Q_UNUSED(word)
101 Q_UNUSED(language)
102 #endif
103 return false;
104 }
105
106 20 QStringList SpellService::suggestions(const QString &word, const QString &language, int maximum) {
107 #ifdef MAILJD_HAVE_SONNET
108
3/4
✓ Branch 3 → 4 taken 20 times.
✗ Branch 3 → 10 not taken.
✓ Branch 4 → 5 taken 20 times.
✓ Branch 6 → 7 taken 20 times.
20 if (auto *checker = speller(language)) return checker->suggest(word).mid(0, qMax(0, maximum));
109 #else
110 Q_UNUSED(word)
111 Q_UNUSED(language)
112 Q_UNUSED(maximum)
113 #endif
114 ✗ return {};
115 }
116
117 ✗ void SpellService::addToDictionary(const QString &word, const QString &language) {
118 #ifdef MAILJD_HAVE_SONNET
119 ✗ if (auto *checker = speller(language)) checker->addToPersonal(word);
120 ✗ m_known[language].remove(word);
121 #else
122 Q_UNUSED(word)
123 Q_UNUSED(language)
124 #endif
125 ✗ }
126
127 #ifdef MAILJD_HAVE_SONNET
128 namespace {
129 // Two words are enough for Sonnet (it also asks the dictionaries); one word
130 // ("Hi", "ok") belongs to too many languages (F108-23).
131 28 bool hasTwoWords(const QString &text) {
132
4/7
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 15 taken 25 times.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
31 static const QRegularExpression word(QStringLiteral("[\\p{L}']{2,}"));
133
1/1
✓ Branch 16 → 17 taken 28 times.
28 auto words = word.globalMatch(text);
134 28 int count = 0;
135
7/7
✓ Branch 21 → 22 taken 52 times.
✓ Branch 21 → 25 taken 24 times.
✓ Branch 22 → 23 taken 52 times.
✓ Branch 23 → 24 taken 48 times.
✓ Branch 23 → 25 taken 4 times.
✓ Branch 26 → 18 taken 48 times.
✓ Branch 26 → 27 taken 28 times.
76 while (count < 2 && words.hasNext()) {
136
1/1
✓ Branch 18 → 19 taken 48 times.
48 words.next();
137 48 ++count;
138 }
139 28 return count >= 2;
140 28 }
141 } // namespace
142 #endif
143
144 28 QString SpellService::guess(const QString &text) const {
145 #ifdef MAILJD_HAVE_SONNET
146
7/8
✓ Branch 2 → 3 taken 28 times.
✓ Branch 3 → 4 taken 28 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 28 times.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 24 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 24 times.
28 if (!available() || !hasTwoWords(text)) return {};
147
1/1
✓ Branch 10 → 11 taken 24 times.
24 Sonnet::GuessLanguage guesser;
148
3/3
✓ Branch 11 → 12 taken 24 times.
✓ Branch 12 → 13 taken 24 times.
✓ Branch 13 → 14 taken 24 times.
24 const QString found = guesser.identify(text.left(1000), languages());
149 // The language, with its usual country (de_BE → de_DE), as bestLanguage.
150
4/8
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 24 times.
✓ Branch 22 → 23 taken 24 times.
✓ Branch 23 → 24 taken 24 times.
✓ Branch 25 → 26 taken 24 times.
✗ Branch 25 → 27 not taken.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
48 return found.isEmpty() ? QString() : bestLanguage(found.section(QLatin1Char('_'), 0, 0));
151 #else
152 Q_UNUSED(text)
153 return {};
154 #endif
155 24 }
156