Files
archived-Reclass/tests/test_source_provider.cpp
2026-02-28 12:54:38 -07:00

321 lines
11 KiB
C++

#include <QtTest/QTest>
#include <QApplication>
#include <QSplitter>
#include <QIcon>
#include <QMenu>
#include <QAction>
#include <QPixmap>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "core.h"
#include "providerregistry.h"
#include "providers/null_provider.h"
#include "providers/buffer_provider.h"
#ifdef Q_OS_WIN
#include <windows.h>
#endif
using namespace rcx;
// Minimal mock IProviderPlugin that reads from the current process
class SelfProcessPlugin : public IProviderPlugin {
public:
std::string Name() const override { return "TestProcessMemory"; }
std::string Version() const override { return "1.0"; }
std::string Author() const override { return "Test"; }
std::string Description() const override { return "Mock plugin for testing"; }
QIcon Icon() const override { return {}; }
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
bool canHandle(const QString&) const override { return true; }
std::unique_ptr<Provider> createProvider(const QString& target, QString*) override {
// Create a buffer provider with a known pattern at a known base
QByteArray data(256, '\0');
// Write a recognizable pattern: 0xDE 0xAD 0xBE 0xEF ...
data[0] = (char)0xDE;
data[1] = (char)0xAD;
data[2] = (char)0xBE;
data[3] = (char)0xEF;
data[4] = (char)0xCA;
data[5] = (char)0xFE;
data[6] = (char)0xBA;
data[7] = (char)0xBE;
m_lastBase = 0x7FF000000000ULL; // simulate typical image base
Q_UNUSED(target);
return std::make_unique<BufferProvider>(data, "self");
}
bool selectTarget(QWidget*, QString* target) override {
*target = "self";
return true;
}
uint64_t getInitialBaseAddress(const QString&) const override {
return m_lastBase;
}
uint64_t lastBase() const { return m_lastBase; }
private:
uint64_t m_lastBase = 0;
};
static void buildTree(NodeTree& tree, uint64_t base) {
tree.baseAddress = base;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "TestStruct";
root.name = "TestStruct";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
for (int off = 0; off < 64; off += 8) {
Node f;
f.kind = NodeKind::Hex64;
f.name = QStringLiteral("field_%1").arg(off, 2, 16, QLatin1Char('0'));
f.parentId = rootId;
f.offset = off;
tree.addNode(f);
}
}
class TestSourceProvider : public QObject {
Q_OBJECT
private:
RcxDocument* m_doc = nullptr;
RcxController* m_ctrl = nullptr;
QSplitter* m_splitter = nullptr;
SelfProcessPlugin* m_plugin = nullptr;
private slots:
void init() {
m_doc = new RcxDocument();
m_splitter = new QSplitter();
m_ctrl = new RcxController(m_doc, nullptr);
m_ctrl->addSplitEditor(m_splitter);
m_splitter->resize(800, 600);
m_splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(m_splitter));
QApplication::processEvents();
}
void cleanup() {
// Unregister test providers
ProviderRegistry::instance().unregisterProvider("testprocessmemory");
delete m_ctrl; m_ctrl = nullptr;
delete m_splitter; m_splitter = nullptr;
delete m_doc; m_doc = nullptr;
m_plugin = nullptr; // owned by PluginManager/test scope, don't double-delete
}
// ── attachViaPlugin must NOT overwrite pre-set base address ──
void testAttachViaPluginPreservesBaseAddress() {
// Register our mock plugin
m_plugin = new SelfProcessPlugin();
ProviderRegistry::instance().registerProvider(
"TestProcessMemory", "testprocessmemory", m_plugin);
// Pre-set base address (like selfTest/buildEditorDemo does)
const uint64_t demoBase = 0x0000020E2FAB1770ULL;
buildTree(m_doc->tree, demoBase);
QCOMPARE(m_doc->tree.baseAddress, demoBase);
// Attach via plugin — this should NOT overwrite the base address
m_ctrl->attachViaPlugin(QStringLiteral("testprocessmemory"), QStringLiteral("self"));
// Base address must still be the demo address, NOT the provider's image base
QCOMPARE(m_doc->tree.baseAddress, demoBase);
QVERIFY(m_doc->tree.baseAddress != m_plugin->lastBase());
// Provider should be valid and readable
QVERIFY(m_doc->provider != nullptr);
QVERIFY(m_doc->provider->isValid());
delete m_plugin;
m_plugin = nullptr;
}
// ── Provider reads correct data after attach ──
void testProviderReadsCorrectData() {
m_plugin = new SelfProcessPlugin();
ProviderRegistry::instance().registerProvider(
"TestProcessMemory", "testprocessmemory", m_plugin);
buildTree(m_doc->tree, 0x1000);
m_ctrl->attachViaPlugin(QStringLiteral("testprocessmemory"), QStringLiteral("self"));
// Read the known pattern written by the mock provider
QVERIFY(m_doc->provider->isValid());
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0xDE);
QCOMPARE(m_doc->provider->readU8(1), (uint8_t)0xAD);
QCOMPARE(m_doc->provider->readU8(2), (uint8_t)0xBE);
QCOMPARE(m_doc->provider->readU8(3), (uint8_t)0xEF);
QCOMPARE(m_doc->provider->readU8(4), (uint8_t)0xCA);
QCOMPARE(m_doc->provider->readU8(5), (uint8_t)0xFE);
QCOMPARE(m_doc->provider->readU8(6), (uint8_t)0xBA);
QCOMPARE(m_doc->provider->readU8(7), (uint8_t)0xBE);
// Read as u64 — should be 0xBEBAFECAEFBEADDE in little-endian
uint64_t val = m_doc->provider->readU64(0);
QCOMPARE(val, 0xBEBAFECAEFBEADDEULL);
delete m_plugin;
m_plugin = nullptr;
}
// ── Provider data is not garbage (not PE header) ──
void testProviderDataIsNotPEHeader() {
m_plugin = new SelfProcessPlugin();
ProviderRegistry::instance().registerProvider(
"TestProcessMemory", "testprocessmemory", m_plugin);
const uint64_t demoBase = 0x0000020E2FAB1770ULL;
buildTree(m_doc->tree, demoBase);
m_ctrl->attachViaPlugin(QStringLiteral("testprocessmemory"), QStringLiteral("self"));
// The data should NOT start with 'MZ' (PE header signature)
// If it does, the base address was wrongly set to the process image base
uint16_t mz = m_doc->provider->readU16(0);
QVERIFY2(mz != 0x5A4D, "Data starts with MZ — base address was overwritten to image base!");
// Verify our known pattern instead
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0xDE);
delete m_plugin;
m_plugin = nullptr;
}
// ── ProviderRegistry: registration and lookup ──
void testProviderRegistryRegisterAndFind() {
m_plugin = new SelfProcessPlugin();
ProviderRegistry::instance().registerProvider(
"TestProcessMemory", "testprocessmemory", m_plugin, "libTestPlugin.dll");
const auto* info = ProviderRegistry::instance().findProvider("testprocessmemory");
QVERIFY(info != nullptr);
QCOMPARE(info->name, QStringLiteral("TestProcessMemory"));
QCOMPARE(info->identifier, QStringLiteral("testprocessmemory"));
QCOMPARE(info->dllFileName, QStringLiteral("libTestPlugin.dll"));
QVERIFY(!info->isBuiltin);
QVERIFY(info->plugin != nullptr);
delete m_plugin;
m_plugin = nullptr;
}
void testProviderRegistryUnregister() {
m_plugin = new SelfProcessPlugin();
ProviderRegistry::instance().registerProvider(
"TestProcessMemory", "testprocessmemory", m_plugin);
QVERIFY(ProviderRegistry::instance().findProvider("testprocessmemory") != nullptr);
ProviderRegistry::instance().unregisterProvider("testprocessmemory");
QVERIFY(ProviderRegistry::instance().findProvider("testprocessmemory") == nullptr);
delete m_plugin;
m_plugin = nullptr;
}
// ── SVG icons load from resources ──
void testSourceMenuIconsLoad() {
// These are the icons used in the source menu
const QStringList iconPaths = {
QStringLiteral(":/vsicons/file-binary.svg"),
QStringLiteral(":/vsicons/server-process.svg"),
QStringLiteral(":/vsicons/remote.svg"),
QStringLiteral(":/vsicons/debug.svg"),
QStringLiteral(":/vsicons/plug.svg"),
QStringLiteral(":/vsicons/extensions.svg"),
QStringLiteral(":/vsicons/clear-all.svg"),
};
for (const QString& path : iconPaths) {
QIcon icon(path);
QVERIFY2(!icon.isNull(),
qPrintable(QStringLiteral("Icon is null: %1").arg(path)));
// Verify it can actually render a pixmap
QPixmap pm = icon.pixmap(16, 16);
QVERIFY2(!pm.isNull(),
qPrintable(QStringLiteral("Pixmap is null: %1").arg(path)));
QVERIFY2(pm.width() > 0 && pm.height() > 0,
qPrintable(QStringLiteral("Pixmap has zero size: %1").arg(path)));
}
}
// ── Menu actions have icons set and forced visible ──
void testMenuActionIconVisibility() {
QMenu menu;
QIcon icon(QStringLiteral(":/vsicons/file-binary.svg"));
QVERIFY(!icon.isNull());
auto* act = menu.addAction(icon, "Test Item");
act->setIconVisibleInMenu(true);
QVERIFY(!act->icon().isNull());
QVERIFY(act->isIconVisibleInMenu());
// Verify pixmap can be extracted from the action's icon
QPixmap pm = act->icon().pixmap(16, 16);
QVERIFY(!pm.isNull());
}
// ── selectSource with provider updates base address ──
void testSelectSourceUpdatesBaseAddress() {
// This tests that selectSource (user-initiated) DOES update the base,
// while attachViaPlugin does NOT.
m_plugin = new SelfProcessPlugin();
ProviderRegistry::instance().registerProvider(
"TestProcessMemory", "testprocessmemory", m_plugin);
// Start with zero base
m_doc->tree.baseAddress = 0;
// attachViaPlugin should NOT set the base (it's 0 and stays 0)
m_ctrl->attachViaPlugin(QStringLiteral("testprocessmemory"), QStringLiteral("self"));
// Base stays at 0 because attachViaPlugin doesn't touch it
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0);
delete m_plugin;
m_plugin = nullptr;
}
// ── dllFileName propagated through registry ──
void testDllFileNameInProviderInfo() {
m_plugin = new SelfProcessPlugin();
ProviderRegistry::instance().registerProvider(
"TestProcessMemory", "testprocessmemory", m_plugin, "MyPlugin.dll");
const auto& providers = ProviderRegistry::instance().providers();
bool found = false;
for (const auto& p : providers) {
if (p.identifier == "testprocessmemory") {
QCOMPARE(p.dllFileName, QStringLiteral("MyPlugin.dll"));
found = true;
break;
}
}
QVERIFY2(found, "testprocessmemory not found in provider list");
delete m_plugin;
m_plugin = nullptr;
}
};
QTEST_MAIN(TestSourceProvider)
#include "test_source_provider.moc"