Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#if CHECK_SIMTEXTH
#include "../shared/simtexth.h"
#endif
#include <QtCore/QByteArray>
#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QElapsedTimer>
#include <QtCore/private/qconfig_p.h>
#include <QtCore/QSet>
#include <QtCore/QSysInfo>
#include <QtTest/QtTest>
#include <QtTools/private/qttools-config_p.h>
#include <algorithm>
#include <limits>
using namespace Qt::Literals::StringLiterals;
static constexpr int TIMEOUT = 120000;
class tst_lupdate : public QObject
{
Q_OBJECT
public:
tst_lupdate();
private slots:
void cleanupTestCase();
void good_data();
void good();
#if CHECK_SIMTEXTH
void simtexth();
void simtexth_data();
#endif
private:
QString m_cmdLupdate;
QString m_cmdLupdatePro;
QString m_basePath;
QElapsedTimer m_timer;
qint64 m_maxElapsed = -1;
static void doCompare(QList<QStringView> actual, const QString &expectedFn, bool err);
void doCompare(const QString &actualFn, const QString &expectedFn, bool err);
};
tst_lupdate::tst_lupdate()
{
m_timer.start();
QString binPath = QLibraryInfo::path(QLibraryInfo::BinariesPath);
m_cmdLupdate = binPath + "/lupdate"_L1;
m_cmdLupdatePro = binPath + "/lupdate-pro"_L1;
m_basePath = QFINDTESTDATA("testdata/");
}
void tst_lupdate::cleanupTestCase()
{
if (m_maxElapsed > 0)
qInfo().noquote().nospace() << "max elapsed: " << m_maxElapsed << "ms";
}
static bool prepareMatch(QStringView expect, QString *tmpl, qsizetype *require, qsizetype *accept)
{
if (expect.startsWith(QLatin1Char('\\'))) {
*tmpl = expect.sliced(1).toString();
*require = *accept = 1;
} else if (expect.startsWith(u'?')) {
*tmpl = expect.sliced(1).toString();
*require = 0;
*accept = 1;
} else if (expect.startsWith(u'*')) {
*tmpl = expect.sliced(1).toString();
*require = 0;
*accept = std::numeric_limits<qsizetype>::max();
} else if (expect.startsWith(u'+')) {
*tmpl = expect.sliced(1).toString();
*require = 1;
*accept = std::numeric_limits<qsizetype>::max();
} else if (expect.startsWith(u'{')) {
const auto brc = expect.indexOf(u'}', 1);
if (brc < 0)
return false;
*tmpl = expect.sliced(brc + 1).toString();
auto sub = expect.sliced(1, brc - 1);
const auto com = sub.indexOf(u',');
bool ok{};
if (com < 0) {
*require = *accept = sub.toInt(&ok);
return ok;
}
*require = sub.left(com).toInt();
*accept = sub.sliced(com + 1).toInt(&ok);
if (!ok)
*accept = std::numeric_limits<qsizetype>::max();
return *accept >= *require;
} else {
*tmpl = expect.toString();
*require = *accept = 1;
}
return true;
}
static bool isStashMessage(QStringView v)
{
return v.startsWith("Info: creating stash file "_L1);
}
void tst_lupdate::doCompare(QList<QStringView> actual, const QString &expectedFn, bool err)
{
QFile file(expectedFn);
QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text), qPrintable(expectedFn));
const QString expectedS = QString::fromUtf8(file.readAll());
auto expected = QStringView{expectedS}.split(u'\n');
actual.erase(std::remove_if(actual.begin(), actual.end(), isStashMessage), actual.end());
qsizetype ei = 0, ai = 0, em = expected.size(), am = actual.size();
qsizetype oei = 0, oai = 0, oem = em, oam = am;
qsizetype require = 0, accept = 0;
QString tmpl;
forever {
if (!accept) {
oei = ei, oai = ai;
if (ei == em) {
if (ai == am)
return;
break;
}
if (!prepareMatch(expected.at(ei++), &tmpl, &require, &accept))
QFAIL(qPrintable(QString("Malformed expected %1 at %3:%2"_L1)
.arg(err ? "output"_L1 : "result"_L1).arg(ei).arg(expectedFn)));
}
if (ai == am) {
if (require <= 0) {
accept = 0;
continue;
}
break;
}
if (err
? !QRegularExpression(QRegularExpression::anchoredPattern(tmpl)).matchView(actual.at(ai)).hasMatch()
: (actual.at(ai) != tmpl)) {
if (require <= 0) {
accept = 0;
continue;
}
ei--;
require = accept = 0;
forever {
if (!accept) {
oem = em, oam = am;
if (ei == em)
break;
if (!prepareMatch(expected.at(--em), &tmpl, &require, &accept))
QFAIL(qPrintable(QString("Malformed expected %1 at %3:%2"_L1)
.arg(err ? "output"_L1 : "result"_L1)
.arg(em + 1).arg(expectedFn)));
}
if (ai == am || (err
? !QRegularExpression(QRegularExpression::anchoredPattern(tmpl)).matchView(actual.at(am - 1)).hasMatch()
: (actual.at(am - 1) != tmpl))) {
if (require <= 0) {
accept = 0;
continue;
}
break;
}
accept--;
require--;
am--;
}
break;
}
accept--;
require--;
ai++;
}
QString diff;
for (qsizetype j = qMax(qsizetype(0), oai - 3); j < oai; j++) {
diff += actual.at(j);
diff += u'\n';
}
diff += "<<<<<<< got\n"_L1;
for (qsizetype j = oai; j < oam; j++) {
diff += actual.at(j);
diff += u'\n';
if (j >= oai + 5) {
diff += "...\n"_L1;
break;
}
}
diff += "=========\n"_L1;
for (qsizetype j = oei; j < oem; j++) {
diff += expected.at(j);
diff += u'\n';
if (j >= oei + 5) {
diff += "...\n"_L1;
break;
}
}
diff += ">>>>>>> expected\n"_L1;
for (qsizetype j = oam; j < qMin(oam + 3, actual.size()); j++) {
diff += actual.at(j);
diff += u'\n';
}
QFAIL(qPrintable((err ? "Output for "_L1 : "Result for "_L1)
+ expectedFn + " does not meet expectations:\n"_L1 + diff));
}
void tst_lupdate::doCompare(const QString &actualFn, const QString &expectedFn, bool err)
{
QFile afile(actualFn);
QVERIFY2(afile.open(QIODevice::ReadOnly | QIODevice::Text), qPrintable(actualFn));
const QString actual = QString::fromUtf8(afile.readAll());
doCompare(QStringView{actual}.split(u'\n'), expectedFn, err);
}
void tst_lupdate::good_data()
{
QTest::addColumn<QString>("directory");
QDir parsingDir(m_basePath + "good"_L1);
QStringList dirs = parsingDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
#ifndef Q_OS_WIN
dirs.removeAll("backslashes"_L1);
#endif
#ifndef Q_OS_MACOS
dirs.removeAll("parseobjc"_L1);
#endif
for (const QString &dir : dirs) {
QTest::newRow(dir.toLocal8Bit().constData()) << dir;
}
}
static QByteArray msgStartFailed(const QProcess &process)
{
const QString result = u'"' + process.program() + u' ' + process.arguments().join(u' ')
+ "\": "_L1 + process.errorString();
return result.toLocal8Bit();
}
static QByteArray msgTimeout(const QProcess &process)
{
const QString result = u'"' + process.program() + u' ' + process.arguments().join(u' ')
+ "\" timed out: "_L1 + process.errorString();
return result.toLocal8Bit();
}
static QByteArray msgCrashed(const QProcess &process, const QByteArray &output)
{
const QString result = u'"' + process.program() + u' ' + process.arguments().join(u' ')
+ "\" crashed\n"_L1;
return result.toLocal8Bit() + output;
}
static QByteArray msgExitCode(const QProcess &process, const QByteArray &output)
{
const QString result = u'"' + process.program() + u' ' + process.arguments().join(u' ')
+ "\" exited with code "_L1 + QString::number(process.exitCode())
+ u'\n';
return result.toLocal8Bit() + output;
}
void tst_lupdate::good()
{
QFETCH(QString, directory);
QString dir = m_basePath + "good/"_L1 + directory;
QString workDir = dir;
QStringList generatedtsfiles("project.ts"_L1);
QStringList lupdateArguments;
QFile file(dir + "/lupdatecmd"_L1);
if (file.exists()) {
QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text), qPrintable(file.fileName()));
while (!file.atEnd()) {
QByteArray cmdstring = file.readLine().simplified();
if (cmdstring.startsWith(u'#'))
continue;
if (cmdstring.startsWith("lupdate"_L1)) {
for (auto argument : cmdstring.sliced(8).simplified().split(' '))
lupdateArguments += argument;
break;
}
if (cmdstring.startsWith("TRANSLATION:"_L1)) {
cmdstring.remove(0, 12);
generatedtsfiles.clear();
const auto parts = cmdstring.split(u' ');
for (const QByteArray &s : parts)
if (!s.isEmpty())
generatedtsfiles << QLatin1StringView(s);
} else if (cmdstring.startsWith("cd "_L1)) {
cmdstring.remove(0, 3);
workDir = QDir::cleanPath(dir + u'/' + QLatin1StringView(cmdstring));
}
}
file.close();
}
for (const QString &ts : std::as_const(generatedtsfiles)) {
QString genTs = workDir + u'/' + ts;
QFile::remove(genTs);
QString beforetsfile = dir + u'/' + ts + ".before"_L1;
if (QFile::exists(beforetsfile))
QVERIFY2(QFile::copy(beforetsfile, genTs), qPrintable(beforetsfile));
}
file.setFileName(workDir + "/.qmake.cache"_L1);
QVERIFY(file.open(QIODevice::WriteOnly));
file.close();
if (lupdateArguments.isEmpty()) {
// Automatically pass "project.pro" or "-project project.json".
if (QFile::exists(dir + u"/project.json"_s)) {
lupdateArguments << u"-project"_s << u"project.json"_s;
} else {
lupdateArguments.append("project.pro"_L1);
}
}
lupdateArguments.prepend("-silent"_L1);
QString command;
if (lupdateArguments.contains("project.pro"_L1)
|| lupdateArguments.contains("subdir1/subdir1.pro"_L1))
command = m_cmdLupdatePro;
else
command = m_cmdLupdate;
QProcess proc;
proc.setWorkingDirectory(workDir);
proc.setProcessChannelMode(QProcess::MergedChannels);
const auto startTime = m_timer.elapsed();
proc.start(command, lupdateArguments, QIODevice::ReadWrite | QIODevice::Text);
QVERIFY2(proc.waitForStarted(), msgStartFailed(proc).constData());
if (!proc.waitForFinished(TIMEOUT)) {
const auto message = msgTimeout(proc);
proc.kill();
proc.waitForFinished(50);
QFAIL(message.constData());
}
const auto elapsed = m_timer.elapsed() - startTime;
if (elapsed > m_maxElapsed)
m_maxElapsed = elapsed;
const QByteArray output = proc.readAll();
QVERIFY2(proc.exitStatus() == QProcess::NormalExit, msgCrashed(proc, output).constData());
QVERIFY2(proc.exitCode() == 0, msgExitCode(proc, output).constData());
qInfo().noquote().nospace() << elapsed << "ms";
// If the file expectedoutput.txt exists, compare the
// console output with the content of that file
QFile outfile(dir + "/expectedoutput.txt"_L1);
if (outfile.exists()) {
const QString errslist = QString::fromLocal8Bit(output);
doCompare(QStringView{errslist}.split(u'\n'), outfile.fileName(), true);
if (QTest::currentTestFailed())
return;
}
for (const QString &ts : std::as_const(generatedtsfiles)) {
doCompare(workDir + u'/' + ts,
dir + u'/' + ts + ".result"_L1, false);
}
}
#if CHECK_SIMTEXTH
void tst_lupdate::simtexth()
{
QFETCH(QString, one);
QFETCH(QString, two);
QFETCH(int, expected);
int measured = getSimilarityScore(one, two.toLatin1());
QCOMPARE(measured, expected);
}
void tst_lupdate::simtexth_data()
{
using namespace QTest;
addColumn<QString>("one");
addColumn<QString>("two");
addColumn<int>("expected");
newRow("00") << "" << "" << 1024;
newRow("01") << "a" << "a" << 1024;
newRow("02") << "ab" << "ab" << 1024;
newRow("03") << "abc" << "abc" << 1024;
newRow("04") << "abcd" << "abcd" << 1024;
}
#endif
QTEST_MAIN(tst_lupdate)
#include "tst_lupdate.moc"