Files
KDDockWidgets/tests/fuzzer/Fuzzer.cpp
Sergio Martins 07fc7d0437 tests: Move waitForDeleted() into Platform
So each platform can implement their own.
Meaning old Testing.cpp can be deleted too.
2022-05-20 16:41:59 +01:00

461 lines
13 KiB
C++

/*
This file is part of KDDockWidgets.
SPDX-FileCopyrightText: 2019-2022 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
Author: Sérgio Martins <sergio.martins@kdab.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
// We don't care about performance related checks in the tests
// clazy:excludeall=ctor-missing-parent-argument,missing-qobject-macro,range-loop,missing-typeinfo,detaching-member,function-args-by-ref,non-pod-global-static,reserve-candidates,qstring-allocations
#include "Fuzzer.h"
#include "DockRegistry_p.h"
#include "DockWidget.h"
#include "MainWindow.h"
#include "controllers/FloatingWindow.h"
#include <QJsonDocument>
#include <QString>
#include <QTest>
using namespace KDDockWidgets;
using namespace KDDockWidgets::Testing;
using namespace KDDockWidgets::Testing::Operations;
#define OPERATIONS_PER_TEST 200
struct AddDockWidgetParams
{
QString mainWindowName;
QString dockWidgetName;
QString relativeToName;
KDDockWidgets::Location location;
KDDockWidgets::InitialVisibilityOption addingOption;
bool isNull() const
{
return mainWindowName.isEmpty();
}
QVariantMap toVariantMap() const
{
QVariantMap map;
map["mainWindowName"] = mainWindowName;
map["dockWidgetName"] = dockWidgetName;
if (!relativeToName.isEmpty())
map["relativeToName"] = relativeToName;
map["location"] = location;
map["addingOption"] = QVariant::fromValue(addingOption);
return map;
}
static AddDockWidgetParams fillFromVariantMap(const QVariantMap &map)
{
AddDockWidgetParams params;
params.mainWindowName = map["mainWindowName"].toString();
params.dockWidgetName = map["dockWidgetName"].toString();
params.relativeToName = map["relativeToName"].toString();
params.location = KDDockWidgets::Location(map["location"].toInt());
params.addingOption = map["addingOption"].value<KDDockWidgets::InitialVisibilityOption>();
return params;
}
KDDockWidgets::Controllers::MainWindow *mainWindow() const
{
return KDDockWidgets::DockRegistry::self()->mainWindowByName(mainWindowName);
}
Controllers::DockWidget *dockWidget() const
{
return KDDockWidgets::DockRegistry::self()->dockByName(dockWidgetName);
}
Controllers::DockWidget *relativeTo() const
{
return KDDockWidgets::DockRegistry::self()->dockByName(relativeToName);
}
};
static MainWindow *createMainWindow(const Fuzzer::MainWindowDescriptor &mwd)
{
auto mainWindow = new MainWindow(mwd.name, mwd.mainWindowOption);
mainWindow->setGeometry(mwd.geometry);
mainWindow->show();
return mainWindow;
}
static DockWidget *createDockWidget(const Fuzzer::DockWidgetDescriptor &dwd)
{
auto dockWidget = new DockWidget(dwd.name);
dockWidget->setWidget(new Testing::HostedWidget(dwd.minSize));
if (dwd.isFloating)
dockWidget->setGeometry(dwd.geometry);
if (dwd.isVisible)
dockWidget->show();
return dockWidget;
}
static void createLayout(const Fuzzer::Layout &layout)
{
for (const Fuzzer::MainWindowDescriptor &mwd : layout.mainWindows) {
createMainWindow(mwd);
}
for (const Fuzzer::DockWidgetDescriptor &dwd : layout.dockWidgets) {
createDockWidget(dwd);
}
}
void Fuzzer::runTest(const Test &test)
{
m_lastSavedLayout.clear();
m_currentTest = test;
if (!DockRegistry::self()->isEmpty())
qFatal("There's dock widgets and the start runTest");
const bool skipsLast = m_options & Option_SkipLast;
createLayout(test.initialLayout);
int index = 0;
auto operations = test.operations;
auto last = operations.last();
if (skipsLast)
operations.removeLast();
for (const auto &op : operations) {
index++;
#if 0
// Uncomment to set a breakpoint! Check the last good index printed in the terminal
if (index == 82) {
qDebug() << "About to run the bad guy. Layout:";
DockRegistry::self()->checkSanityAll(/*dumpDebug=*/true);
qDebug() << "Running the bad guy:";
}
#endif
op->execute();
if (op->hasParams())
qDebug() << "Ran" << op->description() << index;
QTest::qWait(m_operationDelayMS);
DockRegistry::self()->checkSanityAll();
}
if (skipsLast)
qDebug() << "Skipped" << last->toString() << "\n";
const bool willQuit = !(m_options & Option_NoQuit);
if (willQuit) {
for (MainWindowBase *mw : DockRegistry::self()->mainwindows())
delete mw;
for (Controllers::FloatingWindow *fw : DockRegistry::self()->floatingWindows())
delete fw;
for (Controllers::DockWidget *dw : DockRegistry::self()->dockwidgets())
delete dw;
if (!DockRegistry::self()->isEmpty())
qFatal("There's still dock widgets and the end of runTest");
}
}
Fuzzer::Fuzzer(bool dumpJsonOnFailure, Options options, QObject *parent)
: QObject(parent)
, m_randomEngine(m_randomDevice())
, m_dumpJsonOnFailure(dumpJsonOnFailure)
, m_options(options)
{
Testing::installFatalMessageHandler();
Testing::setWarningObserver(this);
}
Fuzzer::Layout Fuzzer::generateRandomLayout()
{
// for now we only support 1 main window
Fuzzer::Layout layout;
Fuzzer::MainWindowDescriptor mainWindow;
static int count = 0;
count++;
mainWindow.name = QStringLiteral("MainWindow-%1").arg(count);
mainWindow.geometry = randomGeometry();
mainWindow.mainWindowOption = MainWindowOption_None; // TODO: Maybe test other options
layout.mainWindows << mainWindow;
std::uniform_int_distribution<> numDocksDistrib(1, 10); // TODO: Increase
const int numDockWidgets = numDocksDistrib(m_randomEngine);
for (int i = 0; i < numDockWidgets; ++i) {
layout.dockWidgets << generateRandomDockWidget();
}
return layout;
}
Fuzzer::DockWidgetDescriptor Fuzzer::generateRandomDockWidget()
{
Fuzzer::DockWidgetDescriptor dwd;
static int count = 0;
count++;
dwd.name = QStringLiteral("DockWidget-%1").arg(count);
dwd.isFloating = getRandomBool(35);
dwd.isVisible = getRandomBool(70);
std::uniform_int_distribution<> minSizeDistriv(150, 600);
dwd.minSize.setWidth(minSizeDistriv(m_randomEngine));
dwd.minSize.setHeight(minSizeDistriv(m_randomEngine));
const bool hasMaxSize = getRandomBool(25); // 25% of all dock widgets have a max-size
if (hasMaxSize) {
std::uniform_int_distribution<> maxSizeDistriv(200, 600);
dwd.maxSize.setWidth(dwd.minSize.width() + minSizeDistriv(m_randomEngine));
dwd.maxSize.setHeight(dwd.minSize.height() + minSizeDistriv(m_randomEngine));
}
QPoint pos = getRandomPos();
std::uniform_int_distribution<> widthDistrib(dwd.minSize.width(), hasMaxSize ? dwd.maxSize.width() : dwd.minSize.width() + 600);
std::uniform_int_distribution<> heightDistrib(dwd.minSize.height(), hasMaxSize ? dwd.maxSize.height() : dwd.minSize.height() + 600);
dwd.geometry = QRect(pos, QSize(widthDistrib(m_randomEngine), heightDistrib(m_randomEngine)));
return dwd;
}
Fuzzer::DockWidgetDescriptor::List Fuzzer::generateRandomDockWidgets(int num)
{
Fuzzer::DockWidgetDescriptor::List dockWidgets;
for (int i = 0; i < num; ++i) {
dockWidgets << generateRandomDockWidget();
}
return dockWidgets;
}
bool Fuzzer::getRandomBool(int truePercentage)
{
std::uniform_int_distribution<> distrib(1, 100);
return distrib(m_randomEngine) < truePercentage;
}
Testing::AddDockWidgetParams Fuzzer::getRandomAddDockWidgetParams()
{
AddDockWidgetParams params;
if (auto dw = getRandomDockWidget()) {
params.dockWidgetName = dw->uniqueName();
} else {
qWarning() << Q_FUNC_INFO << "No dock widgets exist yet!";
return {};
}
if (auto mw = getRandomMainWindow()) {
params.mainWindowName = mw->uniqueName();
} else {
qWarning() << Q_FUNC_INFO << "No main widgets exist yet!";
return {};
}
if (getRandomBool()) {
if (auto rt = getRandomRelativeTo(params.mainWindow(), params.dockWidget())) {
params.relativeToName = rt->uniqueName();
}
}
params.location = getRandomLocation();
params.addingOption = InitialVisibilityOption::StartVisible; // TODO: Test the other ones
return params;
}
MainWindowBase *Fuzzer::getRandomMainWindow()
{
auto windows = DockRegistry::self()->mainwindows();
if (windows.isEmpty()) {
qWarning() << Q_FUNC_INFO << "No MainWindows exist yet!";
return nullptr;
}
return windows.first();
}
Controllers::DockWidget *Fuzzer::getRandomDockWidget(const Controllers::DockWidget::List &excluding)
{
auto docks = DockRegistry::self()->dockwidgets();
for (auto dw : excluding)
docks.removeOne(dw);
if (docks.isEmpty())
return nullptr;
std::uniform_int_distribution<> locationDistrib(0, docks.size() - 1);
return docks[locationDistrib(m_randomEngine)];
}
Controllers::DockWidget *Fuzzer::getRandomRelativeTo(MainWindowBase *mainWindow, Controllers::DockWidget *excluding)
{
auto docks = DockRegistry::self()->dockwidgets();
Controllers::DockWidget::List candidates;
for (Controllers::DockWidget *dw : docks) {
if (dw != excluding && dw->window() == mainWindow)
candidates << dw;
}
if (candidates.isEmpty())
return nullptr;
std::uniform_int_distribution<> locationDistrib(0, candidates.size() - 1);
return candidates[locationDistrib(m_randomEngine)];
}
Location Fuzzer::getRandomLocation()
{
std::uniform_int_distribution<> locationDistrib(1, 4);
return Location(locationDistrib(m_randomEngine));
}
QPoint Fuzzer::getRandomPos()
{
std::uniform_int_distribution<> posDistrib(0, 500);
const int x = posDistrib(m_randomEngine);
const int y = posDistrib(m_randomEngine);
return { x, y };
}
OperationBase::Ptr Fuzzer::getRandomOperation()
{
std::uniform_int_distribution<> operationDistrib(OperationType_None + 1, OperationType_Count - 1);
auto operationType = OperationType(operationDistrib(m_randomEngine));
return OperationBase::newOperation(this, operationType);
}
Fuzzer::Test Fuzzer::generateRandomTest()
{
Fuzzer::Test test;
test.initialLayout = generateRandomLayout();
const int numOperationsPerTest = OPERATIONS_PER_TEST;
test.operations.reserve(numOperationsPerTest);
for (int i = 0; i < numOperationsPerTest; ++i)
test.operations << getRandomOperation();
return test;
}
Fuzzer::Test::List Fuzzer::generateRandomTests(int num)
{
Fuzzer::Test::List tests;
for (int i = 0; i < num; ++i) {
tests << generateRandomTest();
}
return tests;
}
void Fuzzer::fuzz(FuzzerConfig config)
{
const Fuzzer::Test::List tests = generateRandomTests(config.numTests);
qDebug().noquote() << "Running" << QString("%1 tests...").arg(tests.size());
for (const auto &test : tests) {
runTest(test);
}
}
void Fuzzer::fuzz(const QStringList &jsonFiles)
{
if (jsonFiles.size() > 1 && (m_options & Option_SkipLast)) {
qFatal("Use -d only when passing a single json file");
}
for (const QString &jsonFile : jsonFiles)
fuzz(jsonFile);
}
void Fuzzer::fuzz(const QString &jsonFile)
{
m_currentJsonFile = jsonFile;
qDebug() << "\n"
<< Q_FUNC_INFO << jsonFile;
QFile file(jsonFile);
if (file.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
const QVariantMap map = doc.toVariant().toMap();
Test test = Test::fromVariantMap(this, map);
runTest(test);
} else {
qWarning() << Q_FUNC_INFO << "Failed to open file" << jsonFile;
}
}
QRect Fuzzer::randomGeometry()
{
std::uniform_int_distribution<> posDistrib(0, 500);
std::uniform_int_distribution<> sizeDistrib(700, 1500);
const int width = posDistrib(m_randomEngine);
const int height = sizeDistrib(m_randomEngine);
QPoint pos = getRandomPos();
return QRect(pos, QSize(width, height));
}
void Fuzzer::onFatal()
{
if (m_dumpJsonOnFailure) {
// Tests failed! Let's dump
m_currentTest.dumpToJsonFile("fuzzer_dump.json");
}
if (!m_currentJsonFile.isEmpty()) {
qDebug() << "failed json: " << m_currentJsonFile;
}
}
void Fuzzer::setDelayBetweenOperations(int delay)
{
m_operationDelayMS = delay;
}
QByteArray Fuzzer::lastSavedLayout() const
{
return m_lastSavedLayout;
}
void Fuzzer::setLastSavedLayout(const QByteArray &serialized)
{
m_lastSavedLayout = serialized;
}
void Fuzzer::Test::dumpToJsonFile(const QString &filename) const
{
const QVariantMap map = toVariantMap();
QJsonDocument jsonDoc = QJsonDocument::fromVariant(map);
QFile file(filename);
if (file.open(QIODevice::WriteOnly)) {
file.write(jsonDoc.toJson());
} else {
qDebug() << Q_FUNC_INFO << "Error opening file";
}
file.close();
}