fix: add missing test source files to repository

This commit is contained in:
IChooseYou
2026-02-28 12:54:38 -07:00
committed by IChooseYou
parent 851d744263
commit ecfac3decf
3 changed files with 1053 additions and 0 deletions

224
tests/bench_large_class.cpp Normal file
View 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"

View 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"

View 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"