From ecfac3decf0ed521e35f8119d55daa2c6e5b6a3b Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Sat, 28 Feb 2026 12:54:38 -0700 Subject: [PATCH] fix: add missing test source files to repository --- tests/bench_large_class.cpp | 224 +++++++++++++++ tests/test_source_provider.cpp | 320 +++++++++++++++++++++ tests/test_static_fields.cpp | 509 +++++++++++++++++++++++++++++++++ 3 files changed, 1053 insertions(+) create mode 100644 tests/bench_large_class.cpp create mode 100644 tests/test_source_provider.cpp create mode 100644 tests/test_static_fields.cpp diff --git a/tests/bench_large_class.cpp b/tests/bench_large_class.cpp new file mode 100644 index 0000000..df2bbda --- /dev/null +++ b/tests/bench_large_class.cpp @@ -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 +#include +#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 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{}); + } + 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 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 empty; + QSet sel1; sel1.insert(10); + QSet 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" diff --git a/tests/test_source_provider.cpp b/tests/test_source_provider.cpp new file mode 100644 index 0000000..cb0de3f --- /dev/null +++ b/tests/test_source_provider.cpp @@ -0,0 +1,320 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "controller.h" +#include "core.h" +#include "providerregistry.h" +#include "providers/null_provider.h" +#include "providers/buffer_provider.h" +#ifdef Q_OS_WIN +#include +#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 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(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" diff --git a/tests/test_static_fields.cpp b/tests/test_static_fields.cpp new file mode 100644 index 0000000..cc514fd --- /dev/null +++ b/tests/test_static_fields.cpp @@ -0,0 +1,509 @@ +#include +#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"