Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QFile>
++ #include <QLibraryInfo>
++ #include <QProcess>
++ #include <QTemporaryDir>
++ #include <QTest>
++
++ #include <QtCore/private/qexpected_p.h>
++
++ using namespace Qt::Literals::StringLiterals;
++ using namespace std::chrono_literals;
++
++ class tst_objcnamemangler : public QObject
++ {
++ Q_OBJECT
++
++ public:
++ tst_objcnamemangler();
++
++ private slots:
++ void initTestCase();
++
++ void replaceSuffix();
++ void replacePrefix();
++ void replaceInfix();
++ void excludeClass();
++ void dryRun();
++
++ private:
++ struct ProcessResult {
++ int exitCode;
++ QString stdOut;
++ QString stdErr;
++ };
++
++ q23::expected<ProcessResult, QString> runProcess(const QString &program,
++ const QStringList &arguments,
++ std::chrono::milliseconds timeout = 5s);
++
++ q23::expected<void, QString> runObjcNameMangler(const QStringList &args);
++ q23::expected<QString, QString> runTestExecutable(const QString &binaryPath);
++ q23::expected<void, QString> codesignBinary(const QString &binaryPath);
++ q23::expected<QString, QString> copyTestBinary(const QString &suffix);
++
++ QString objcnamemangler;
++ QString testBinary;
++ QTemporaryDir tempDir;
++ };
++
++ tst_objcnamemangler::tst_objcnamemangler()
++ : objcnamemangler(QLibraryInfo::path(QLibraryInfo::LibraryExecutablesPath)
++ + u"/objcnamemangler"_s)
++ {
++ }
++
++ void tst_objcnamemangler::initTestCase()
++ {
++ // Verify objcnamemangler tool exists
++ QVERIFY2(QFile::exists(objcnamemangler),
++ qPrintable(u"objcnamemangler not found at: %1"_s.arg(objcnamemangler)));
++
++ // Find the test binary built by CMake
++ testBinary = QCoreApplication::applicationDirPath() + u"/objc_test_binary"_s;
++ QVERIFY2(QFile::exists(testBinary),
++ qPrintable(u"Test binary not found at: %1"_s.arg(testBinary)));
++
++ // Verify temporary directory is valid
++ QVERIFY2(tempDir.isValid(),
++ qPrintable(u"Failed to create temporary directory: %1"_s.arg(tempDir.errorString())));
++ }
++
++ q23::expected<tst_objcnamemangler::ProcessResult, QString>
++ tst_objcnamemangler::runProcess(const QString &program, const QStringList &arguments,
++ std::chrono::milliseconds timeout)
++ {
++ QProcess proc;
++ proc.start(program, arguments);
++ int timeoutMs = int(timeout.count());
++
++ if (!proc.waitForStarted())
++ return q23::unexpected(u"Failed to start %1: %2"_s
++ .arg(program, proc.errorString()));
++
++ if (!proc.waitForFinished(timeoutMs)) {
++ proc.kill();
++ proc.waitForFinished();
++ return q23::unexpected(u"%1 timed out after %2ms"_s
++ .arg(program).arg(timeoutMs));
++ }
++
++ if (proc.exitStatus() != QProcess::NormalExit)
++ return q23::unexpected(u"%1 crashed"_s.arg(program));
++
++ ProcessResult result;
++ result.exitCode = proc.exitCode();
++ result.stdOut = QString::fromUtf8(proc.readAllStandardOutput());
++ result.stdErr = QString::fromUtf8(proc.readAllStandardError());
++
++ return result;
++ }
++
++ q23::expected<void, QString> tst_objcnamemangler::runObjcNameMangler(const QStringList &args)
++ {
++ auto result = runProcess(objcnamemangler, args);
++ if (!result.has_value())
++ return q23::unexpected(result.error());
++
++ if (result->exitCode != 0) {
++ return q23::unexpected(u"objcnamemangler failed (exit code %1): %2"_s
++ .arg(result->exitCode).arg(result->stdErr));
++ }
++
++ return {};
++ }
++
++ q23::expected<QString, QString> tst_objcnamemangler::runTestExecutable(const QString &binaryPath)
++ {
++ auto result = runProcess(binaryPath, QStringList(), 3s);
++ if (!result.has_value())
++ return q23::unexpected(result.error());
++
++ if (result->exitCode != 0) {
++ return q23::unexpected(u"Test executable failed with exit code %1"_s
++ .arg(result->exitCode));
++ }
++
++ return result->stdOut.trimmed();
++ }
++
++ q23::expected<void, QString> tst_objcnamemangler::codesignBinary(const QString &binaryPath)
++ {
++ QStringList args{
++ u"--sign"_s,
++ u"-"_s,
++ binaryPath,
++ };
++
++ auto result = runProcess(u"codesign"_s, args);
++ if (!result.has_value())
++ return q23::unexpected(result.error());
++
++ if (result->exitCode != 0) {
++ return q23::unexpected(u"codesign failed (exit code %1): %2"_s
++ .arg(result->exitCode).arg(result->stdErr));
++ }
++
++ return {};
++ }
++
++ q23::expected<QString, QString> tst_objcnamemangler::copyTestBinary(const QString &suffix)
++ {
++ QString destPath = tempDir.filePath(u"test_binary_%1"_s.arg(suffix));
++
++ if (!QFile::copy(testBinary, destPath))
++ return q23::unexpected(u"Failed to copy test binary to %1"_s.arg(destPath));
++
++ // Set executable permissions
++ QFile destFile(destPath);
++ if (!destFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner |
++ QFile::ReadGroup | QFile::ExeGroup |
++ QFile::ReadOther | QFile::ExeOther)) {
++ return q23::unexpected(u"Failed to set executable permissions on %1"_s.arg(destPath));
++ }
++
++ return destPath;
++ }
++
++ void tst_objcnamemangler::replaceSuffix()
++ {
++ // Copy test binary
++ auto testCopyResult = copyTestBinary(u"suffix"_s);
++ if (!testCopyResult.has_value())
++ QFAIL(qPrintable(testCopyResult.error()));
++ QString testCopy = testCopyResult.value();
++
++ // Run objcnamemangler to replace suffix
++ QStringList args{
++ u"--replace"_s,
++ u"_Suffix"_s,
++ u"_SUFFIX"_s,
++ testCopy,
++ };
++ auto manglerResult = runObjcNameMangler(args);
++ if (!manglerResult.has_value())
++ QFAIL(qPrintable(manglerResult.error()));
++
++ // Re-sign the binary
++ auto codesignResult = codesignBinary(testCopy);
++ if (!codesignResult.has_value())
++ QFAIL(qPrintable(codesignResult.error()));
++
++ // Run modified binary and verify output
++ auto outputResult = runTestExecutable(testCopy);
++ if (!outputResult.has_value())
++ QFAIL(qPrintable(outputResult.error()));
++ QCOMPARE(outputResult.value(), u"TestClass_SUFFIX"_s);
++ }
++
++ void tst_objcnamemangler::replacePrefix()
++ {
++ // Copy test binary
++ auto testCopyResult = copyTestBinary(u"prefix"_s);
++ if (!testCopyResult.has_value())
++ QFAIL(qPrintable(testCopyResult.error()));
++ QString testCopy = testCopyResult.value();
++
++ // Run objcnamemangler to replace prefix
++ QStringList args{
++ u"--replace"_s,
++ u"TestClass_"_s,
++ u"TestKlass_"_s,
++ testCopy,
++ };
++ auto manglerResult = runObjcNameMangler(args);
++ if (!manglerResult.has_value())
++ QFAIL(qPrintable(manglerResult.error()));
++
++ // Re-sign the binary
++ auto codesignResult = codesignBinary(testCopy);
++ if (!codesignResult.has_value())
++ QFAIL(qPrintable(codesignResult.error()));
++
++ // Run modified binary and verify output
++ auto outputResult = runTestExecutable(testCopy);
++ if (!outputResult.has_value())
++ QFAIL(qPrintable(outputResult.error()));
++ QCOMPARE(outputResult.value(), u"TestKlass_Suffix"_s);
++ }
++
++ void tst_objcnamemangler::replaceInfix()
++ {
++ // Copy test binary
++ auto testCopyResult = copyTestBinary(u"infix"_s);
++ if (!testCopyResult.has_value())
++ QFAIL(qPrintable(testCopyResult.error()));
++ QString testCopy = testCopyResult.value();
++
++ // Run objcnamemangler to replace infix
++ QStringList args{
++ u"--replace"_s,
++ u"Class_"_s,
++ u"Qlass_"_s,
++ testCopy,
++ };
++ auto manglerResult = runObjcNameMangler(args);
++ if (!manglerResult.has_value())
++ QFAIL(qPrintable(manglerResult.error()));
++
++ // Re-sign the binary
++ auto codesignResult = codesignBinary(testCopy);
++ if (!codesignResult.has_value())
++ QFAIL(qPrintable(codesignResult.error()));
++
++ // Run modified binary and verify output
++ auto outputResult = runTestExecutable(testCopy);
++ if (!outputResult.has_value())
++ QFAIL(qPrintable(outputResult.error()));
++ QCOMPARE(outputResult.value(), u"TestQlass_Suffix"_s);
++ }
++
++ void tst_objcnamemangler::excludeClass()
++ {
++ // Copy test binary
++ auto testCopyResult = copyTestBinary(u"exclude"_s);
++ if (!testCopyResult.has_value())
++ QFAIL(qPrintable(testCopyResult.error()));
++ QString testCopy = testCopyResult.value();
++
++ // Run objcnamemangler with --exclude flag
++ // This should NOT modify the class because it's excluded
++ QStringList args{
++ u"--exclude"_s, u"TestClass_Suffix"_s,
++ u"--replace"_s, u"_Suffix"_s, u"_SUFFIX"_s,
++ testCopy
++ };
++ auto manglerResult = runObjcNameMangler(args);
++ if (!manglerResult.has_value())
++ QFAIL(qPrintable(manglerResult.error()));
++
++ // Re-sign the binary
++ auto codesignResult = codesignBinary(testCopy);
++ if (!codesignResult.has_value())
++ QFAIL(qPrintable(codesignResult.error()));
++
++ // Run modified binary and verify output - should be unchanged
++ auto outputResult = runTestExecutable(testCopy);
++ if (!outputResult.has_value())
++ QFAIL(qPrintable(outputResult.error()));
++ QCOMPARE(outputResult.value(), u"TestClass_Suffix"_s);
++ }
++
++ void tst_objcnamemangler::dryRun()
++ {
++ // Copy test binary
++ auto testCopyResult = copyTestBinary(u"dryrun"_s);
++ if (!testCopyResult.has_value())
++ QFAIL(qPrintable(testCopyResult.error()));
++ QString testCopy = testCopyResult.value();
++
++ // Run objcnamemangler with --dry-run flag
++ // This should NOT modify the binary
++ QStringList args{
++ u"--dry-run"_s,
++ u"--exclude"_s, u"TestClass_Suffix"_s,
++ u"--replace"_s, u"_Suffix"_s, u"_SUFFIX"_s,
++ testCopy
++ };
++ auto manglerResult = runObjcNameMangler(args);
++ if (!manglerResult.has_value())
++ QFAIL(qPrintable(manglerResult.error()));
++
++ // No need to re-sign since binary wasn't modified
++
++ // Run binary and verify output - should be unchanged
++ auto outputResult = runTestExecutable(testCopy);
++ if (!outputResult.has_value())
++ QFAIL(qPrintable(outputResult.error()));
++ QCOMPARE(outputResult.value(), u"TestClass_Suffix"_s);
++ }
++
++ QTEST_MAIN(tst_objcnamemangler)
++
++ #include "tst_objcnamemangler.moc"