mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Tests] Replay XML test files through a headless runner (#7694)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
2e0a718cd7
commit
ee57d740c2
9 changed files with 469 additions and 4 deletions
|
|
@ -567,6 +567,9 @@ target_link_libraries(
|
|||
cockatrice_target_enable_crash_reporting(cockatrice)
|
||||
|
||||
if(WITH_GUI_TEST)
|
||||
# recording.cpp translates between XML test files and the QtTesting event pipeline;
|
||||
# it needs the QtTesting headers, so it only exists with the harness.
|
||||
target_sources(cockatrice PRIVATE src/interface/recording.cpp)
|
||||
target_link_libraries(cockatrice PRIVATE QtTesting)
|
||||
target_compile_definitions(cockatrice PRIVATE WITH_GUI_TEST)
|
||||
endif()
|
||||
|
|
|
|||
102
cockatrice/src/interface/recording.cpp
Normal file
102
cockatrice/src/interface/recording.cpp
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#include "recording.h"
|
||||
|
||||
#include "pqEventTypes.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
XMLEventObserver::XMLEventObserver(QObject *parent) : pqEventObserver(parent)
|
||||
{
|
||||
}
|
||||
|
||||
XMLEventObserver::~XMLEventObserver() = default;
|
||||
|
||||
void XMLEventObserver::setStream(QTextStream *stream)
|
||||
{
|
||||
if (xmlStream) {
|
||||
xmlStream->writeEndElement();
|
||||
xmlStream->writeEndDocument();
|
||||
xmlStream.reset();
|
||||
}
|
||||
if (Stream) {
|
||||
*Stream << xmlString;
|
||||
}
|
||||
xmlString.clear();
|
||||
pqEventObserver::setStream(stream);
|
||||
if (Stream) {
|
||||
xmlStream.reset(new QXmlStreamWriter(&xmlString));
|
||||
xmlStream->setAutoFormatting(true);
|
||||
xmlStream->writeStartDocument();
|
||||
xmlStream->writeStartElement(QStringLiteral("events"));
|
||||
}
|
||||
}
|
||||
|
||||
void XMLEventObserver::onRecordEvent(const QString &widget,
|
||||
const QString &command,
|
||||
const QString &arguments,
|
||||
const int &eventType)
|
||||
{
|
||||
if (!xmlStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
xmlStream->writeStartElement(QStringLiteral("event"));
|
||||
xmlStream->writeAttribute(QStringLiteral("widget"), widget);
|
||||
if (eventType == pqEventTypes::ACTION_EVENT) {
|
||||
xmlStream->writeAttribute(QStringLiteral("command"), command);
|
||||
} else { // pqEventTypes::CHECK_EVENT
|
||||
xmlStream->writeAttribute(QStringLiteral("property"), command);
|
||||
}
|
||||
xmlStream->writeAttribute(QStringLiteral("arguments"), arguments);
|
||||
xmlStream->writeEndElement();
|
||||
}
|
||||
|
||||
XMLEventSource::XMLEventSource(QObject *parent) : pqEventSource(parent)
|
||||
{
|
||||
}
|
||||
|
||||
XMLEventSource::~XMLEventSource() = default;
|
||||
|
||||
void XMLEventSource::setContent(const QString &xmlFileName)
|
||||
{
|
||||
xmlStream.reset();
|
||||
|
||||
QFile xml(xmlFileName);
|
||||
if (!xml.open(QIODevice::ReadOnly)) {
|
||||
qCWarning(RecordingLog) << "Could not read test file" << xmlFileName;
|
||||
return;
|
||||
}
|
||||
|
||||
xmlStream.reset(new QXmlStreamReader(xml.readAll()));
|
||||
}
|
||||
|
||||
int XMLEventSource::getNextEvent(QString &widget, QString &command, QString &arguments, int &eventType)
|
||||
{
|
||||
if (!xmlStream) {
|
||||
return DONE;
|
||||
}
|
||||
|
||||
while (!xmlStream->atEnd()) {
|
||||
if (xmlStream->readNext() != QXmlStreamReader::StartElement || xmlStream->name() != QStringLiteral("event")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QXmlStreamAttributes attributes = xmlStream->attributes();
|
||||
eventType = pqEventTypes::ACTION_EVENT;
|
||||
widget = attributes.value(QStringLiteral("widget")).toString();
|
||||
command = attributes.value(QStringLiteral("command")).toString();
|
||||
if (command.isEmpty()) {
|
||||
// A recorded check event carries a property instead of a command.
|
||||
command = attributes.value(QStringLiteral("property")).toString();
|
||||
}
|
||||
arguments = attributes.value(QStringLiteral("arguments")).toString();
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
if (xmlStream->hasError()) {
|
||||
qCWarning(RecordingLog) << "Malformed test file:" << xmlStream->errorString();
|
||||
}
|
||||
return DONE;
|
||||
}
|
||||
54
cockatrice/src/interface/recording.h
Normal file
54
cockatrice/src/interface/recording.h
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#ifndef COCKATRICE_RECORDING_H
|
||||
#define COCKATRICE_RECORDING_H
|
||||
|
||||
#include "pqEventObserver.h"
|
||||
#include "pqEventSource.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QScopedPointer>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(RecordingLog, "recording");
|
||||
|
||||
class QXmlStreamReader;
|
||||
class QXmlStreamWriter;
|
||||
|
||||
/**
|
||||
* @brief Writes a session recording as an XML test file.
|
||||
*
|
||||
* The events are buffered in memory and flushed to the stream when recording
|
||||
* ends, because the stream is handed over once the file name is known.
|
||||
*/
|
||||
class XMLEventObserver : public pqEventObserver
|
||||
{
|
||||
QScopedPointer<QXmlStreamWriter> xmlStream;
|
||||
QString xmlString;
|
||||
|
||||
public:
|
||||
explicit XMLEventObserver(QObject *parent);
|
||||
~XMLEventObserver() override;
|
||||
|
||||
protected:
|
||||
void setStream(QTextStream *stream) override;
|
||||
void onRecordEvent(const QString &widget,
|
||||
const QString &command,
|
||||
const QString &arguments,
|
||||
const int &eventType) override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Plays the events of an XML test file back, one at a time.
|
||||
*/
|
||||
class XMLEventSource : public pqEventSource
|
||||
{
|
||||
QScopedPointer<QXmlStreamReader> xmlStream;
|
||||
|
||||
public:
|
||||
explicit XMLEventSource(QObject *parent);
|
||||
~XMLEventSource() override;
|
||||
|
||||
protected:
|
||||
void setContent(const QString &xmlFileName) override;
|
||||
int getNextEvent(QString &widget, QString &command, QString &arguments, int &eventType) override;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_RECORDING_H
|
||||
|
|
@ -55,6 +55,13 @@
|
|||
#include "widgets/server/handle_public_servers.h"
|
||||
#include "widgets/settings_page/abstract_settings_page.h"
|
||||
#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h"
|
||||
#ifdef WITH_GUI_TEST
|
||||
#include "pqEventObserver.h"
|
||||
#include "pqEventSource.h"
|
||||
#include "pqEventTypes.h"
|
||||
#include "pqTestUtility.h"
|
||||
#include "recording.h"
|
||||
#endif
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
|
|
@ -310,6 +317,7 @@ void MainWindow::actAbout()
|
|||
mb.setIconPixmap(
|
||||
themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||
mb.setObjectName("aboutDialog");
|
||||
mb.exec();
|
||||
}
|
||||
|
||||
|
|
@ -348,6 +356,24 @@ void MainWindow::actOpenSettingsFolder()
|
|||
QtUtils::openPathInFileManager(dir);
|
||||
}
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
void MainWindow::actRecord()
|
||||
{
|
||||
QString filename = QFileDialog::getSaveFileName(this, tr("Test File Name"), QString(), tr("XML Files (*.xml)"));
|
||||
if (!filename.isEmpty()) {
|
||||
testUtility->recordTests(filename);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::actPlayRecording()
|
||||
{
|
||||
QString filename = QFileDialog::getOpenFileName(this, tr("Test File Name"), QString(), tr("XML Files (*.xml)"));
|
||||
if (!filename.isEmpty()) {
|
||||
testUtility->playTests(filename);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainWindow::retranslateUi()
|
||||
{
|
||||
connectionController->refreshWindowTitle();
|
||||
|
|
@ -392,6 +418,10 @@ void MainWindow::retranslateUi()
|
|||
aViewLog->setText(tr("View &Debug Log"));
|
||||
aOpenSettingsFolder->setText(tr("Open Settings Folder"));
|
||||
aFirstRunWizard->setText(tr("Re-run Onboarding Wizard..."));
|
||||
#ifdef WITH_GUI_TEST
|
||||
aRecord->setText(tr("&Record Test..."));
|
||||
aPlayRecording->setText(tr("&Play Test..."));
|
||||
#endif
|
||||
|
||||
aShow->setText(tr("Show/Hide"));
|
||||
|
||||
|
|
@ -455,6 +485,12 @@ void MainWindow::createActions()
|
|||
connect(aOpenSettingsFolder, &QAction::triggered, this, &MainWindow::actOpenSettingsFolder);
|
||||
aFirstRunWizard = new QAction(this);
|
||||
connect(aFirstRunWizard, &QAction::triggered, this, [this] { runFirstRunWizard(); });
|
||||
#ifdef WITH_GUI_TEST
|
||||
aRecord = new QAction(this);
|
||||
connect(aRecord, &QAction::triggered, this, &MainWindow::actRecord);
|
||||
aPlayRecording = new QAction(this);
|
||||
connect(aPlayRecording, &QAction::triggered, this, &MainWindow::actPlayRecording);
|
||||
#endif
|
||||
|
||||
aShow = new QAction(this);
|
||||
connect(aShow, &QAction::triggered, this, &MainWindow::actShow);
|
||||
|
|
@ -495,6 +531,7 @@ void MainWindow::createActions()
|
|||
|
||||
void MainWindow::createMenus()
|
||||
{
|
||||
menuBar()->setObjectName("menubar");
|
||||
cockatriceMenu = menuBar()->addMenu(QString());
|
||||
cockatriceMenu->addAction(aConnect);
|
||||
cockatriceMenu->addAction(aDisconnect);
|
||||
|
|
@ -523,6 +560,7 @@ void MainWindow::createMenus()
|
|||
tabsMenu = menuBar()->addMenu(QString());
|
||||
|
||||
helpMenu = menuBar()->addMenu(QString());
|
||||
helpMenu->setObjectName("helpMenu");
|
||||
helpMenu->addAction(aAbout);
|
||||
helpMenu->addAction(aTips);
|
||||
helpMenu->addSeparator();
|
||||
|
|
@ -535,12 +573,19 @@ void MainWindow::createMenus()
|
|||
helpMenu->addAction(aOpenSettingsFolder);
|
||||
helpMenu->addSeparator();
|
||||
helpMenu->addAction(aFirstRunWizard);
|
||||
#ifdef WITH_GUI_TEST
|
||||
helpMenu->addSeparator();
|
||||
helpMenu->addAction(aRecord);
|
||||
helpMenu->addAction(aPlayRecording);
|
||||
#endif
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false),
|
||||
: QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false), noConnect(false),
|
||||
cardUpdateProcess(nullptr), logviewDialog(nullptr)
|
||||
{
|
||||
setObjectName("MainWindow");
|
||||
|
||||
connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::pixmapCacheSizeChanged, this,
|
||||
&MainWindow::pixmapCacheSizeChanged);
|
||||
pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize());
|
||||
|
|
@ -626,6 +671,13 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
// run startup check async
|
||||
QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
|
||||
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
testUtility = new pqTestUtility(this);
|
||||
testUtility->setRecordWithDialog(true);
|
||||
testUtility->addEventObserver("xml", new XMLEventObserver(this));
|
||||
testUtility->addEventSource("xml", new XMLEventSource(this));
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::startupConfigCheck()
|
||||
|
|
@ -780,6 +832,11 @@ void MainWindow::runFirstRunWizard(bool firstRun)
|
|||
*/
|
||||
void MainWindow::applyStartupDestination()
|
||||
{
|
||||
if (noConnect) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Startup destination suppressed (--no-connect)";
|
||||
return;
|
||||
}
|
||||
|
||||
// An explicit command-line connect takes precedence over the startup destination.
|
||||
if (!connectTo.isEmpty()) {
|
||||
return;
|
||||
|
|
@ -1049,6 +1106,10 @@ void MainWindow::handleCockatriceLink(const QString &url)
|
|||
|
||||
void MainWindow::attemptStartupAutoConnect()
|
||||
{
|
||||
if (noConnect) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Auto-connect suppressed (--no-connect)";
|
||||
return;
|
||||
}
|
||||
if (startupAutoConnectAttempted || skipStartupAutoConnect) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
class QAction;
|
||||
class QToolButton;
|
||||
class QWidget;
|
||||
class pqTestUtility;
|
||||
struct LocalGameOptions;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(WindowMainLog, "window_main");
|
||||
|
|
@ -104,6 +105,11 @@ private slots:
|
|||
void actUpdate();
|
||||
void actViewLog();
|
||||
void actOpenSettingsFolder();
|
||||
#ifdef WITH_GUI_TEST
|
||||
void actRecord();
|
||||
void actPlayRecording();
|
||||
#endif
|
||||
|
||||
void actShow();
|
||||
void showWindowIfHidden();
|
||||
void onUrlChainFinished(bool connected);
|
||||
|
|
@ -185,6 +191,9 @@ private:
|
|||
*aReloadCardDatabase;
|
||||
QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aFirstRunWizard, *aStatusBar,
|
||||
*aViewLog, *aOpenSettingsFolder;
|
||||
#ifdef WITH_GUI_TEST
|
||||
QAction *aRecord, *aPlayRecording;
|
||||
#endif
|
||||
|
||||
TabSupervisor *tabSupervisor;
|
||||
IntentUrlParser *urlParser;
|
||||
|
|
@ -208,6 +217,7 @@ private:
|
|||
bool skipStartupAutoConnect = false;
|
||||
bool startupAutoConnectAttempted = false;
|
||||
bool firstRunWizardActive = false;
|
||||
bool noConnect = false; ///< --no-connect: skip every startup connection
|
||||
QProcess *cardUpdateProcess;
|
||||
QByteArray cardUpdateOutputBuffer;
|
||||
QByteArray cardUpdateErrorOutput; ///< diagnostic output collected from the updater's stderr
|
||||
|
|
@ -219,6 +229,9 @@ private:
|
|||
GameReplay *replay;
|
||||
DlgTipOfTheDay *tip;
|
||||
QUrl connectTo;
|
||||
#ifdef WITH_GUI_TEST
|
||||
pqTestUtility *testUtility;
|
||||
#endif
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
|
|
@ -226,6 +239,16 @@ public:
|
|||
{
|
||||
connectTo = QUrl(QString("cockatrice://%1").arg(url));
|
||||
}
|
||||
/**
|
||||
* @brief Suppresses every startup connection: --connect, auto-connect and the
|
||||
* configured startup destination. Used by the GUI test harness so a
|
||||
* playback run never dials out.
|
||||
*/
|
||||
void setNoConnect(bool value)
|
||||
{
|
||||
noConnect = value;
|
||||
}
|
||||
|
||||
// When set, the window's own startup connection (--connect or auto-connect
|
||||
// on first activation) is skipped. Used for activation launches: the intent
|
||||
// chain triggered by a cockatrice:// URL owns the connection, and letting
|
||||
|
|
@ -266,6 +289,13 @@ public:
|
|||
*/
|
||||
void requestInstallerClose();
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
pqTestUtility *getTestUtility() const
|
||||
{
|
||||
return testUtility;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@
|
|||
#include "single_instance_manager.h"
|
||||
#include "version_string.h"
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
#include "pqTestUtility.h"
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QByteArrayView>
|
||||
|
|
@ -62,10 +66,12 @@
|
|||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QStringLiteral>
|
||||
#include <QThread>
|
||||
#include <QTranslator>
|
||||
#include <QUrl>
|
||||
#include <QtGlobal>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/rng/rng_sfmt.h>
|
||||
#include <libcockatrice/settings/card_database_settings.h>
|
||||
|
|
@ -343,9 +349,16 @@ int main(int argc, char *argv[])
|
|||
parser.addOptions(
|
||||
{{{"c", "connect"}, QCoreApplication::translate("main", "Connect on startup"), "user:pass@host:port"},
|
||||
{{"d", "debug-output"}, QCoreApplication::translate("main", "Debug to file")},
|
||||
{"simulate-crash",
|
||||
QCoreApplication::translate("main",
|
||||
"Show the crash-report dialog (saves a mock file when no DSN is configured)")}});
|
||||
{"simulate-crash", QCoreApplication::translate(
|
||||
"main", "Show the crash-report dialog (saves a mock file when no DSN is configured)")},
|
||||
{"no-connect", QCoreApplication::translate("main", "Do not connect to any server on startup")}
|
||||
#ifdef WITH_GUI_TEST
|
||||
,
|
||||
{{"t", "test"},
|
||||
QCoreApplication::translate("main", "Play back a QtTesting XML test file and exit"),
|
||||
"test-file.xml"}
|
||||
#endif
|
||||
});
|
||||
|
||||
parser.process(app);
|
||||
|
||||
|
|
@ -483,6 +496,9 @@ int main(int argc, char *argv[])
|
|||
if (parser.isSet("connect")) {
|
||||
ui.setConnectTo(parser.value("connect"));
|
||||
}
|
||||
if (parser.isSet("no-connect")) {
|
||||
ui.setNoConnect(true);
|
||||
}
|
||||
qCInfo(MainLog) << "MainWindow constructor finished";
|
||||
|
||||
ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
|
||||
|
|
@ -523,6 +539,27 @@ int main(int argc, char *argv[])
|
|||
handleActivation(file);
|
||||
}
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
if (parser.isSet("test")) {
|
||||
QString testFile = parser.value("test");
|
||||
qCInfo(MainLog) << "Playing test file:" << testFile;
|
||||
QTimer::singleShot(0, &ui, [&ui, testFile]() {
|
||||
bool success = ui.getTestUtility()->playTests(testFile);
|
||||
qCInfo(MainLog) << "Test playback" << (success ? "succeeded" : "FAILED");
|
||||
qApp->exit(success ? 0 : 1);
|
||||
|
||||
// An event that fails while a modal dialog is open aborts the dispatcher,
|
||||
// but the dialog's nested event loop can then keep the application alive
|
||||
// forever. Hard-exit after a grace period so a red test always terminates.
|
||||
auto *watchdog = QThread::create([success] {
|
||||
QThread::sleep(10);
|
||||
::_Exit(success ? 0 : 1);
|
||||
});
|
||||
watchdog->start();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
// Connect to future file/URL events from other instances
|
||||
QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) {
|
||||
for (const QString &file : files) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue