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