| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2016 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the Qt Linguist of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | #if CHECK_SIMTEXTH |
| 30 | #include "../shared/simtexth.h" |
| 31 | #endif |
| 32 | |
| 33 | #include <QtCore/QDir> |
| 34 | #include <QtCore/QDebug> |
| 35 | #include <QtCore/QFile> |
| 36 | #include <QtCore/QByteArray> |
| 37 | |
| 38 | #include <QtTest/QtTest> |
| 39 | |
| 40 | class tst_lupdate : public QObject |
| 41 | { |
| 42 | Q_OBJECT |
| 43 | public: |
| 44 | tst_lupdate(); |
| 45 | |
| 46 | private slots: |
| 47 | void good_data(); |
| 48 | void good(); |
| 49 | #if CHECK_SIMTEXTH |
| 50 | void simtexth(); |
| 51 | void simtexth_data(); |
| 52 | #endif |
| 53 | |
| 54 | private: |
| 55 | QString m_cmdLupdate; |
| 56 | QString m_basePath; |
| 57 | |
| 58 | void doCompare(QStringList actual, const QString &expectedFn, bool err); |
| 59 | void doCompare(const QString &actualFn, const QString &expectedFn, bool err); |
| 60 | }; |
| 61 | |
| 62 | |
| 63 | tst_lupdate::tst_lupdate() |
| 64 | { |
| 65 | QString binPath = QLibraryInfo::location(QLibraryInfo::BinariesPath); |
| 66 | m_cmdLupdate = binPath + QLatin1String("/lupdate" ); |
| 67 | m_basePath = QFINDTESTDATA("testdata/" ); |
| 68 | } |
| 69 | |
| 70 | static bool prepareMatch(const QString &expect, QString *tmpl, int *require, int *accept) |
| 71 | { |
| 72 | if (expect.startsWith(c: QLatin1Char('\\'))) { |
| 73 | *tmpl = expect.mid(position: 1); |
| 74 | *require = *accept = 1; |
| 75 | } else if (expect.startsWith(c: QLatin1Char('?'))) { |
| 76 | *tmpl = expect.mid(position: 1); |
| 77 | *require = 0; |
| 78 | *accept = 1; |
| 79 | } else if (expect.startsWith(c: QLatin1Char('*'))) { |
| 80 | *tmpl = expect.mid(position: 1); |
| 81 | *require = 0; |
| 82 | *accept = INT_MAX; |
| 83 | } else if (expect.startsWith(c: QLatin1Char('+'))) { |
| 84 | *tmpl = expect.mid(position: 1); |
| 85 | *require = 1; |
| 86 | *accept = INT_MAX; |
| 87 | } else if (expect.startsWith(c: QLatin1Char('{'))) { |
| 88 | int brc = expect.indexOf(c: QLatin1Char('}'), from: 1); |
| 89 | if (brc < 0) |
| 90 | return false; |
| 91 | *tmpl = expect.mid(position: brc + 1); |
| 92 | QString sub = expect.mid(position: 1, n: brc - 1); |
| 93 | int com = sub.indexOf(c: QLatin1Char(',')); |
| 94 | bool ok; |
| 95 | if (com < 0) { |
| 96 | *require = *accept = sub.toInt(ok: &ok); |
| 97 | return ok; |
| 98 | } else { |
| 99 | *require = sub.left(n: com).toInt(); |
| 100 | *accept = sub.mid(position: com + 1).toInt(ok: &ok); |
| 101 | if (!ok) |
| 102 | *accept = INT_MAX; |
| 103 | return *accept >= *require; |
| 104 | } |
| 105 | } else { |
| 106 | *tmpl = expect; |
| 107 | *require = *accept = 1; |
| 108 | } |
| 109 | return true; |
| 110 | } |
| 111 | |
| 112 | void tst_lupdate::doCompare(QStringList actual, const QString &expectedFn, bool err) |
| 113 | { |
| 114 | QFile file(expectedFn); |
| 115 | QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text), qPrintable(expectedFn)); |
| 116 | QStringList expected = QString(file.readAll()).split(sep: '\n'); |
| 117 | |
| 118 | for (int i = actual.size() - 1; i >= 0; --i) { |
| 119 | if (actual.at(i).startsWith(s: QLatin1String("Info: creating stash file " ))) |
| 120 | actual.removeAt(i); |
| 121 | } |
| 122 | |
| 123 | int ei = 0, ai = 0, em = expected.size(), am = actual.size(); |
| 124 | int oei = 0, oai = 0, oem = em, oam = am; |
| 125 | int require = 0, accept = 0; |
| 126 | QString tmpl; |
| 127 | forever { |
| 128 | if (!accept) { |
| 129 | oei = ei, oai = ai; |
| 130 | if (ei == em) { |
| 131 | if (ai == am) |
| 132 | return; |
| 133 | break; |
| 134 | } |
| 135 | if (!prepareMatch(expect: expected.at(i: ei++), tmpl: &tmpl, require: &require, accept: &accept)) |
| 136 | QFAIL(qPrintable(QString("Malformed expected %1 at %3:%2" ) |
| 137 | .arg(err ? "output" : "result" ).arg(ei).arg(expectedFn))); |
| 138 | } |
| 139 | if (ai == am) { |
| 140 | if (require <= 0) { |
| 141 | accept = 0; |
| 142 | continue; |
| 143 | } |
| 144 | break; |
| 145 | } |
| 146 | if (err ? !QRegExp(tmpl).exactMatch(str: actual.at(i: ai)) : (actual.at(i: ai) != tmpl)) { |
| 147 | if (require <= 0) { |
| 148 | accept = 0; |
| 149 | continue; |
| 150 | } |
| 151 | ei--; |
| 152 | require = accept = 0; |
| 153 | forever { |
| 154 | if (!accept) { |
| 155 | oem = em, oam = am; |
| 156 | if (ei == em) |
| 157 | break; |
| 158 | if (!prepareMatch(expect: expected.at(i: --em), tmpl: &tmpl, require: &require, accept: &accept)) |
| 159 | QFAIL(qPrintable(QString("Malformed expected %1 at %3:%2" ) |
| 160 | .arg(err ? "output" : "result" ) |
| 161 | .arg(em + 1).arg(expectedFn))); |
| 162 | } |
| 163 | if (ai == am || (err ? !QRegExp(tmpl).exactMatch(str: actual.at(i: am - 1)) : |
| 164 | (actual.at(i: am - 1) != tmpl))) { |
| 165 | if (require <= 0) { |
| 166 | accept = 0; |
| 167 | continue; |
| 168 | } |
| 169 | break; |
| 170 | } |
| 171 | accept--; |
| 172 | require--; |
| 173 | am--; |
| 174 | } |
| 175 | break; |
| 176 | } |
| 177 | accept--; |
| 178 | require--; |
| 179 | ai++; |
| 180 | } |
| 181 | QString diff; |
| 182 | for (int j = qMax(a: 0, b: oai - 3); j < oai; j++) |
| 183 | diff += actual.at(i: j) + '\n'; |
| 184 | diff += "<<<<<<< got\n" ; |
| 185 | for (int j = oai; j < oam; j++) { |
| 186 | diff += actual.at(i: j) + '\n'; |
| 187 | if (j >= oai + 5) { |
| 188 | diff += "...\n" ; |
| 189 | break; |
| 190 | } |
| 191 | } |
| 192 | diff += "=========\n" ; |
| 193 | for (int j = oei; j < oem; j++) { |
| 194 | diff += expected.at(i: j) + '\n'; |
| 195 | if (j >= oei + 5) { |
| 196 | diff += "...\n" ; |
| 197 | break; |
| 198 | } |
| 199 | } |
| 200 | diff += ">>>>>>> expected\n" ; |
| 201 | for (int j = oam; j < qMin(a: oam + 3, b: actual.size()); j++) |
| 202 | diff += actual.at(i: j) + '\n'; |
| 203 | QFAIL(qPrintable((err ? "Output for " : "Result for " ) + expectedFn + " does not meet expectations:\n" + diff)); |
| 204 | } |
| 205 | |
| 206 | void tst_lupdate::doCompare(const QString &actualFn, const QString &expectedFn, bool err) |
| 207 | { |
| 208 | QFile afile(actualFn); |
| 209 | QVERIFY2(afile.open(QIODevice::ReadOnly | QIODevice::Text), qPrintable(actualFn)); |
| 210 | QStringList actual = QString(afile.readAll()).split(sep: '\n'); |
| 211 | |
| 212 | doCompare(actual, expectedFn, err); |
| 213 | } |
| 214 | |
| 215 | void tst_lupdate::good_data() |
| 216 | { |
| 217 | QTest::addColumn<QString>(name: "directory" ); |
| 218 | |
| 219 | QDir parsingDir(m_basePath + "good" ); |
| 220 | QStringList dirs = parsingDir.entryList(filters: QDir::Dirs | QDir::NoDotAndDotDot, sort: QDir::Name); |
| 221 | |
| 222 | #ifndef Q_OS_WIN |
| 223 | dirs.removeAll(t: QLatin1String("backslashes" )); |
| 224 | #endif |
| 225 | #ifndef Q_OS_MACOS |
| 226 | dirs.removeAll(t: QLatin1String("parseobjc" )); |
| 227 | #endif |
| 228 | |
| 229 | for (const QString &dir : qAsConst(t&: dirs)) |
| 230 | QTest::newRow(dataTag: dir.toLocal8Bit()) << dir; |
| 231 | } |
| 232 | |
| 233 | void tst_lupdate::good() |
| 234 | { |
| 235 | QFETCH(QString, directory); |
| 236 | |
| 237 | QString dir = m_basePath + "good/" + directory; |
| 238 | |
| 239 | qDebug() << "Checking..." ; |
| 240 | |
| 241 | QString workDir = dir; |
| 242 | QStringList generatedtsfiles(QLatin1String("project.ts" )); |
| 243 | QStringList lupdateArguments; |
| 244 | |
| 245 | QFile file(dir + "/lupdatecmd" ); |
| 246 | if (file.exists()) { |
| 247 | QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text), qPrintable(file.fileName())); |
| 248 | while (!file.atEnd()) { |
| 249 | QByteArray cmdstring = file.readLine().simplified(); |
| 250 | if (cmdstring.startsWith(c: '#')) |
| 251 | continue; |
| 252 | if (cmdstring.startsWith(c: "lupdate" )) { |
| 253 | for (auto argument : cmdstring.mid(index: 8).simplified().split(sep: ' ')) |
| 254 | lupdateArguments += argument; |
| 255 | break; |
| 256 | } else if (cmdstring.startsWith(c: "TRANSLATION:" )) { |
| 257 | cmdstring.remove(index: 0, len: 12); |
| 258 | generatedtsfiles.clear(); |
| 259 | const auto parts = cmdstring.split(sep: ' '); |
| 260 | for (const QByteArray &s : parts) |
| 261 | if (!s.isEmpty()) |
| 262 | generatedtsfiles << s; |
| 263 | } else if (cmdstring.startsWith(c: "cd " )) { |
| 264 | cmdstring.remove(index: 0, len: 3); |
| 265 | workDir = QDir::cleanPath(path: dir + QLatin1Char('/') + cmdstring); |
| 266 | } |
| 267 | } |
| 268 | file.close(); |
| 269 | } |
| 270 | |
| 271 | for (const QString &ts : qAsConst(t&: generatedtsfiles)) { |
| 272 | QString genTs = workDir + QLatin1Char('/') + ts; |
| 273 | QFile::remove(fileName: genTs); |
| 274 | QString beforetsfile = dir + QLatin1Char('/') + ts + QLatin1String(".before" ); |
| 275 | if (QFile::exists(fileName: beforetsfile)) |
| 276 | QVERIFY2(QFile::copy(beforetsfile, genTs), qPrintable(beforetsfile)); |
| 277 | } |
| 278 | |
| 279 | file.setFileName(workDir + QStringLiteral("/.qmake.cache" )); |
| 280 | QVERIFY(file.open(QIODevice::WriteOnly)); |
| 281 | file.close(); |
| 282 | |
| 283 | if (lupdateArguments.isEmpty()) |
| 284 | lupdateArguments.append(t: QLatin1String("project.pro" )); |
| 285 | lupdateArguments.prepend(t: "-silent" ); |
| 286 | |
| 287 | QProcess proc; |
| 288 | proc.setWorkingDirectory(workDir); |
| 289 | proc.setProcessChannelMode(QProcess::MergedChannels); |
| 290 | const QString command = m_cmdLupdate + ' ' + lupdateArguments.join(sep: ' '); |
| 291 | proc.start(program: m_cmdLupdate, arguments: lupdateArguments, mode: QIODevice::ReadWrite | QIODevice::Text); |
| 292 | QVERIFY2(proc.waitForStarted(), qPrintable(command + QLatin1String(" :" ) + proc.errorString())); |
| 293 | QVERIFY2(proc.waitForFinished(30000), qPrintable(command)); |
| 294 | const QString output = QString::fromLocal8Bit(str: proc.readAll()); |
| 295 | QVERIFY2(proc.exitStatus() == QProcess::NormalExit, |
| 296 | qPrintable(QLatin1Char('"') + command + "\" crashed\n" + output)); |
| 297 | QVERIFY2(!proc.exitCode(), |
| 298 | qPrintable(QLatin1Char('"') + command + "\" exited with code " + |
| 299 | QString::number(proc.exitCode()) + '\n' + output)); |
| 300 | |
| 301 | // If the file expectedoutput.txt exists, compare the |
| 302 | // console output with the content of that file |
| 303 | QFile outfile(dir + "/expectedoutput.txt" ); |
| 304 | if (outfile.exists()) { |
| 305 | QStringList errslist = output.split(sep: QLatin1Char('\n')); |
| 306 | doCompare(actual: errslist, expectedFn: outfile.fileName(), err: true); |
| 307 | if (QTest::currentTestFailed()) |
| 308 | return; |
| 309 | } |
| 310 | |
| 311 | for (const QString &ts : qAsConst(t&: generatedtsfiles)) |
| 312 | doCompare(actualFn: workDir + QLatin1Char('/') + ts, |
| 313 | expectedFn: dir + QLatin1Char('/') + ts + QLatin1String(".result" ), err: false); |
| 314 | } |
| 315 | |
| 316 | #if CHECK_SIMTEXTH |
| 317 | void tst_lupdate::simtexth() |
| 318 | { |
| 319 | QFETCH(QString, one); |
| 320 | QFETCH(QString, two); |
| 321 | QFETCH(int, expected); |
| 322 | |
| 323 | int measured = getSimilarityScore(one, two.toLatin1()); |
| 324 | QCOMPARE(measured, expected); |
| 325 | } |
| 326 | |
| 327 | |
| 328 | void tst_lupdate::simtexth_data() |
| 329 | { |
| 330 | using namespace QTest; |
| 331 | |
| 332 | addColumn<QString>("one" ); |
| 333 | addColumn<QString>("two" ); |
| 334 | addColumn<int>("expected" ); |
| 335 | |
| 336 | newRow("00" ) << "" << "" << 1024; |
| 337 | newRow("01" ) << "a" << "a" << 1024; |
| 338 | newRow("02" ) << "ab" << "ab" << 1024; |
| 339 | newRow("03" ) << "abc" << "abc" << 1024; |
| 340 | newRow("04" ) << "abcd" << "abcd" << 1024; |
| 341 | } |
| 342 | #endif |
| 343 | |
| 344 | QTEST_MAIN(tst_lupdate) |
| 345 | #include "tst_lupdate.moc" |
| 346 | |