Revision control

Copy as Markdown

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QByteArray>
#include <QDir>
#include <QDebug>
#include <QFile>
#include <QLibraryInfo>
#include <QProcess>
#include <QTest>
using namespace Qt::Literals::StringLiterals;
class tst_lcheck : public QObject
{
Q_OBJECT
public:
tst_lcheck()
: lcheck(QLibraryInfo::path(QLibraryInfo::BinariesPath) + "/lcheck"),
dataDir(QFINDTESTDATA("testdata/"))
{
}
private:
static QStringList extractValidationLines(const QByteArray &ba);
private slots:
void invalidTranslations();
void validTranslations();
void allViolationsReported();
void allPluralViolationsReported();
void disabledPlaceMarkerWithPluralDoesNotCrash();
void checkFinished_defaultDisabled();
void checkFinished_enabled();
void disableAcceleratorCheck();
void disableWhitespaceCheck();
void disablePunctuationCheck();
void disablePlaceMarkerCheck();
void outputToFile_optionWritesReport();
private:
void doCompare(const QStringList &actual, const QString &expectedFn);
QString lcheck;
QString dataDir;
};
void tst_lcheck::doCompare(const QStringList &actual, const QString &expectedFn)
{
QFile file(expectedFn);
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
QStringList expected = QString(file.readAll()).trimmed().split('\n');
int i = 0, ei = expected.size(), gi = actual.size();
for (;; i++) {
if (i == gi) {
if (i == ei)
return;
gi = 0;
break;
} else if (i == ei) {
ei = 0;
break;
} else if (!QRegularExpression(QRegularExpression::anchoredPattern(expected.at(i)))
.match(actual.at(i))
.hasMatch()) {
while ((ei - 1) >= i && (gi - 1) >= i
&& (QRegularExpression(QRegularExpression::anchoredPattern(expected.at(ei - 1)))
.match(actual.at(gi - 1)))
.hasMatch())
ei--, gi--;
break;
}
}
QString diff;
for (int j = qMax(0, i - 3); j < i; j++)
diff += expected.at(j) + '\n';
diff += "<<<<<<< got\n";
for (int j = i; j < gi; j++) {
diff += actual.at(j) + '\n';
if (j >= i + 5) {
diff += "...\n";
break;
}
}
diff += "=========\n";
for (int j = i; j < ei; j++) {
diff += expected.at(j) + '\n';
if (j >= i + 5) {
diff += "...\n";
break;
}
}
diff += ">>>>>>> expected\n";
for (int j = ei; j < qMin(ei + 3, expected.size()); j++)
diff += expected.at(j) + '\n';
QFAIL(qPrintable("Output for " + expectedFn + " does not meet expectations:\n" + diff));
}
QStringList tst_lcheck::extractValidationLines(const QByteArray &ba)
{
const QString out = QString::fromUtf8(ba);
QStringList actual;
for (const QString &line : out.split('\n')) {
const QString trimmed = line.trimmed();
if (!trimmed.isEmpty())
actual << trimmed;
}
return actual;
}
void tst_lcheck::invalidTranslations()
{
QProcess proc;
proc.start(lcheck, { dataDir + "invalid_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
auto stderrOutput = proc.readAllStandardError();
QVERIFY(stderrOutput.contains("Translation: Text nicht &gefunden"));
QVERIFY(stderrOutput.contains("Translation: Text gefunden %1"));
}
void tst_lcheck::validTranslations()
{
QProcess proc;
proc.start(lcheck, { dataDir + "valid_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 0);
const QByteArray err = proc.readAllStandardError();
QVERIFY(!err.contains("Translation: "));
}
// Regression test for QTBUG-148562: all violations of the same check across
// multiple messages must be reported, not collapsed into one.
void tst_lcheck::allViolationsReported()
{
QProcess proc;
proc.start(lcheck, { dataDir + "multiple_same_check_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
const QByteArray err = proc.readAllStandardError();
QCOMPARE(static_cast<int>(err.count("Translation does not end with the same punctuation")), 3);
QVERIFY(err.contains("Translation: Hallo"));
QVERIFY(err.contains("Translation: Tschuess"));
QVERIFY(err.contains("Translation: Willkommen"));
}
// Regression test for QTBUG-148562: all numerus forms of a plural message that
// fail the same check must be reported.
void tst_lcheck::allPluralViolationsReported()
{
QProcess proc;
proc.start(lcheck, { dataDir + "plural_same_check_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
const QByteArray err = proc.readAllStandardError();
QCOMPARE(static_cast<int>(err.count("Translation does not end with the same punctuation")), 2);
QVERIFY(err.contains("Translation: %n Datei"));
QVERIFY(err.contains("Translation: %n Dateien"));
}
// --no-place-marker skips gathering numerus info, leaving countRefNeeds empty.
// A plural message must not make the validator index that empty list.
void tst_lcheck::disabledPlaceMarkerWithPluralDoesNotCrash()
{
QProcess proc;
proc.start(lcheck, { "--no-place-marker", dataDir + "plural_same_check_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
QCOMPARE(static_cast<int>(proc.readAllStandardError().count(
"Translation does not end with the same punctuation")),
2);
}
void tst_lcheck::checkFinished_defaultDisabled()
{
QProcess proc;
proc.start(lcheck, { dataDir + "finished_issues_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 0);
const QByteArray err = proc.readAllStandardError();
QVERIFY(!err.contains("Translation: Text nicht gefunden"));
QVERIFY(!err.contains("Translation: Beenden"));
}
void tst_lcheck::checkFinished_enabled()
{
QProcess proc;
proc.start(lcheck, { "--check-finished", dataDir + "finished_issues_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
const QByteArray err = proc.readAllStandardError();
QVERIFY(err.contains("Translation: Text nicht gefunden"));
QVERIFY(err.contains("Translation: Beenden"));
}
void tst_lcheck::disableAcceleratorCheck()
{
{
QProcess proc;
proc.start(lcheck, { dataDir + "accelerator_mismatch_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
QVERIFY(proc.readAllStandardError().contains("Translation: Beenden"));
}
{
QProcess proc;
proc.start(lcheck, { "--no-accelerator", dataDir + "accelerator_mismatch_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 0);
}
}
void tst_lcheck::disableWhitespaceCheck()
{
{
QProcess proc;
proc.start(lcheck, { dataDir + "whitespace_mismatch_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
QVERIFY(proc.readAllStandardError().contains("Translation: Bereit "));
}
{
QProcess proc;
proc.start(lcheck, { "--no-whitespaces", dataDir + "whitespace_mismatch_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 0);
}
}
void tst_lcheck::disablePunctuationCheck()
{
{
QProcess proc;
proc.start(lcheck, { dataDir + "punctuation_mismatch_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
QVERIFY(proc.readAllStandardError().contains("Translation: Bereit"));
}
{
QProcess proc;
proc.start(lcheck, { "--no-punctuation", dataDir + "punctuation_mismatch_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 0);
}
}
void tst_lcheck::disablePlaceMarkerCheck()
{
{
QProcess proc;
proc.start(lcheck, { dataDir + "placemarker_mismatch_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
QVERIFY(proc.readAllStandardError().contains("Translation: Gefunden %1 Elemente"));
}
{
QProcess proc;
proc.start(lcheck, { "--no-place-marker", dataDir + "placemarker_mismatch_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 0);
}
}
void tst_lcheck::outputToFile_optionWritesReport()
{
QTemporaryDir tmp;
QVERIFY(tmp.isValid());
const QString report = tmp.path() + "/report.txt";
QProcess proc;
proc.start(lcheck, { "-o", report, dataDir + "invalid_de.ts" });
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc.exitCode(), 1);
const QByteArray err = proc.readAllStandardError();
QVERIFY(!err.contains("Translation: "));
QFile f(report);
QVERIFY(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QByteArray fileData = f.readAll();
QVERIFY(!fileData.isEmpty());
const QStringList actual = extractValidationLines(fileData);
doCompare(actual, dataDir + "expected_invalid_report.txt");
}
QTEST_MAIN(tst_lcheck)
#include "tst_lcheck.moc"