mudlet/test/CMakeListsConsistencyTest.cpp
Mike Conley 9ae6df7561
Infrastructure: Make sure every source file is included in the build list (#9350)
#### Brief overview of PR changes/additions

A few of Mudlet's files had been accidentally left out of the build
list, and a couple were listed twice. This adds the missing ones,
removes the duplicates, and adds an automatic check that catches this
kind of slip-up from now on.

#### Motivation for adding to Mudlet

When a file is missing from the build list it can quietly cause trouble
for the people working on Mudlet, and in one recent case it contributed
to a feature breaking. The new check makes the problem impossible to
miss, so it gets noticed and fixed before it reaches anyone.

See Also: [Discord
chat](https://discord.com/channels/283581582550237184/283582439002210305/1517465119700029440)

#### Other info (issues closed, discussion etc)

Builds and runs cleanly on macOS; the new check passes and was confirmed
to flag the original missing and duplicated files.

---------

Signed-off-by: Mike Conley (aka Tamarindo) <sousesider@gmail.com>
Signed-off-by: Michael Conley <sousesider@gmail.com>
2026-06-20 10:48:17 +00:00

181 lines
8.1 KiB
C++

/***************************************************************************
* Copyright (C) 2026 by Mike Conley - mike.conley@stickmud.com *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include <QtTest/QtTest>
#include <QDir>
#include <QFile>
#include <QMap>
#include <QRegularExpression>
#include <QSet>
#include <QString>
#include <QStringList>
/*
* Build-file consistency check for src/CMakeLists.txt.
*
* The project lists its sources and headers explicitly (rather than globbing),
* which is the recommended CMake practice but makes it easy to forget to add a
* new file - or to paste one in twice. That is exactly what happened in #9344,
* where TEncodingHelper.h (and, it turned out, several other headers) were left
* out of mudlet_HDRS, and a few sources were listed twice.
*
* This test walks the top-level files in src/ and the text of src/CMakeLists.txt
* and fails if any .cpp / .h is missing from the build file, or is listed more
* than once. The src/ path is provided at configure time via MUDLET_SRC_DIR.
*
* Scope note: only top-level .cpp, .h and .mm files directly in src/ are checked.
* Files in subdirectories (updater/, etc.) and any path-prefixed entries are
* excluded from both the presence and the duplicate checks, and are left for a
* future extension if needed.
*/
class CMakeListsConsistencyTest : public QObject
{
Q_OBJECT
static QString srcDir() { return QStringLiteral(MUDLET_SRC_DIR); }
static QString readCMakeLists()
{
QFile file(srcDir() + QStringLiteral("/CMakeLists.txt"));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return QString();
}
// Drop comments so a filename mentioned in a comment cannot be mistaken
// for a build-list entry.
QString content;
const QString raw = QString::fromUtf8(file.readAll());
const auto lines = raw.split(QChar::LineFeed);
for (const QString& line : lines) {
const qsizetype hash = line.indexOf(QLatin1Char('#'));
content.append(hash >= 0 ? line.left(hash) : line);
content.append(QChar::LineFeed);
}
return content;
}
// Un-prefixed *.cpp / *.h / *.mm base names referenced in the (comment-stripped)
// CMakeLists text. The leading negative lookbehind requires the name to start a
// fresh token (not preceded by a path separator or another name character), so a
// path-prefixed mention such as "src/sparkleupdater.h" in set_source_files_properties
// is NOT counted - only its bare list entry "sparkleupdater.h" is. That way a file
// removed from mudlet_SRCS/mudlet_HDRS is still reported missing even when it is
// referenced elsewhere by full path. Bare list entries (including the last one,
// which carries the trailing set() ")"), conditional list(APPEND ...) entries, and
// the add_executable() source all match.
static QSet<QString> referencedNames(const QString& content)
{
QSet<QString> names;
static const QRegularExpression re(QStringLiteral("(?<![/A-Za-z0-9_-])([A-Za-z0-9_-]+\\.(?:cpp|h|mm))\\b"));
auto it = re.globalMatch(content);
while (it.hasNext()) {
names.insert(it.next().captured(1));
}
return names;
}
// Counts "bare" top-level list entries - a filename with no path, alone on its
// line, the form used inside the set(mudlet_SRCS ...) / set(mudlet_HDRS ...)
// lists. This is the form the duplicates removed in this PR took. Path-prefixed
// bare entries and conditional list(APPEND ...) lines are deliberately excluded:
// the former are outside the top-level scope this test checks, and a file may be
// appended under several mutually-exclusive platform branches.
static QMap<QString, int> bareEntryCounts(const QString& content)
{
QMap<QString, int> counts;
static const QRegularExpression re(QStringLiteral("^([A-Za-z0-9_-]+\\.(?:cpp|h|mm))$"));
const auto lines = content.split(QChar::LineFeed);
for (const QString& line : lines) {
// Trim first so a CRLF checkout (trailing '\r') still matches.
const auto match = re.match(line.trimmed());
if (!match.hasMatch()) {
continue;
}
counts[match.captured(1)] += 1;
}
return counts;
}
static QStringList topLevelSources()
{
QDir dir(srcDir());
return dir.entryList({QStringLiteral("*.cpp"), QStringLiteral("*.h"), QStringLiteral("*.mm")}, QDir::Files);
}
private slots:
void sourceDirectory_isReadable()
{
QVERIFY2(QDir(srcDir()).exists(), qPrintable(QStringLiteral("src dir not found: %1").arg(srcDir())));
QVERIFY2(!readCMakeLists().isEmpty(), "src/CMakeLists.txt could not be read");
QVERIFY2(!topLevelSources().isEmpty(), "no source files found in src/");
}
void everySourceAndHeader_isListed()
{
const QSet<QString> refs = referencedNames(readCMakeLists());
QStringList missing;
for (const QString& file : topLevelSources()) {
if (!refs.contains(file)) {
missing << file;
}
}
missing.sort();
QVERIFY2(missing.isEmpty(), qPrintable(QStringLiteral("Files present in src/ but not listed in src/CMakeLists.txt: %1").arg(missing.join(QStringLiteral(", ")))));
}
void noSourceOrHeader_isListedTwice()
{
const QMap<QString, int> refs = bareEntryCounts(readCMakeLists());
const QStringList files = topLevelSources();
const QSet<QString> present(files.cbegin(), files.cend());
// Sanity floor: if the bare-entry parser recognised hardly any of the known
// files, the CMakeLists format has likely changed and an empty duplicate
// result would be meaningless. Fail loudly rather than silently stopping
// guarding.
int matched = 0;
for (auto it = refs.cbegin(); it != refs.cend(); ++it) {
if (present.contains(it.key())) {
++matched;
}
}
QVERIFY2(matched > present.size() / 2,
qPrintable(QStringLiteral("bare-entry parser matched only %1 of %2 src files - "
"src/CMakeLists.txt format likely changed; the duplicate check cannot be trusted")
.arg(matched)
.arg(present.size())));
QStringList duplicates;
for (auto it = refs.cbegin(); it != refs.cend(); ++it) {
if (it.value() > 1 && present.contains(it.key())) {
duplicates << QStringLiteral("%1 (x%2)").arg(it.key()).arg(it.value());
}
}
duplicates.sort();
QVERIFY2(duplicates.isEmpty(), qPrintable(QStringLiteral("Files listed more than once in src/CMakeLists.txt: %1").arg(duplicates.join(QStringLiteral(", ")))));
}
};
QTEST_GUILESS_MAIN(CMakeListsConsistencyTest)
#include "CMakeListsConsistencyTest.moc"