mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
fix: add missing test source files to repository
This commit is contained in:
224
tests/bench_large_class.cpp
Normal file
224
tests/bench_large_class.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* bench_large_class — benchmark compose, applyDocument, hover highlight,
|
||||
* and selection overlay on a large struct (500+ fields).
|
||||
*
|
||||
* Simulates EPROCESS-class structures to measure editor performance.
|
||||
*/
|
||||
#include <QtTest/QtTest>
|
||||
#include <QElapsedTimer>
|
||||
#include "core.h"
|
||||
#include "editor.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
/* ── Build a large struct tree with N fields of mixed types ──────── */
|
||||
|
||||
static NodeTree buildLargeTree(int fieldCount)
|
||||
{
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x7FF600000000ULL;
|
||||
|
||||
// Root struct
|
||||
Node root;
|
||||
root.id = 1;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("EPROCESS");
|
||||
root.structTypeName = QStringLiteral("_EPROCESS");
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
tree.addNode(root);
|
||||
|
||||
// Cycle through common field types
|
||||
const NodeKind kinds[] = {
|
||||
NodeKind::Int32, NodeKind::UInt64, NodeKind::Float,
|
||||
NodeKind::Pointer64, NodeKind::Int16, NodeKind::UInt32,
|
||||
NodeKind::Double, NodeKind::Bool, NodeKind::Hex8
|
||||
};
|
||||
const int kindCount = sizeof(kinds) / sizeof(kinds[0]);
|
||||
|
||||
int offset = 0;
|
||||
for (int i = 0; i < fieldCount; ++i) {
|
||||
Node n;
|
||||
n.id = (uint64_t)(i + 2);
|
||||
n.kind = kinds[i % kindCount];
|
||||
n.name = QStringLiteral("field_%1").arg(i, 4, 10, QChar('0'));
|
||||
n.parentId = 1;
|
||||
n.offset = offset;
|
||||
tree.addNode(n);
|
||||
offset += sizeForKind(n.kind);
|
||||
}
|
||||
tree.m_nextId = (uint64_t)(fieldCount + 2);
|
||||
return tree;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
class BenchLargeClass : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
NodeTree m_tree;
|
||||
BufferProvider m_prov;
|
||||
ComposeResult m_result;
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void benchCompose();
|
||||
void benchApplyDocument();
|
||||
void benchHoverHighlight();
|
||||
void benchSelectionOverlay();
|
||||
void benchHoverHighlightRepeated();
|
||||
|
||||
public:
|
||||
BenchLargeClass() : m_prov(QByteArray()) {}
|
||||
};
|
||||
|
||||
void BenchLargeClass::initTestCase()
|
||||
{
|
||||
m_tree = buildLargeTree(500);
|
||||
|
||||
// Create buffer large enough for all fields
|
||||
QByteArray buf(0x10000, '\0');
|
||||
// Fill with pattern so values are non-zero
|
||||
for (int i = 0; i < buf.size(); ++i)
|
||||
buf[i] = (char)(i & 0xFF);
|
||||
m_prov = BufferProvider(buf, QStringLiteral("bench_data"));
|
||||
|
||||
// Pre-compose for tests that need the result
|
||||
m_result = rcx::compose(m_tree, m_prov);
|
||||
qDebug() << "Tree:" << m_tree.nodes.size() << "nodes,"
|
||||
<< m_result.meta.size() << "display lines,"
|
||||
<< m_result.text.size() << "chars";
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchCompose()
|
||||
{
|
||||
const int ITERS = 100;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
ComposeResult r = rcx::compose(m_tree, m_prov);
|
||||
Q_UNUSED(r);
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Compose Benchmark (500 fields) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-compose:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchApplyDocument()
|
||||
{
|
||||
RcxEditor editor;
|
||||
editor.resize(800, 600);
|
||||
|
||||
const int ITERS = 50;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
editor.applyDocument(m_result);
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== ApplyDocument Benchmark (500 fields) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-apply:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchHoverHighlight()
|
||||
{
|
||||
RcxEditor editor;
|
||||
editor.resize(800, 600);
|
||||
editor.applyDocument(m_result);
|
||||
|
||||
// Simulate hovering over the first field
|
||||
// We need access to internals, so we measure via public methods
|
||||
// by toggling selection which triggers applyHoverHighlight internally
|
||||
QSet<uint64_t> sel;
|
||||
sel.insert(2); // first field node id
|
||||
|
||||
const int ITERS = 200;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
editor.applySelectionOverlay(i % 2 == 0 ? sel : QSet<uint64_t>{});
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Hover/Selection Overlay Benchmark (500 fields) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-cycle:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchSelectionOverlay()
|
||||
{
|
||||
RcxEditor editor;
|
||||
editor.resize(800, 600);
|
||||
editor.applyDocument(m_result);
|
||||
|
||||
// Select many nodes (simulate multi-select of 50 fields)
|
||||
QSet<uint64_t> bigSel;
|
||||
for (int i = 0; i < 50; ++i)
|
||||
bigSel.insert((uint64_t)(i + 2));
|
||||
|
||||
const int ITERS = 100;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
editor.applySelectionOverlay(bigSel);
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Multi-Selection Overlay Benchmark (50 selected, 500 fields) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-overlay:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchHoverHighlightRepeated()
|
||||
{
|
||||
RcxEditor editor;
|
||||
editor.resize(800, 600);
|
||||
editor.applyDocument(m_result);
|
||||
|
||||
// Simulate rapid hover changes: alternate between two different nodes
|
||||
// This is the worst case - every call does a full marker clear + rescan
|
||||
QSet<uint64_t> empty;
|
||||
QSet<uint64_t> sel1; sel1.insert(10);
|
||||
QSet<uint64_t> sel2; sel2.insert(100);
|
||||
|
||||
const int ITERS = 500;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
editor.applySelectionOverlay(i % 3 == 0 ? sel1 : (i % 3 == 1 ? sel2 : empty));
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Rapid Hover Change Benchmark (500 fields, alternating nodes) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-change:" << (double)elapsed / ITERS << "ms";
|
||||
qDebug() << " Simulated events/sec:" << (ITERS * 1000.0 / elapsed);
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(BenchLargeClass)
|
||||
#include "bench_large_class.moc"
|
||||
320
tests/test_source_provider.cpp
Normal file
320
tests/test_source_provider.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#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"
|
||||
509
tests/test_static_fields.cpp
Normal file
509
tests/test_static_fields.cpp
Normal file
@@ -0,0 +1,509 @@
|
||||
#include <QtTest/QTest>
|
||||
#include "core.h"
|
||||
#include "addressparser.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestStaticFields : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
// Convenience: build a struct with N regular fields + static fields
|
||||
struct TestTree {
|
||||
NodeTree tree;
|
||||
uint64_t rootId = 0;
|
||||
|
||||
TestTree() {
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "TestStruct";
|
||||
root.structTypeName = "TestStruct";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
rootId = tree.nodes[ri].id;
|
||||
}
|
||||
|
||||
int addField(const QString& name, NodeKind kind, int offset) {
|
||||
Node f;
|
||||
f.kind = kind;
|
||||
f.name = name;
|
||||
f.parentId = rootId;
|
||||
f.offset = offset;
|
||||
return tree.addNode(f);
|
||||
}
|
||||
|
||||
int addStaticField(const QString& name, const QString& expr,
|
||||
NodeKind kind = NodeKind::Hex64) {
|
||||
Node h;
|
||||
h.kind = kind;
|
||||
h.name = name;
|
||||
h.parentId = rootId;
|
||||
h.offset = 0;
|
||||
h.isStatic = true;
|
||||
h.offsetExpr = expr;
|
||||
return tree.addNode(h);
|
||||
}
|
||||
};
|
||||
|
||||
private slots:
|
||||
|
||||
// ── Basic properties ──
|
||||
|
||||
void testStaticFieldFlag() {
|
||||
TestTree t;
|
||||
int hi = t.addStaticField("h", "base");
|
||||
QCOMPARE(t.tree.nodes[hi].isStatic, true);
|
||||
QCOMPARE(t.tree.nodes[hi].offsetExpr, QStringLiteral("base"));
|
||||
}
|
||||
|
||||
void testRegularFieldNotStatic() {
|
||||
TestTree t;
|
||||
int fi = t.addField("x", NodeKind::UInt32, 0);
|
||||
QCOMPARE(t.tree.nodes[fi].isStatic, false);
|
||||
QCOMPARE(t.tree.nodes[fi].offsetExpr, QString());
|
||||
}
|
||||
|
||||
void testStaticFieldIsChild() {
|
||||
TestTree t;
|
||||
int hi = t.addStaticField("h", "base");
|
||||
QCOMPARE(t.tree.nodes[hi].parentId, t.rootId);
|
||||
auto children = t.tree.childrenOf(t.rootId);
|
||||
QVERIFY(children.contains(hi));
|
||||
}
|
||||
|
||||
// ── JSON serialization ──
|
||||
|
||||
void testStaticFieldJsonRoundTrip() {
|
||||
TestTree t;
|
||||
t.addField("e_lfanew", NodeKind::UInt32, 0x3C);
|
||||
t.addStaticField("nt_hdr", "base + e_lfanew", NodeKind::Struct);
|
||||
|
||||
QJsonObject json = t.tree.toJson();
|
||||
NodeTree t2 = NodeTree::fromJson(json);
|
||||
|
||||
QCOMPARE(t2.nodes.size(), 3);
|
||||
// root
|
||||
QCOMPARE(t2.nodes[0].isStatic, false);
|
||||
// field
|
||||
QCOMPARE(t2.nodes[1].isStatic, false);
|
||||
QCOMPARE(t2.nodes[1].name, QStringLiteral("e_lfanew"));
|
||||
// static field
|
||||
QCOMPARE(t2.nodes[2].isStatic, true);
|
||||
QCOMPARE(t2.nodes[2].offsetExpr, QStringLiteral("base + e_lfanew"));
|
||||
QCOMPARE(t2.nodes[2].name, QStringLiteral("nt_hdr"));
|
||||
QCOMPARE(t2.nodes[2].kind, NodeKind::Struct);
|
||||
}
|
||||
|
||||
void testStaticFieldJsonBackwardCompat() {
|
||||
// Old JSON without isStatic should default to false
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Old";
|
||||
root.parentId = 0;
|
||||
tree.addNode(root);
|
||||
|
||||
QJsonObject json = tree.toJson();
|
||||
NodeTree t2 = NodeTree::fromJson(json);
|
||||
QCOMPARE(t2.nodes[0].isStatic, false);
|
||||
QCOMPARE(t2.nodes[0].offsetExpr, QString());
|
||||
}
|
||||
|
||||
void testMultipleStaticFieldsRoundTrip() {
|
||||
TestTree t;
|
||||
t.addField("ptr", NodeKind::Pointer64, 0);
|
||||
t.addStaticField("h1", "base");
|
||||
t.addStaticField("h2", "base + ptr");
|
||||
t.addStaticField("h3", "base + 0x100");
|
||||
|
||||
QJsonObject json = t.tree.toJson();
|
||||
NodeTree t2 = NodeTree::fromJson(json);
|
||||
|
||||
int staticFieldCount = 0;
|
||||
for (const auto& n : t2.nodes)
|
||||
if (n.isStatic) staticFieldCount++;
|
||||
QCOMPARE(staticFieldCount, 3);
|
||||
}
|
||||
|
||||
// ── Struct span exclusion ──
|
||||
|
||||
void testStructSpanExcludesStaticFields() {
|
||||
TestTree t;
|
||||
t.addField("a", NodeKind::UInt32, 0); // 0+4 = 4
|
||||
t.addField("b", NodeKind::UInt64, 4); // 4+8 = 12
|
||||
t.addStaticField("h", "base"); // should NOT affect span
|
||||
|
||||
QCOMPARE(t.tree.structSpan(t.rootId), 12);
|
||||
}
|
||||
|
||||
void testStructSpanWithOnlyStaticFields() {
|
||||
TestTree t;
|
||||
t.addStaticField("h1", "base");
|
||||
t.addStaticField("h2", "base + 0x100");
|
||||
|
||||
// No regular fields -> span = 0
|
||||
QCOMPARE(t.tree.structSpan(t.rootId), 0);
|
||||
}
|
||||
|
||||
void testStructSpanMixedOrder() {
|
||||
// Static fields interleaved with regular fields
|
||||
TestTree t;
|
||||
t.addField("x", NodeKind::Float, 0); // 0+4 = 4
|
||||
t.addStaticField("h1", "base");
|
||||
t.addField("y", NodeKind::Float, 4); // 4+4 = 8
|
||||
t.addStaticField("h2", "base + x");
|
||||
t.addField("z", NodeKind::Float, 8); // 8+4 = 12
|
||||
|
||||
QCOMPARE(t.tree.structSpan(t.rootId), 12);
|
||||
}
|
||||
|
||||
// ── Address expression evaluation ──
|
||||
|
||||
void testExprBase() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x1000; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0x1000);
|
||||
}
|
||||
|
||||
void testExprBaseAddHex() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x1000; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base + 0x3C", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0x103C);
|
||||
}
|
||||
|
||||
void testExprBaseAddField() {
|
||||
// Simulate: base=0x1000, e_lfanew value=0xE8
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x1000; }
|
||||
if (name == "e_lfanew") { *ok = true; return 0xE8; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base + e_lfanew", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0x10E8);
|
||||
}
|
||||
|
||||
void testExprSubtraction() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x2000; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base - 0x10", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0x1FF0);
|
||||
}
|
||||
|
||||
void testExprMultiplication() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x100; }
|
||||
if (name == "index") { *ok = true; return 3; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base + index * 8", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0x118); // 0x100 + 3*8
|
||||
}
|
||||
|
||||
void testExprUnresolvedIdentifier() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x1000; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base + unknown_field", 8, &cbs);
|
||||
QVERIFY(!r.ok);
|
||||
}
|
||||
|
||||
void testExprPureHex() {
|
||||
auto r = AddressParser::evaluate("0x7FF600000000", 8, nullptr);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0x7FF600000000ULL);
|
||||
}
|
||||
|
||||
void testExprParentheses() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x1000; }
|
||||
if (name == "offset") { *ok = true; return 0x10; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("(base + offset) * 2", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0x2020); // (0x1000 + 0x10) * 2
|
||||
}
|
||||
|
||||
void testExprEmptyString() {
|
||||
auto r = AddressParser::evaluate("", 8, nullptr);
|
||||
QVERIFY(!r.ok);
|
||||
}
|
||||
|
||||
// ── Static field with BufferProvider (simulates live resolution) ──
|
||||
|
||||
void testStaticFieldResolveFromBuffer() {
|
||||
// Build tree: struct with UInt32 "offset_field" at +0x10
|
||||
// Static field expression: "base + offset_field"
|
||||
// Buffer has 0x000000E8 at address 0x10
|
||||
QByteArray data(64, '\0');
|
||||
// Write 0xE8 at offset 0x10 (little-endian uint32)
|
||||
data[0x10] = (char)0xE8;
|
||||
data[0x11] = 0;
|
||||
data[0x12] = 0;
|
||||
data[0x13] = 0;
|
||||
|
||||
BufferProvider prov(data);
|
||||
|
||||
// Build resolver mimicking compose.cpp's makeResolver
|
||||
uint64_t baseAddr = 0; // buffer starts at 0
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [&prov, baseAddr](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return baseAddr; }
|
||||
if (name == "offset_field") {
|
||||
uint64_t addr = baseAddr + 0x10;
|
||||
if (prov.isReadable(addr, 4)) {
|
||||
*ok = true;
|
||||
return (uint64_t)prov.readU32(addr);
|
||||
}
|
||||
}
|
||||
*ok = false; return 0;
|
||||
};
|
||||
|
||||
auto r = AddressParser::evaluate("base + offset_field", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0xE8); // 0 + 0xE8
|
||||
}
|
||||
|
||||
void testStaticFieldResolvePointerChain() {
|
||||
// Buffer: addr 0x00 has pointer to 0x20, addr 0x20 has pointer to 0x40
|
||||
QByteArray data(64, '\0');
|
||||
// Write pointer at 0x00 -> 0x20 (little-endian uint64)
|
||||
data[0x00] = 0x20;
|
||||
// Write pointer at 0x20 -> 0x40
|
||||
data[0x20] = 0x40;
|
||||
|
||||
BufferProvider prov(data);
|
||||
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
cbs.readPointer = [&prov](uint64_t addr, bool* ok) -> uint64_t {
|
||||
if (prov.isReadable(addr, 8)) {
|
||||
*ok = true;
|
||||
return prov.readU64(addr);
|
||||
}
|
||||
*ok = false; return 0;
|
||||
};
|
||||
|
||||
// [base] = deref pointer at base (0x00) -> 0x20
|
||||
auto r = AddressParser::evaluate("[base]", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, (uint64_t)0x20);
|
||||
|
||||
// [[base]] = double deref: [0x00]->0x20, [0x20]->0x40
|
||||
auto r2 = AddressParser::evaluate("[[base]]", 8, &cbs);
|
||||
QVERIFY(r2.ok);
|
||||
QCOMPARE(r2.value, (uint64_t)0x40);
|
||||
}
|
||||
|
||||
// ── Compose output ──
|
||||
|
||||
void testComposeStaticFieldHeader() {
|
||||
TestTree t;
|
||||
t.addField("x", NodeKind::Float, 0);
|
||||
t.addStaticField("h", "base");
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(t.tree, prov);
|
||||
|
||||
// Static field header should contain "static" keyword
|
||||
bool foundStatic = false;
|
||||
QStringList lines = result.text.split('\n');
|
||||
for (const auto& line : lines) {
|
||||
if (line.contains(QStringLiteral("static ")) && line.contains(QStringLiteral("{"))) {
|
||||
foundStatic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundStatic, "Static field header line not found in compose output");
|
||||
}
|
||||
|
||||
void testComposeStaticFieldLine() {
|
||||
TestTree t;
|
||||
t.addField("x", NodeKind::Float, 0);
|
||||
t.addStaticField("h", "base");
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(t.tree, prov);
|
||||
|
||||
// Find the static field line and check its meta
|
||||
bool foundStaticField = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.isStaticLine) {
|
||||
foundStaticField = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundStaticField, "Static field line metadata not found in compose output");
|
||||
}
|
||||
|
||||
void testComposeNoStaticFieldsWhenCollapsed() {
|
||||
TestTree t;
|
||||
t.addField("x", NodeKind::Float, 0);
|
||||
t.addStaticField("h", "base");
|
||||
// Collapse the root struct
|
||||
t.tree.nodes[0].collapsed = true;
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(t.tree, prov);
|
||||
|
||||
// When collapsed, no static field lines should appear
|
||||
QStringList lines = result.text.split('\n');
|
||||
for (const auto& lm : result.meta)
|
||||
QVERIFY2(!lm.isStaticLine,
|
||||
"Static field line should not appear when struct is collapsed");
|
||||
}
|
||||
|
||||
void testComposeStaticFieldExprDisplay() {
|
||||
TestTree t;
|
||||
t.addField("offset", NodeKind::UInt32, 0);
|
||||
t.addStaticField("target", "base + offset");
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(t.tree, prov);
|
||||
|
||||
// Static field line should contain the expression text
|
||||
bool foundExpr = false;
|
||||
QStringList lines = result.text.split('\n');
|
||||
for (const auto& line : lines) {
|
||||
if (line.contains(QStringLiteral("base + offset"))) {
|
||||
foundExpr = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundExpr, "Static field expression not found in compose output");
|
||||
}
|
||||
|
||||
void testComposeStaticFieldsAfterRegularFields() {
|
||||
TestTree t;
|
||||
t.addField("a", NodeKind::UInt32, 0);
|
||||
t.addField("b", NodeKind::UInt64, 4);
|
||||
t.addStaticField("h", "base");
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(t.tree, prov);
|
||||
|
||||
// Find meta indices: last regular field vs first static field line
|
||||
int lastFieldMeta = -1;
|
||||
int firstStaticFieldMeta = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].lineKind == LineKind::Field
|
||||
&& !result.meta[i].isStaticLine)
|
||||
lastFieldMeta = i;
|
||||
if (result.meta[i].isStaticLine && firstStaticFieldMeta < 0)
|
||||
firstStaticFieldMeta = i;
|
||||
}
|
||||
|
||||
QVERIFY(lastFieldMeta >= 0);
|
||||
QVERIFY(firstStaticFieldMeta >= 0);
|
||||
QVERIFY2(firstStaticFieldMeta > lastFieldMeta,
|
||||
"Static field lines must come after all regular fields");
|
||||
}
|
||||
|
||||
// ── Node byteSize for static fields ──
|
||||
|
||||
void testStaticFieldByteSize() {
|
||||
// Static field nodes should still report their kind's byte size
|
||||
Node h;
|
||||
h.kind = NodeKind::Hex64;
|
||||
h.isStatic = true;
|
||||
h.offsetExpr = "base";
|
||||
QCOMPARE(h.byteSize(), 8);
|
||||
|
||||
h.kind = NodeKind::Struct;
|
||||
QCOMPARE(h.byteSize(), 0); // struct static fields have 0 size (children determine)
|
||||
}
|
||||
|
||||
// ── Children ordering ──
|
||||
|
||||
void testChildrenOfIncludesStaticFields() {
|
||||
TestTree t;
|
||||
t.addField("a", NodeKind::UInt32, 0);
|
||||
int hi = t.addStaticField("h", "base");
|
||||
|
||||
auto children = t.tree.childrenOf(t.rootId);
|
||||
QCOMPARE(children.size(), 2);
|
||||
QVERIFY(children.contains(hi));
|
||||
}
|
||||
|
||||
// ── Edge cases ──
|
||||
|
||||
void testStaticFieldWithEmptyExpr() {
|
||||
TestTree t;
|
||||
Node h;
|
||||
h.kind = NodeKind::Hex64;
|
||||
h.name = "h";
|
||||
h.parentId = t.rootId;
|
||||
h.isStatic = true;
|
||||
h.offsetExpr = QString(); // empty expression
|
||||
int hi = t.tree.addNode(h);
|
||||
|
||||
QCOMPARE(t.tree.nodes[hi].isStatic, true);
|
||||
QCOMPARE(t.tree.nodes[hi].offsetExpr, QString());
|
||||
|
||||
// JSON round-trip should preserve empty expr
|
||||
QJsonObject json = t.tree.toJson();
|
||||
NodeTree t2 = NodeTree::fromJson(json);
|
||||
QCOMPARE(t2.nodes[hi].isStatic, true);
|
||||
}
|
||||
|
||||
void testStaticFieldStructType() {
|
||||
// Static field can be a struct (pointing to a different address)
|
||||
TestTree t;
|
||||
int hi = t.addStaticField("nt_headers", "base + 0xE8", NodeKind::Struct);
|
||||
t.tree.nodes[hi].structTypeName = "IMAGE_NT_HEADERS";
|
||||
|
||||
QCOMPARE(t.tree.nodes[hi].kind, NodeKind::Struct);
|
||||
QCOMPARE(t.tree.nodes[hi].isStatic, true);
|
||||
QCOMPARE(t.tree.nodes[hi].structTypeName, QStringLiteral("IMAGE_NT_HEADERS"));
|
||||
}
|
||||
|
||||
void testStaticFieldPointerType() {
|
||||
// Static field can be a pointer type
|
||||
TestTree t;
|
||||
int hi = t.addStaticField("indirect", "base + 0x20", NodeKind::Pointer64);
|
||||
|
||||
QCOMPARE(t.tree.nodes[hi].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(t.tree.nodes[hi].isStatic, true);
|
||||
}
|
||||
|
||||
// ── Validate expression syntax ──
|
||||
|
||||
void testExprValidate() {
|
||||
// Valid expressions
|
||||
QCOMPARE(AddressParser::validate("base"), QString());
|
||||
QCOMPARE(AddressParser::validate("base + 0x10"), QString());
|
||||
QCOMPARE(AddressParser::validate("0x1000"), QString());
|
||||
QCOMPARE(AddressParser::validate("(base + offset) * 2"), QString());
|
||||
|
||||
// Invalid expressions
|
||||
QVERIFY(!AddressParser::validate("").isEmpty());
|
||||
QVERIFY(!AddressParser::validate("+ +").isEmpty());
|
||||
QVERIFY(!AddressParser::validate("(base").isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestStaticFields)
|
||||
#include "test_static_fields.moc"
|
||||
Reference in New Issue
Block a user