Initial commit: ReclassX structured binary editor

This commit is contained in:
sysadmin
2026-02-01 11:37:32 -07:00
commit 0be67c8396
786 changed files with 473499 additions and 0 deletions

674
tests/test_compose.cpp Normal file
View File

@@ -0,0 +1,674 @@
#include <QtTest/QTest>
#include "core.h"
using namespace rcx;
class TestCompose : public QObject {
Q_OBJECT
private slots:
void testBasicStruct() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f1;
f1.kind = NodeKind::Hex32;
f1.name = "field_0";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
Node f2;
f2.kind = NodeKind::Float;
f2.name = "value";
f2.parentId = rootId;
f2.offset = 4;
tree.addNode(f2);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Header + 2 fields + footer = 4 lines
QCOMPARE(result.meta.size(), 4);
// Header is fold head
QVERIFY(result.meta[0].foldHead);
QCOMPARE(result.meta[0].lineKind, LineKind::Header);
// Fields are not fold heads
QVERIFY(!result.meta[1].foldHead);
QVERIFY(!result.meta[2].foldHead);
// Footer
QCOMPARE(result.meta[3].lineKind, LineKind::Footer);
// Offset text
QCOMPARE(result.meta[0].offsetText, QString("+0x0"));
QCOMPARE(result.meta[1].offsetText, QString("+0x0"));
QCOMPARE(result.meta[2].offsetText, QString("+0x4"));
// Header is expanded by default (fold indicator in line text)
QVERIFY(!result.meta[0].foldCollapsed);
}
void testVec3Continuation() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node v;
v.kind = NodeKind::Vec3;
v.name = "pos";
v.parentId = rootId;
v.offset = 0;
tree.addNode(v);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Header + 3 Vec3 lines + footer = 5 lines
QCOMPARE(result.meta.size(), 5);
// Line 1 (first Vec3 component): not continuation
QVERIFY(!result.meta[1].isContinuation);
QCOMPARE(result.meta[1].offsetText, QString("+0x0"));
// Lines 2-3: continuation
QVERIFY(result.meta[2].isContinuation);
QCOMPARE(result.meta[2].offsetText, QString(" \u00B7"));
QVERIFY(result.meta[3].isContinuation);
QCOMPARE(result.meta[3].offsetText, QString(" \u00B7"));
// Continuation marker
QVERIFY(result.meta[2].markerMask & (1u << M_CONT));
QVERIFY(result.meta[3].markerMask & (1u << M_CONT));
}
void testPaddingMarker() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "R";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node pad;
pad.kind = NodeKind::Padding;
pad.name = "pad";
pad.parentId = rootId;
pad.offset = 0;
tree.addNode(pad);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Header + padding + footer = 3
QCOMPARE(result.meta.size(), 3);
QVERIFY(result.meta[1].markerMask & (1u << M_PAD));
}
void testNullPointerMarker() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "R";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "ptr";
ptr.parentId = rootId;
ptr.offset = 0;
tree.addNode(ptr);
// Provider with zeros (null ptr)
QByteArray data(64, '\0');
FileProvider prov(data);
ComposeResult result = compose(tree, prov);
QCOMPARE(result.meta.size(), 3);
QVERIFY(result.meta[1].markerMask & (1u << M_PTR0));
}
void testCollapsedStruct() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
root.collapsed = true;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f;
f.kind = NodeKind::Hex32;
f.name = "field";
f.parentId = rootId;
f.offset = 0;
tree.addNode(f);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Collapsed: header + footer only = 2 lines
QCOMPARE(result.meta.size(), 2);
QVERIFY(result.meta[0].foldHead);
}
void testUnreadablePointerNoRead() {
// A pointer at an unreadable address should get M_ERR, not M_PTR0
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "R";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "ptr";
ptr.parentId = rootId;
ptr.offset = 0;
tree.addNode(ptr);
// Provider with only 4 bytes — not enough for Pointer64 (8 bytes)
QByteArray data(4, '\0');
FileProvider prov(data);
ComposeResult result = compose(tree, prov);
QCOMPARE(result.meta.size(), 3);
// Should have M_ERR, should NOT have M_PTR0
QVERIFY(result.meta[1].markerMask & (1u << M_ERR));
QVERIFY(!(result.meta[1].markerMask & (1u << M_PTR0)));
}
void testFoldLevels() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node child;
child.kind = NodeKind::Struct;
child.name = "Child";
child.parentId = rootId;
child.offset = 0;
int ci = tree.addNode(child);
uint64_t childId = tree.nodes[ci].id;
Node leaf;
leaf.kind = NodeKind::Hex8;
leaf.name = "x";
leaf.parentId = childId;
leaf.offset = 0;
tree.addNode(leaf);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Root header (depth 0, head) -> 0x400 | 0x2000
QCOMPARE(result.meta[0].foldLevel, 0x400 | 0x2000);
QCOMPARE(result.meta[0].depth, 0);
// Child header (depth 1, head) -> 0x401 | 0x2000
QCOMPARE(result.meta[1].foldLevel, 0x401 | 0x2000);
QCOMPARE(result.meta[1].depth, 1);
// Leaf (depth 2, not head) -> 0x402
QCOMPARE(result.meta[2].foldLevel, 0x402);
QCOMPARE(result.meta[2].depth, 2);
}
void testNestedStruct() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Outer";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f1;
f1.kind = NodeKind::UInt32;
f1.name = "flags";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
Node inner;
inner.kind = NodeKind::Struct;
inner.name = "Inner";
inner.parentId = rootId;
inner.offset = 4;
int ii = tree.addNode(inner);
uint64_t innerId = tree.nodes[ii].id;
Node f2;
f2.kind = NodeKind::UInt16;
f2.name = "x";
f2.parentId = innerId;
f2.offset = 0;
tree.addNode(f2);
Node f3;
f3.kind = NodeKind::UInt16;
f3.name = "y";
f3.parentId = innerId;
f3.offset = 2;
tree.addNode(f3);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Outer header + flags + Inner header + x + y + Inner footer + Outer footer = 7
QCOMPARE(result.meta.size(), 7);
// Outer header
QCOMPARE(result.meta[0].lineKind, LineKind::Header);
QCOMPARE(result.meta[0].depth, 0);
QVERIFY(result.meta[0].foldHead);
// flags field
QCOMPARE(result.meta[1].lineKind, LineKind::Field);
QCOMPARE(result.meta[1].depth, 1);
// Inner header
QCOMPARE(result.meta[2].lineKind, LineKind::Header);
QCOMPARE(result.meta[2].depth, 1);
QVERIFY(result.meta[2].foldHead);
QCOMPARE(result.meta[2].foldLevel, 0x401 | 0x2000);
// Inner fields at depth 2
QCOMPARE(result.meta[3].depth, 2);
QCOMPARE(result.meta[3].foldLevel, 0x402);
QCOMPARE(result.meta[4].depth, 2);
// Inner footer
QCOMPARE(result.meta[5].lineKind, LineKind::Footer);
QCOMPARE(result.meta[5].depth, 1);
// Outer footer
QCOMPARE(result.meta[6].lineKind, LineKind::Footer);
QCOMPARE(result.meta[6].depth, 0);
}
void testPointerDerefExpansion() {
NodeTree tree;
tree.baseAddress = 0;
// Main struct
Node main;
main.kind = NodeKind::Struct;
main.name = "Main";
main.parentId = 0;
main.offset = 0;
int mi = tree.addNode(main);
uint64_t mainId = tree.nodes[mi].id;
Node magic;
magic.kind = NodeKind::UInt32;
magic.name = "magic";
magic.parentId = mainId;
magic.offset = 0;
tree.addNode(magic);
// Template struct (separate root)
Node tmpl;
tmpl.kind = NodeKind::Struct;
tmpl.name = "VTable";
tmpl.parentId = 0;
tmpl.offset = 200; // far away so standalone rendering uses offset 200
int ti = tree.addNode(tmpl);
uint64_t tmplId = tree.nodes[ti].id;
Node fn1;
fn1.kind = NodeKind::UInt64;
fn1.name = "fn_one";
fn1.parentId = tmplId;
fn1.offset = 0;
tree.addNode(fn1);
Node fn2;
fn2.kind = NodeKind::UInt64;
fn2.name = "fn_two";
fn2.parentId = tmplId;
fn2.offset = 8;
tree.addNode(fn2);
// Pointer in Main referencing VTable
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "vtable_ptr";
ptr.parentId = mainId;
ptr.offset = 4;
ptr.refId = tmplId;
tree.addNode(ptr);
// Provider: pointer at offset 4 points to address 100
QByteArray data(256, '\0');
uint64_t ptrVal = 100;
memcpy(data.data() + 4, &ptrVal, 8);
// Some data at the pointer target
uint64_t v1 = 0xDEADBEEF;
memcpy(data.data() + 100, &v1, 8);
uint64_t v2 = 0xCAFEBABE;
memcpy(data.data() + 108, &v2, 8);
FileProvider prov(data);
ComposeResult result = compose(tree, prov);
// Main: header + magic + ptr(fold head) + VTable header + fn1 + fn2 + VTable footer + Main footer = 8
// VTable standalone: header + fn1 + fn2 + footer = 4
// Total = 12
QCOMPARE(result.meta.size(), 12);
// Main header
QCOMPARE(result.meta[0].lineKind, LineKind::Header);
QCOMPARE(result.meta[0].depth, 0);
// magic field
QCOMPARE(result.meta[1].lineKind, LineKind::Field);
QCOMPARE(result.meta[1].depth, 1);
// Pointer as fold head
QCOMPARE(result.meta[2].lineKind, LineKind::Field);
QCOMPARE(result.meta[2].depth, 1);
QVERIFY(result.meta[2].foldHead);
QCOMPARE(result.meta[2].nodeKind, NodeKind::Pointer64);
// Expanded VTable header at depth 2
QCOMPARE(result.meta[3].lineKind, LineKind::Header);
QCOMPARE(result.meta[3].depth, 2);
// Expanded fields at depth 3
QCOMPARE(result.meta[4].depth, 3);
QCOMPARE(result.meta[5].depth, 3);
// Expanded VTable footer
QCOMPARE(result.meta[6].lineKind, LineKind::Footer);
QCOMPARE(result.meta[6].depth, 2);
// Main footer
QCOMPARE(result.meta[7].lineKind, LineKind::Footer);
QCOMPARE(result.meta[7].depth, 0);
}
void testPointerDerefNull() {
NodeTree tree;
tree.baseAddress = 0;
Node main;
main.kind = NodeKind::Struct;
main.name = "Main";
main.parentId = 0;
main.offset = 0;
int mi = tree.addNode(main);
uint64_t mainId = tree.nodes[mi].id;
Node tmpl;
tmpl.kind = NodeKind::Struct;
tmpl.name = "Target";
tmpl.parentId = 0;
tmpl.offset = 200;
int ti = tree.addNode(tmpl);
uint64_t tmplId = tree.nodes[ti].id;
Node tf;
tf.kind = NodeKind::UInt32;
tf.name = "field";
tf.parentId = tmplId;
tf.offset = 0;
tree.addNode(tf);
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "ptr";
ptr.parentId = mainId;
ptr.offset = 0;
ptr.refId = tmplId;
tree.addNode(ptr);
// All zeros = null pointer
QByteArray data(256, '\0');
FileProvider prov(data);
ComposeResult result = compose(tree, prov);
// Main: header + ptr(fold head, no expansion) + footer = 3
// Target standalone: header + field + footer = 3
// Total = 6
QCOMPARE(result.meta.size(), 6);
// Pointer is fold head but has no children (null ptr)
QCOMPARE(result.meta[1].lineKind, LineKind::Field);
QVERIFY(result.meta[1].foldHead);
// Next line is Main footer (no expansion)
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
QCOMPARE(result.meta[2].depth, 0);
}
void testPointerDerefCollapsed() {
NodeTree tree;
tree.baseAddress = 0;
Node main;
main.kind = NodeKind::Struct;
main.name = "Main";
main.parentId = 0;
main.offset = 0;
int mi = tree.addNode(main);
uint64_t mainId = tree.nodes[mi].id;
Node tmpl;
tmpl.kind = NodeKind::Struct;
tmpl.name = "Target";
tmpl.parentId = 0;
tmpl.offset = 200;
int ti = tree.addNode(tmpl);
uint64_t tmplId = tree.nodes[ti].id;
Node tf;
tf.kind = NodeKind::UInt32;
tf.name = "field";
tf.parentId = tmplId;
tf.offset = 0;
tree.addNode(tf);
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "ptr";
ptr.parentId = mainId;
ptr.offset = 0;
ptr.refId = tmplId;
ptr.collapsed = true; // collapsed
tree.addNode(ptr);
// Non-null pointer
QByteArray data(256, '\0');
uint64_t ptrVal = 100;
memcpy(data.data(), &ptrVal, 8);
FileProvider prov(data);
ComposeResult result = compose(tree, prov);
// Main: header + ptr(fold head, collapsed) + footer = 3
// Target standalone: header + field + footer = 3
// Total = 6
QCOMPARE(result.meta.size(), 6);
// Pointer is fold head
QVERIFY(result.meta[1].foldHead);
// No expansion — next is Main footer
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
QCOMPARE(result.meta[2].depth, 0);
}
void testPointerDerefCycle() {
NodeTree tree;
tree.baseAddress = 0;
Node main;
main.kind = NodeKind::Struct;
main.name = "Main";
main.parentId = 0;
main.offset = 0;
int mi = tree.addNode(main);
uint64_t mainId = tree.nodes[mi].id;
// Template struct with a self-referencing pointer
Node tmpl;
tmpl.kind = NodeKind::Struct;
tmpl.name = "Recursive";
tmpl.parentId = 0;
tmpl.offset = 200;
int ti = tree.addNode(tmpl);
uint64_t tmplId = tree.nodes[ti].id;
Node tf;
tf.kind = NodeKind::UInt32;
tf.name = "data";
tf.parentId = tmplId;
tf.offset = 0;
tree.addNode(tf);
// Self-referencing pointer inside the template
Node backPtr;
backPtr.kind = NodeKind::Pointer64;
backPtr.name = "self";
backPtr.parentId = tmplId;
backPtr.offset = 4;
backPtr.refId = tmplId; // points back to same struct
tree.addNode(backPtr);
// Pointer in Main → Recursive
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "ptr";
ptr.parentId = mainId;
ptr.offset = 0;
ptr.refId = tmplId;
tree.addNode(ptr);
// Provider: main ptr at offset 0 points to 100
// Inside expansion: backPtr at offset 100+4=104 also points to 100
QByteArray data(256, '\0');
uint64_t ptrVal = 100;
memcpy(data.data(), &ptrVal, 8); // main ptr → 100
memcpy(data.data() + 104, &ptrVal, 8); // backPtr at 104 → 100
FileProvider prov(data);
ComposeResult result = compose(tree, prov);
// Must not infinite-loop. Verify we got a finite result.
QVERIFY(result.meta.size() > 0);
QVERIFY(result.meta.size() < 100); // sanity: bounded output
// First expansion happens: Main header + ptr fold head + Recursive header + data + backPtr fold head
// Second expansion blocked by cycle guard: no children under backPtr
// Then: Recursive footer + Main footer
// Plus standalone Recursive rendering
// The exact count depends on cycle guard behavior but must be finite
QCOMPARE(result.meta[0].lineKind, LineKind::Header); // Main header
QVERIFY(result.meta[1].foldHead); // ptr fold head
QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion)
}
void testStructFooterSizeof() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Sized";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f1;
f1.kind = NodeKind::UInt32;
f1.name = "a";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
Node f2;
f2.kind = NodeKind::UInt64;
f2.name = "b";
f2.parentId = rootId;
f2.offset = 4;
tree.addNode(f2);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// Footer is the last line
int lastLine = result.meta.size() - 1;
QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer);
// Footer text should contain sizeof=0xC (4+8=12=0xC)
QString footerText = result.text.split('\n').last();
QVERIFY(footerText.contains("sizeof=0xC"));
}
void testLineMetaHasNodeId() {
using namespace rcx;
NodeTree tree;
tree.baseAddress = 0;
Node root; root.kind = NodeKind::Struct; root.name = "Root"; root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f; f.kind = NodeKind::Hex32; f.name = "x"; f.parentId = rootId; f.offset = 0;
tree.addNode(f);
NullProvider prov;
ComposeResult result = compose(tree, prov);
for (int i = 0; i < result.meta.size(); i++) {
QVERIFY2(result.meta[i].nodeId != 0,
qPrintable(QString("Line %1 has nodeId=0").arg(i)));
int ni = result.meta[i].nodeIdx;
QVERIFY(ni >= 0 && ni < tree.nodes.size());
QCOMPARE(result.meta[i].nodeId, tree.nodes[ni].id);
}
}
};
QTEST_MAIN(TestCompose)
#include "test_compose.moc"

557
tests/test_core.cpp Normal file
View File

@@ -0,0 +1,557 @@
#include <QtTest/QTest>
#include "core.h"
class TestCore : public QObject {
Q_OBJECT
private slots:
void testSizeForKind() {
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Hex8), 1);
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Hex16), 2);
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Hex32), 4);
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Hex64), 8);
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Float), 4);
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Double), 8);
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Vec3), 12);
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Mat4x4), 64);
QCOMPARE(rcx::sizeForKind(rcx::NodeKind::Struct), 0);
}
void testLinesForKind() {
QCOMPARE(rcx::linesForKind(rcx::NodeKind::Hex32), 1);
QCOMPARE(rcx::linesForKind(rcx::NodeKind::Vec2), 2);
QCOMPARE(rcx::linesForKind(rcx::NodeKind::Vec3), 3);
QCOMPARE(rcx::linesForKind(rcx::NodeKind::Vec4), 4);
QCOMPARE(rcx::linesForKind(rcx::NodeKind::Mat4x4), 4);
}
void testKindStringRoundTrip() {
for (int i = 0; i <= static_cast<int>(rcx::NodeKind::Array); i++) {
auto kind = static_cast<rcx::NodeKind>(i);
QString s = rcx::kindToString(kind);
QCOMPARE(rcx::kindFromString(s), kind);
}
}
void testNodeTree_addAndChildren() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
QCOMPARE(ri, 0);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node child;
child.kind = rcx::NodeKind::Hex32;
child.name = "field";
child.parentId = rootId;
child.offset = 0;
tree.addNode(child);
auto children = tree.childrenOf(rootId);
QCOMPARE(children.size(), 1);
QCOMPARE(children[0], 1);
auto roots = tree.childrenOf(0);
QCOMPARE(roots.size(), 1);
QCOMPARE(roots[0], 0);
}
void testNodeTree_depth() {
rcx::NodeTree tree;
rcx::Node a; a.kind = rcx::NodeKind::Struct; a.name = "A"; a.parentId = 0;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
rcx::Node b; b.kind = rcx::NodeKind::Struct; b.name = "B"; b.parentId = aId;
int bi = tree.addNode(b);
uint64_t bId = tree.nodes[bi].id;
rcx::Node c; c.kind = rcx::NodeKind::Hex8; c.name = "c"; c.parentId = bId;
tree.addNode(c);
QCOMPARE(tree.depthOf(0), 0);
QCOMPARE(tree.depthOf(1), 1);
QCOMPARE(tree.depthOf(2), 2);
}
void testNodeTree_computeOffset() {
rcx::NodeTree tree;
tree.baseAddress = 0x1000;
rcx::Node root; root.kind = rcx::NodeKind::Struct; root.name = "R";
root.parentId = 0; root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node f; f.kind = rcx::NodeKind::Hex32; f.name = "f";
f.parentId = rootId; f.offset = 16;
tree.addNode(f);
QCOMPARE(tree.computeOffset(1), 16);
}
void testNodeTree_jsonRoundTrip() {
rcx::NodeTree tree;
tree.baseAddress = 0xDEAD;
rcx::Node root; root.kind = rcx::NodeKind::Struct; root.name = "Test";
root.parentId = 0; root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node child; child.kind = rcx::NodeKind::Float; child.name = "val";
child.parentId = rootId; child.offset = 8;
tree.addNode(child);
QJsonObject json = tree.toJson();
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json);
QCOMPARE(tree2.baseAddress, (uint64_t)0xDEAD);
QCOMPARE(tree2.nodes.size(), 2);
QCOMPARE(tree2.nodes[0].name, QString("Test"));
QCOMPARE(tree2.nodes[1].kind, rcx::NodeKind::Float);
QCOMPARE(tree2.nodes[1].offset, 8);
}
void testFileProvider() {
QByteArray data(16, '\0');
data[0] = 0x42;
data[4] = 0x10;
data[5] = 0x20;
rcx::FileProvider prov(data);
QVERIFY(prov.isValid());
QCOMPARE(prov.size(), 16);
QCOMPARE(prov.readU8(0), (uint8_t)0x42);
QCOMPARE(prov.readU16(4), (uint16_t)0x2010);
}
void testNullProvider() {
rcx::NullProvider prov;
QVERIFY(!prov.isValid());
QVERIFY(!prov.isReadable(0, 1));
QCOMPARE(prov.readU8(0), (uint8_t)0);
QCOMPARE(prov.readU32(0), (uint32_t)0);
}
void testIsReadable() {
QByteArray data(16, '\0');
rcx::FileProvider prov(data);
QVERIFY(prov.isReadable(0, 4));
QVERIFY(prov.isReadable(0, 16));
QVERIFY(!prov.isReadable(0, 17));
QVERIFY(!prov.isReadable(15, 2));
QVERIFY(prov.isReadable(15, 1));
}
void testStableNodeIds() {
rcx::NodeTree tree;
rcx::Node a; a.kind = rcx::NodeKind::Struct; a.name = "A"; a.parentId = 0;
int ai = tree.addNode(a);
QCOMPARE(tree.nodes[ai].id, (uint64_t)1);
rcx::Node b; b.kind = rcx::NodeKind::Hex32; b.name = "B"; b.parentId = tree.nodes[ai].id;
int bi = tree.addNode(b);
QCOMPARE(tree.nodes[bi].id, (uint64_t)2);
QCOMPARE(tree.indexOfId(1), 0);
QCOMPARE(tree.indexOfId(2), 1);
QCOMPARE(tree.indexOfId(99), -1);
}
void testByteSizeDynamic() {
rcx::Node n;
n.kind = rcx::NodeKind::UTF8;
n.strLen = 128;
QCOMPARE(n.byteSize(), 128);
n.kind = rcx::NodeKind::UTF16;
n.strLen = 32;
QCOMPARE(n.byteSize(), 64); // 32 * 2
n.kind = rcx::NodeKind::Float;
QCOMPARE(n.byteSize(), 4); // falls back to sizeForKind
}
void testSubtreeCycleSafe() {
rcx::NodeTree tree;
rcx::Node a; a.kind = rcx::NodeKind::Struct; a.name = "A"; a.parentId = 0;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
// Create a child that points back to A's id as parent — not a cycle per se,
// but test that subtree collection terminates
rcx::Node b; b.kind = rcx::NodeKind::Hex8; b.name = "B"; b.parentId = aId;
tree.addNode(b);
// Should return both nodes without hanging
auto sub = tree.subtreeIndices(aId);
QCOMPARE(sub.size(), 2);
QVERIFY(sub.contains(0));
QVERIFY(sub.contains(1));
}
void testIsReadableOverflow() {
QByteArray data(16, '\0');
rcx::FileProvider prov(data);
// Normal cases
QVERIFY(prov.isReadable(0, 16));
QVERIFY(!prov.isReadable(0, 17));
// Large address
QVERIFY(!prov.isReadable(0xFFFFFFFFFFFFFFFFULL, 1));
// Negative len
QVERIFY(!prov.isReadable(0, -1));
// Zero len is readable
QVERIFY(prov.isReadable(0, 0));
QVERIFY(prov.isReadable(16, 0));
}
void testAlignmentFor() {
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Hex8), 1);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Hex16), 2);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Hex32), 4);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Hex64), 8);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Float), 4);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Double), 8);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Vec3), 4);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Mat4x4), 4);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::UTF8), 1);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::UTF16), 2);
QCOMPARE(rcx::alignmentFor(rcx::NodeKind::Struct), 1);
}
void testDepthOfCycle() {
rcx::NodeTree tree;
// Create two nodes that reference each other as parents
rcx::Node a; a.kind = rcx::NodeKind::Struct; a.name = "A"; a.parentId = 0;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
rcx::Node b; b.kind = rcx::NodeKind::Struct; b.name = "B"; b.parentId = aId;
int bi = tree.addNode(b);
uint64_t bId = tree.nodes[bi].id;
// Manually create a cycle: A's parent → B
tree.nodes[ai].parentId = bId;
tree.invalidateIdCache();
// Should not hang — cycle detection terminates
int d = tree.depthOf(ai);
QVERIFY(d < 100);
}
void testComputeOffsetCycle() {
rcx::NodeTree tree;
rcx::Node a; a.kind = rcx::NodeKind::Struct; a.name = "A"; a.parentId = 0; a.offset = 10;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
rcx::Node b; b.kind = rcx::NodeKind::Struct; b.name = "B"; b.parentId = aId; b.offset = 20;
int bi = tree.addNode(b);
uint64_t bId = tree.nodes[bi].id;
// Create cycle: A → B → A
tree.nodes[ai].parentId = bId;
tree.invalidateIdCache();
// Should not hang
int off = tree.computeOffset(ai);
Q_UNUSED(off);
QVERIFY(true); // reaching here means no hang
}
void testProviderWrite() {
QByteArray data(16, '\0');
rcx::FileProvider prov(data);
QVERIFY(prov.isWritable());
QByteArray patch;
patch.append((char)0x42);
patch.append((char)0x43);
QVERIFY(prov.writeBytes(0, patch));
QCOMPARE(prov.readU8(0), (uint8_t)0x42);
QCOMPARE(prov.readU8(1), (uint8_t)0x43);
// Write past end should fail
QVERIFY(!prov.writeBytes(15, patch));
// NullProvider is not writable
rcx::NullProvider np;
QVERIFY(!np.isWritable());
}
void testComputeOffsetLarge() {
// Verify computeOffset returns int64_t that doesn't overflow
rcx::NodeTree tree;
rcx::Node root; root.kind = rcx::NodeKind::Struct; root.name = "R";
root.parentId = 0; root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node child; child.kind = rcx::NodeKind::Hex8; child.name = "f";
child.parentId = rootId; child.offset = 0x7FFFFFFF; // max int32
tree.addNode(child);
int64_t off = tree.computeOffset(1);
QCOMPARE(off, (int64_t)0x7FFFFFFF);
}
void testKindMetaCompleteness() {
// Every NodeKind enum value must have a KindMeta entry
for (int i = 0; i <= static_cast<int>(rcx::NodeKind::Array); i++) {
auto kind = static_cast<rcx::NodeKind>(i);
const rcx::KindMeta* m = rcx::kindMeta(kind);
QVERIFY2(m != nullptr,
qPrintable(QString("Missing KindMeta for kind %1").arg(i)));
QCOMPARE(m->kind, kind);
QVERIFY(m->name != nullptr);
QVERIFY(m->typeName != nullptr);
QVERIFY(m->lines >= 1);
QVERIFY(m->align >= 1);
}
// sizeForKind/linesForKind/alignmentFor must agree with table
for (const auto& m : rcx::kKindMeta) {
QCOMPARE(rcx::sizeForKind(m.kind), m.size);
QCOMPARE(rcx::linesForKind(m.kind), m.lines);
QCOMPARE(rcx::alignmentFor(m.kind), m.align);
}
}
void testColumnSpan_field() {
rcx::LineMeta lm;
lm.lineKind = rcx::LineKind::Field;
lm.depth = 1;
lm.isContinuation = false;
lm.nodeIdx = 0;
// kFoldCol (3) + depth*3 = 6
auto ts = rcx::typeSpanFor(lm);
QVERIFY(ts.valid);
QCOMPARE(ts.start, 6);
QCOMPARE(ts.end, 16); // 6 + 10
auto ns = rcx::nameSpanFor(lm);
QVERIFY(ns.valid);
QCOMPARE(ns.start, 18); // 6 + 10 + 2
QCOMPARE(ns.end, 42); // 18 + 24
auto vs = rcx::valueSpanFor(lm, 60);
QVERIFY(vs.valid);
QCOMPARE(vs.start, 44); // 18 + 24 + 2
QCOMPARE(vs.end, 60);
}
void testColumnSpan_continuation() {
rcx::LineMeta lm;
lm.lineKind = rcx::LineKind::Continuation;
lm.depth = 1;
lm.isContinuation = true;
lm.nodeIdx = 0;
QVERIFY(!rcx::typeSpanFor(lm).valid);
QVERIFY(!rcx::nameSpanFor(lm).valid);
auto vs = rcx::valueSpanFor(lm, 60);
QVERIFY(vs.valid);
QCOMPARE(vs.start, 6 + 10 + 24 + 4); // kFoldCol+indent + COL_TYPE + COL_NAME + 4
QCOMPARE(vs.end, 60);
}
void testColumnSpan_headerFooter() {
rcx::LineMeta lm;
lm.lineKind = rcx::LineKind::Header;
lm.depth = 0;
lm.nodeIdx = 0;
QVERIFY(!rcx::typeSpanFor(lm).valid);
QVERIFY(!rcx::nameSpanFor(lm).valid);
QVERIFY(!rcx::valueSpanFor(lm, 40).valid);
lm.lineKind = rcx::LineKind::Footer;
QVERIFY(!rcx::typeSpanFor(lm).valid);
QVERIFY(!rcx::nameSpanFor(lm).valid);
QVERIFY(!rcx::valueSpanFor(lm, 40).valid);
}
void testColumnSpan_depth0() {
rcx::LineMeta lm;
lm.lineKind = rcx::LineKind::Field;
lm.depth = 0;
lm.isContinuation = false;
lm.nodeIdx = 0;
// kFoldCol (3) + depth*3(0) = 3
auto ts = rcx::typeSpanFor(lm);
QVERIFY(ts.valid);
QCOMPARE(ts.start, 3);
QCOMPARE(ts.end, 13); // 3 + 10
auto ns = rcx::nameSpanFor(lm);
QVERIFY(ns.valid);
QCOMPARE(ns.start, 15); // 3 + 10 + 2
QCOMPARE(ns.end, 39); // 15 + 24
auto vs = rcx::valueSpanFor(lm, 50);
QVERIFY(vs.valid);
QCOMPARE(vs.start, 41); // 15 + 24 + 2
QCOMPARE(vs.end, 50);
}
void testNodeIdJsonRoundTrip() {
rcx::NodeTree tree;
rcx::Node n; n.kind = rcx::NodeKind::Float; n.name = "x"; n.parentId = 0;
tree.addNode(n);
tree.addNode(n);
QJsonObject json = tree.toJson();
rcx::NodeTree t2 = rcx::NodeTree::fromJson(json);
QCOMPARE(t2.nodes[0].id, tree.nodes[0].id);
QCOMPARE(t2.nodes[1].id, tree.nodes[1].id);
QVERIFY(t2.m_nextId >= 3);
}
void testStructSpan() {
using namespace rcx;
NodeTree tree;
tree.baseAddress = 0;
// Struct with UInt32 (offset 0, 4 bytes) + UInt64 (offset 4, 8 bytes)
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f1;
f1.kind = NodeKind::UInt32;
f1.name = "a";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
Node f2;
f2.kind = NodeKind::UInt64;
f2.name = "b";
f2.parentId = rootId;
f2.offset = 4;
tree.addNode(f2);
// Span = max(0+4, 4+8) = 12
QCOMPARE(tree.structSpan(rootId), 12);
// Nested struct: inner at offset 0 with a UInt64 at offset 0 (size 8)
NodeTree tree2;
Node outer;
outer.kind = NodeKind::Struct;
outer.name = "Outer";
outer.parentId = 0;
int oi = tree2.addNode(outer);
uint64_t outerId = tree2.nodes[oi].id;
Node inner;
inner.kind = NodeKind::Struct;
inner.name = "Inner";
inner.parentId = outerId;
inner.offset = 0;
int ii = tree2.addNode(inner);
uint64_t innerId = tree2.nodes[ii].id;
Node leaf;
leaf.kind = NodeKind::UInt64;
leaf.name = "x";
leaf.parentId = innerId;
leaf.offset = 0;
tree2.addNode(leaf);
// Inner span = 8, outer span = max(0+8) = 8
QCOMPARE(tree2.structSpan(innerId), 8);
QCOMPARE(tree2.structSpan(outerId), 8);
// Empty struct = 0
NodeTree tree3;
Node empty;
empty.kind = NodeKind::Struct;
empty.name = "Empty";
empty.parentId = 0;
int ei = tree3.addNode(empty);
QCOMPARE(tree3.structSpan(tree3.nodes[ei].id), 0);
}
void testNormalizePreferAncestors() {
using namespace rcx;
NodeTree tree;
// Root -> A -> leaf
Node root; root.kind = NodeKind::Struct; root.name = "R"; root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node a; a.kind = NodeKind::Struct; a.name = "A"; a.parentId = rootId;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
Node leaf; leaf.kind = NodeKind::Hex8; leaf.name = "x"; leaf.parentId = aId;
int li = tree.addNode(leaf);
uint64_t leafId = tree.nodes[li].id;
// Select root + leaf: leaf should be pruned (root is ancestor)
QSet<uint64_t> sel = {rootId, leafId};
QSet<uint64_t> norm = tree.normalizePreferAncestors(sel);
QCOMPARE(norm.size(), 1);
QVERIFY(norm.contains(rootId));
// Select A + leaf: leaf pruned (A is ancestor)
sel = {aId, leafId};
norm = tree.normalizePreferAncestors(sel);
QCOMPARE(norm.size(), 1);
QVERIFY(norm.contains(aId));
// Select root + A: A pruned (root is ancestor)
sel = {rootId, aId};
norm = tree.normalizePreferAncestors(sel);
QCOMPARE(norm.size(), 1);
QVERIFY(norm.contains(rootId));
// Select only leaf: nothing pruned
sel = {leafId};
norm = tree.normalizePreferAncestors(sel);
QCOMPARE(norm.size(), 1);
QVERIFY(norm.contains(leafId));
}
void testNormalizePreferDescendants() {
using namespace rcx;
NodeTree tree;
Node root; root.kind = NodeKind::Struct; root.name = "R"; root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node a; a.kind = NodeKind::UInt32; a.name = "a"; a.parentId = rootId;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
Node b; b.kind = NodeKind::UInt32; b.name = "b"; b.parentId = rootId; b.offset = 4;
int bi = tree.addNode(b);
uint64_t bId = tree.nodes[bi].id;
// Select root + a + b: root dropped (has selected descendants)
QSet<uint64_t> sel = {rootId, aId, bId};
QSet<uint64_t> norm = tree.normalizePreferDescendants(sel);
QCOMPARE(norm.size(), 2);
QVERIFY(norm.contains(aId));
QVERIFY(norm.contains(bId));
QVERIFY(!norm.contains(rootId));
// Select root + a: root dropped, a kept
sel = {rootId, aId};
norm = tree.normalizePreferDescendants(sel);
QCOMPARE(norm.size(), 1);
QVERIFY(norm.contains(aId));
// Select only root: nothing dropped (no descendants selected)
sel = {rootId};
norm = tree.normalizePreferDescendants(sel);
QCOMPARE(norm.size(), 1);
QVERIFY(norm.contains(rootId));
}
};
QTEST_MAIN(TestCore)
#include "test_core.moc"

442
tests/test_editor.cpp Normal file
View File

@@ -0,0 +1,442 @@
#include <QtTest/QTest>
#include <QtTest/QSignalSpy>
#include <QApplication>
#include <QKeyEvent>
#include <QFocusEvent>
#include <Qsci/qsciscintilla.h>
#include "editor.h"
#include "core.h"
using namespace rcx;
// Minimal provider for testing
static FileProvider makeTestProvider() {
QByteArray data(256, '\0');
// Write known values: uint16_t=23117 at offset 0, Hex64 at offset 8
uint16_t u16 = 23117;
memcpy(data.data(), &u16, 2);
uint64_t h64 = 0x4D5A900000000000ULL;
memcpy(data.data() + 8, &h64, 8);
return FileProvider(data);
}
// Build a simple tree with a struct containing a few fields
static NodeTree makeTestTree() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "TestStruct";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f1;
f1.kind = NodeKind::UInt16;
f1.name = "field_u16";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
Node f2;
f2.kind = NodeKind::Hex64;
f2.name = "field_hex";
f2.parentId = rootId;
f2.offset = 8;
tree.addNode(f2);
return tree;
}
class TestEditor : public QObject {
Q_OBJECT
private:
RcxEditor* m_editor = nullptr;
ComposeResult m_result;
private slots:
void initTestCase() {
m_editor = new RcxEditor();
m_editor->resize(800, 600);
m_editor->show();
QVERIFY(QTest::qWaitForWindowExposed(m_editor));
NodeTree tree = makeTestTree();
FileProvider prov = makeTestProvider();
m_result = compose(tree, prov);
m_editor->applyDocument(m_result);
}
void cleanupTestCase() {
delete m_editor;
}
// ── Test: inline edit lifecycle (begin → commit → re-edit) ──
void testInlineEditReEntry() {
// Move cursor to line 1 (first field inside struct)
m_editor->scintilla()->setCursorPosition(1, 0);
// Should not be editing
QVERIFY(!m_editor->isEditing());
// Begin edit on Name column
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
// Cancel the edit
m_editor->cancelInlineEdit();
QVERIFY(!m_editor->isEditing());
// Re-apply document (simulates controller refresh)
m_editor->applyDocument(m_result);
// Should be able to edit again
ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
// Cancel again
m_editor->cancelInlineEdit();
QVERIFY(!m_editor->isEditing());
}
// ── Test: commit inline edit then re-edit same line ──
void testCommitThenReEdit() {
m_editor->applyDocument(m_result);
m_editor->scintilla()->setCursorPosition(1, 0);
// Begin value edit
bool ok = m_editor->beginInlineEdit(EditTarget::Value, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
// Simulate Enter key → commit (via signal spy)
QSignalSpy spy(m_editor, &RcxEditor::inlineEditCommitted);
QKeyEvent enter(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
QApplication::sendEvent(m_editor->scintilla(), &enter);
// Should have emitted commit signal and exited edit mode
QCOMPARE(spy.count(), 1);
QVERIFY(!m_editor->isEditing());
// Re-apply document (simulates refresh)
m_editor->applyDocument(m_result);
// Must be able to edit the same line again
ok = m_editor->beginInlineEdit(EditTarget::Value, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit();
}
// ── Test: mouse click during edit commits it ──
void testMouseClickCommitsEdit() {
m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
// Simulate mouse click on viewport — should commit (save), not cancel
QSignalSpy commitSpy(m_editor, &RcxEditor::inlineEditCommitted);
QSignalSpy cancelSpy(m_editor, &RcxEditor::inlineEditCancelled);
QMouseEvent click(QEvent::MouseButtonPress, QPointF(10, 10),
QPointF(10, 10), Qt::LeftButton,
Qt::LeftButton, Qt::NoModifier);
QApplication::sendEvent(m_editor->scintilla()->viewport(), &click);
QVERIFY(!m_editor->isEditing());
QCOMPARE(commitSpy.count(), 1);
QCOMPARE(cancelSpy.count(), 0);
}
// ── Test: FocusOut during edit commits it ──
void testFocusOutCommitsEdit() {
m_editor->applyDocument(m_result);
// Give focus to the scintilla widget first
m_editor->scintilla()->setFocus();
QApplication::processEvents();
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
QSignalSpy commitSpy(m_editor, &RcxEditor::inlineEditCommitted);
QSignalSpy cancelSpy(m_editor, &RcxEditor::inlineEditCancelled);
// Create a dummy widget and transfer focus to it (triggers real FocusOut)
QWidget dummy;
dummy.show();
QVERIFY(QTest::qWaitForWindowExposed(&dummy));
dummy.setFocus();
QApplication::processEvents(); // process focus change + deferred timer
QVERIFY(!m_editor->isEditing());
QCOMPARE(commitSpy.count(), 1);
QCOMPARE(cancelSpy.count(), 0);
// Restore focus to editor for subsequent tests
m_editor->scintilla()->setFocus();
QApplication::processEvents();
}
// ── Test: type edit begins and can be cancelled ──
void testTypeEditCancel() {
m_editor->applyDocument(m_result);
// Begin type edit on a field line
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
// Process deferred events (showTypeAutocomplete is deferred via QTimer)
QApplication::processEvents();
// First Escape closes autocomplete popup (if active) or cancels edit
QKeyEvent esc1(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier);
QApplication::sendEvent(m_editor->scintilla(), &esc1);
// If autocomplete was open, first Esc only closed popup; need second Esc
if (m_editor->isEditing()) {
QKeyEvent esc2(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier);
QApplication::sendEvent(m_editor->scintilla(), &esc2);
}
QVERIFY(!m_editor->isEditing());
}
// ── Test: edit on header line (Name is valid, Type/Value invalid) ──
void testHeaderLineEdit() {
m_editor->applyDocument(m_result);
// Line 0 should be the struct header
const LineMeta* lm = m_editor->metaForLine(0);
QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Header);
// Type edit on header should fail (no type span)
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 0);
QVERIFY(!ok);
QVERIFY(!m_editor->isEditing());
// Name edit on header should succeed (dynamic span)
ok = m_editor->beginInlineEdit(EditTarget::Name, 0);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit();
}
// ── Test: footer line rejects all edits ──
void testFooterLineEdit() {
m_editor->applyDocument(m_result);
// Find the footer line
int footerLine = -1;
for (int i = 0; i < m_result.meta.size(); i++) {
if (m_result.meta[i].lineKind == LineKind::Footer) {
footerLine = i;
break;
}
}
QVERIFY(footerLine >= 0);
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, footerLine));
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, footerLine));
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, footerLine));
QVERIFY(!m_editor->isEditing());
}
// ── Test: showTypeAutocomplete populates list (check via SCI_AUTOCACTIVE) ──
void testTypeAutocompleteShows() {
m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
QVERIFY(ok);
// Process deferred timer (autocomplete is deferred)
QApplication::processEvents();
// Check if the user list is active
long active = m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_AUTOCACTIVE);
QVERIFY2(active != 0, "Autocomplete list should be active after type edit begins");
// Cancel
m_editor->cancelInlineEdit();
m_editor->applyDocument(m_result);
}
// ── Test: parseValue accepts space-separated hex bytes ──
void testParseValueHexWithSpaces() {
bool ok;
// Hex8 with spaces (single byte, but test the .remove(' '))
QByteArray b = fmt::parseValue(NodeKind::Hex8, "4D", &ok);
QVERIFY(ok);
QCOMPARE((uint8_t)b[0], (uint8_t)0x4D);
// Hex32 with space-separated bytes
b = fmt::parseValue(NodeKind::Hex32, "DE AD BE EF", &ok);
QVERIFY(ok);
QCOMPARE(b.size(), 4);
uint32_t v32;
memcpy(&v32, b.data(), 4);
QCOMPARE(v32, (uint32_t)0xDEADBEEF);
// Hex64 with space-separated bytes
b = fmt::parseValue(NodeKind::Hex64, "4D 5A 90 00 00 00 00 00", &ok);
QVERIFY(ok);
QCOMPARE(b.size(), 8);
uint64_t v64;
memcpy(&v64, b.data(), 8);
QCOMPARE(v64, (uint64_t)0x4D5A900000000000ULL);
// Hex64 continuous (should still work)
b = fmt::parseValue(NodeKind::Hex64, "4D5A900000000000", &ok);
QVERIFY(ok);
memcpy(&v64, b.data(), 8);
QCOMPARE(v64, (uint64_t)0x4D5A900000000000ULL);
// Hex64 with 0x prefix and spaces
b = fmt::parseValue(NodeKind::Hex64, "0x4D 5A 90 00 00 00 00 00", &ok);
QVERIFY(ok);
}
// ── Test: type autocomplete accepts typed input and commits ──
void testTypeAutocompleteTypingAndCommit() {
m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
QVERIFY(ok);
// Process deferred autocomplete
QApplication::processEvents();
// Verify autocomplete is active
long active = m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_AUTOCACTIVE);
QVERIFY2(active != 0, "Autocomplete should be active");
// Simulate typing 'i' — filters to typeName entries starting with 'i'
QKeyEvent keyI(QEvent::KeyPress, Qt::Key_I, Qt::NoModifier, "i");
QApplication::sendEvent(m_editor->scintilla(), &keyI);
// Still editing
QVERIFY(m_editor->isEditing());
// Simulate Enter to select from autocomplete (handled synchronously)
QSignalSpy spy(m_editor, &RcxEditor::inlineEditCommitted);
QKeyEvent enter(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
QApplication::sendEvent(m_editor->scintilla(), &enter);
// Should have committed immediately (no deferred timer for type edits)
QCOMPARE(spy.count(), 1);
QVERIFY(!m_editor->isEditing());
// The committed text should be a valid typeName starting with 'i'
QList<QVariant> args = spy.first();
QString committedText = args.at(3).toString();
QVERIFY2(committedText.startsWith('i'),
qPrintable("Expected typeName starting with 'i', got: " + committedText));
m_editor->applyDocument(m_result);
}
// ── Test: type edit click-away commits original (no change) ──
void testTypeEditClickAwayNoChange() {
m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1);
QVERIFY(ok);
// Process deferred autocomplete
QApplication::processEvents();
// Click away on viewport — should commit (not cancel)
QSignalSpy commitSpy(m_editor, &RcxEditor::inlineEditCommitted);
QMouseEvent click(QEvent::MouseButtonPress, QPointF(10, 10),
QPointF(10, 10), Qt::LeftButton,
Qt::LeftButton, Qt::NoModifier);
QApplication::sendEvent(m_editor->scintilla()->viewport(), &click);
QVERIFY(!m_editor->isEditing());
QCOMPARE(commitSpy.count(), 1);
// The committed text should be the original typeName (no change)
QList<QVariant> args = commitSpy.first();
QString committedText = args.at(3).toString();
QVERIFY2(committedText == "uint16_t",
qPrintable("Expected 'uint16_t', got: " + committedText));
m_editor->applyDocument(m_result);
}
// ── Test: column span hit-testing for cursor shape ──
void testColumnSpanHitTest() {
m_editor->applyDocument(m_result);
// Line 1 is a field line (UInt16), verify spans are valid
const LineMeta* lm = m_editor->metaForLine(1);
QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Field);
// Type span should be valid for field lines
ColumnSpan ts = RcxEditor::typeSpan(*lm);
QVERIFY(ts.valid);
QVERIFY(ts.start < ts.end);
// Name span should be valid for field lines
ColumnSpan ns = RcxEditor::nameSpan(*lm);
QVERIFY(ns.valid);
QVERIFY(ns.start < ns.end);
// Value span should be valid for field lines
QString lineText;
int len = (int)m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1);
QVERIFY(len > 0);
ColumnSpan vs = RcxEditor::valueSpan(*lm, len);
QVERIFY(vs.valid);
QVERIFY(vs.start < vs.end);
// Footer line should have no valid type/name spans
int footerLine = -1;
for (int i = 0; i < m_result.meta.size(); i++) {
if (m_result.meta[i].lineKind == LineKind::Footer) {
footerLine = i;
break;
}
}
QVERIFY(footerLine >= 0);
const LineMeta* flm = m_editor->metaForLine(footerLine);
QVERIFY(flm);
ColumnSpan fts = RcxEditor::typeSpan(*flm);
QVERIFY(!fts.valid);
ColumnSpan fns = RcxEditor::nameSpan(*flm);
QVERIFY(!fns.valid);
ColumnSpan fvs = RcxEditor::valueSpan(*flm, 10);
QVERIFY(!fvs.valid);
}
// ── Test: selectedNodeIndices ──
void testSelectedNodeIndices() {
m_editor->applyDocument(m_result);
// Put cursor on first field line
m_editor->scintilla()->setCursorPosition(1, 0);
QSet<int> sel = m_editor->selectedNodeIndices();
QCOMPARE(sel.size(), 1);
// The node index should match the first field
const LineMeta* lm = m_editor->metaForLine(1);
QVERIFY(lm);
QVERIFY(sel.contains(lm->nodeIdx));
}
};
QTEST_MAIN(TestEditor)
#include "test_editor.moc"

248
tests/test_format.cpp Normal file
View File

@@ -0,0 +1,248 @@
#include <QtTest/QTest>
#include "core.h"
using namespace rcx;
class TestFormat : public QObject {
Q_OBJECT
private slots:
void testTypeName() {
QString s = fmt::typeName(NodeKind::Float);
QVERIFY(s.trimmed() == "float");
QCOMPARE(s.size(), 10); // COL_TYPE
}
void testFmtInt32() {
QCOMPARE(fmt::fmtInt32(-42), QString("-42"));
QCOMPARE(fmt::fmtInt32(0), QString("0"));
}
void testFmtFloat() {
QString s = fmt::fmtFloat(3.14159f);
QVERIFY(s.contains("3.14"));
}
void testFmtBool() {
QCOMPARE(fmt::fmtBool(1), QString("true"));
QCOMPARE(fmt::fmtBool(0), QString("false"));
}
void testFmtPointer64_null() {
QCOMPARE(fmt::fmtPointer64(0), QString("-> NULL"));
}
void testFmtPointer64_nonNull() {
QString s = fmt::fmtPointer64(0x400000);
QVERIFY(s.startsWith("-> 0x"));
QVERIFY(s.contains("400000"));
}
void testFmtOffsetMargin_primary() {
QCOMPARE(fmt::fmtOffsetMargin(0x10, false), QString("+0x10"));
QCOMPARE(fmt::fmtOffsetMargin(0, false), QString("+0x0"));
}
void testFmtOffsetMargin_continuation() {
QCOMPARE(fmt::fmtOffsetMargin(0x10, true), QString(" \u00B7"));
}
void testFmtStructHeader() {
Node n;
n.kind = NodeKind::Struct;
n.name = "Test";
QString s = fmt::fmtStructHeader(n, 0);
QVERIFY(s.contains("struct"));
QVERIFY(s.contains("Test"));
QVERIFY(s.contains("{"));
}
void testFmtStructFooter() {
Node n;
n.kind = NodeKind::Struct;
n.name = "Test";
QString s = fmt::fmtStructFooter(n, 0);
QVERIFY(s.contains("};"));
QVERIFY(s.contains("Test"));
}
void testIndent() {
QCOMPARE(fmt::indent(0), QString(""));
QCOMPARE(fmt::indent(1), QString(" "));
QCOMPARE(fmt::indent(3), QString(" "));
}
void testParseValueInt32() {
bool ok;
QByteArray b = fmt::parseValue(NodeKind::Int32, "-42", &ok);
QVERIFY(ok);
QCOMPARE(b.size(), 4);
int32_t v;
memcpy(&v, b.data(), 4);
QCOMPARE(v, -42);
}
void testParseValueFloat() {
bool ok;
QByteArray b = fmt::parseValue(NodeKind::Float, "3.14", &ok);
QVERIFY(ok);
QCOMPARE(b.size(), 4);
float v;
memcpy(&v, b.data(), 4);
QVERIFY(qAbs(v - 3.14f) < 0.01f);
}
void testParseValueHex32() {
bool ok;
QByteArray b = fmt::parseValue(NodeKind::Hex32, "DEADBEEF", &ok);
QVERIFY(ok);
QCOMPARE(b.size(), 4);
uint32_t v;
memcpy(&v, b.data(), 4);
QCOMPARE(v, (uint32_t)0xDEADBEEF);
}
void testParseValueBool() {
bool ok;
QByteArray b = fmt::parseValue(NodeKind::Bool, "true", &ok);
QVERIFY(ok);
QCOMPARE(b.size(), 1);
QCOMPARE((uint8_t)b[0], (uint8_t)1);
b = fmt::parseValue(NodeKind::Bool, "false", &ok);
QVERIFY(ok);
QCOMPARE((uint8_t)b[0], (uint8_t)0);
// Unknown token should fail
fmt::parseValue(NodeKind::Bool, "banana", &ok);
QVERIFY(!ok);
}
void testParseValueHex0xPrefix() {
bool ok;
// Hex32 with 0x prefix should work
QByteArray b = fmt::parseValue(NodeKind::Hex32, "0xDEADBEEF", &ok);
QVERIFY(ok);
uint32_t v;
memcpy(&v, b.data(), 4);
QCOMPARE(v, (uint32_t)0xDEADBEEF);
// Pointer64 with 0x prefix
b = fmt::parseValue(NodeKind::Pointer64, "0x0000000000400000", &ok);
QVERIFY(ok);
uint64_t v64;
memcpy(&v64, b.data(), 8);
QCOMPARE(v64, (uint64_t)0x400000);
}
void testParseValueOverflow() {
bool ok;
// UInt8: 300 exceeds uint8_t max (255) → should fail
fmt::parseValue(NodeKind::UInt8, "300", &ok);
QVERIFY(!ok);
// UInt8: 255 should succeed
QByteArray b = fmt::parseValue(NodeKind::UInt8, "255", &ok);
QVERIFY(ok);
QCOMPARE((uint8_t)b[0], (uint8_t)255);
// Int8: 200 exceeds int8_t max (127) → should fail
fmt::parseValue(NodeKind::Int8, "200", &ok);
QVERIFY(!ok);
// Int8: -129 below min → should fail
fmt::parseValue(NodeKind::Int8, "-129", &ok);
QVERIFY(!ok);
// Int8: -128 is valid
b = fmt::parseValue(NodeKind::Int8, "-128", &ok);
QVERIFY(ok);
int8_t sv;
memcpy(&sv, b.data(), 1);
QCOMPARE(sv, (int8_t)-128);
// UInt16: 70000 exceeds uint16_t max → should fail
fmt::parseValue(NodeKind::UInt16, "70000", &ok);
QVERIFY(!ok);
// Hex8: 0x1FF exceeds uint8_t → should fail
fmt::parseValue(NodeKind::Hex8, "1FF", &ok);
QVERIFY(!ok);
// Hex16: 0x1FFFF exceeds uint16_t → should fail
fmt::parseValue(NodeKind::Hex16, "1FFFF", &ok);
QVERIFY(!ok);
}
void testReadValueBoundsCheck() {
// Vec2 subLine=2 (out of bounds) should return "?"
QByteArray data(16, '\0');
FileProvider prov(data);
Node n;
n.kind = NodeKind::Vec2;
n.name = "v";
QCOMPARE(fmt::readValue(n, prov, 0, 2), QString("?"));
QCOMPARE(fmt::readValue(n, prov, 0, -1), QString("?"));
// Vec3 subLine=3 (out of bounds)
n.kind = NodeKind::Vec3;
QCOMPARE(fmt::readValue(n, prov, 0, 3), QString("?"));
// Vec3 subLine=2 (valid)
QVERIFY(fmt::readValue(n, prov, 0, 2) != QString("?"));
}
void testEditableValueBasic() {
QByteArray data(16, '\0');
// Write a known float value
float val = 3.14f;
memcpy(data.data(), &val, 4);
FileProvider prov(data);
Node n;
n.kind = NodeKind::Float;
n.name = "f";
QString s = fmt::editableValue(n, prov, 0, 0);
QVERIFY(s.contains("3.14"));
// Vec2 out of bounds → "?"
n.kind = NodeKind::Vec2;
QCOMPARE(fmt::editableValue(n, prov, 0, 2), QString("?"));
}
void testParseValueEmptyString() {
bool ok;
// Empty UTF8 should succeed (caller pads)
QByteArray b = fmt::parseValue(NodeKind::UTF8, "", &ok);
QVERIFY(ok);
QVERIFY(b.isEmpty());
// Empty non-string should fail
fmt::parseValue(NodeKind::Int32, "", &ok);
QVERIFY(!ok);
}
void testFmtStructFooterWithSize() {
Node n;
n.kind = NodeKind::Struct;
n.name = "Test";
// With size
QString s1 = fmt::fmtStructFooter(n, 0, 0x14);
QVERIFY(s1.contains("};"));
QVERIFY(s1.contains("Test"));
QVERIFY(s1.contains("sizeof=0x14"));
// Size 0 → no sizeof
QString s2 = fmt::fmtStructFooter(n, 0, 0);
QVERIFY(s2.contains("};"));
QVERIFY(!s2.contains("sizeof"));
// Default (no size arg) → no sizeof
QString s3 = fmt::fmtStructFooter(n, 0);
QVERIFY(s3.contains("};"));
QVERIFY(!s3.contains("sizeof"));
}
};
QTEST_MAIN(TestFormat)
#include "test_format.moc"