mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Initial commit: ReclassX structured binary editor
This commit is contained in:
674
tests/test_compose.cpp
Normal file
674
tests/test_compose.cpp
Normal 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
557
tests/test_core.cpp
Normal 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
442
tests/test_editor.cpp
Normal 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
248
tests/test_format.cpp
Normal 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"
|
||||
Reference in New Issue
Block a user