#include #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); // CommandRow + 2 fields + root footer = 4 QCOMPARE(result.meta.size(), 4); // Line 0 is CommandRow QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow); // Fields at depth 1 QVERIFY(!result.meta[1].foldHead); QCOMPARE(result.meta[1].depth, 1); QVERIFY(!result.meta[2].foldHead); QCOMPARE(result.meta[2].depth, 1); // Offset text QCOMPARE(result.meta[1].offsetText, QString("0000 ")); QCOMPARE(result.meta[2].offsetText, QString("0004 ")); // Line 3 is root footer QCOMPARE(result.meta[3].lineKind, LineKind::Footer); } void testVec3SingleLine() { 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); // CommandRow + 1 Vec3 line + root footer = 3 QCOMPARE(result.meta.size(), 3); // Line 1: single Vec3 line, not continuation, depth 1 QVERIFY(!result.meta[1].isContinuation); QCOMPARE(result.meta[1].offsetText, QString("0000 ")); QCOMPARE(result.meta[1].depth, 1); QCOMPARE(result.meta[1].nodeKind, NodeKind::Vec3); // Line 2 is root footer QCOMPARE(result.meta[2].lineKind, LineKind::Footer); } 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); // CommandRow + padding + root footer = 3 QCOMPARE(result.meta.size(), 3); QVERIFY(result.meta[1].markerMask & (1u << M_PAD)); QCOMPARE(result.meta[1].depth, 1); // Line 2 is root footer QCOMPARE(result.meta[2].lineKind, LineKind::Footer); } 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'); BufferProvider prov(data); ComposeResult result = compose(tree, prov); // CommandRow + ptr + root footer = 3 QCOMPARE(result.meta.size(), 3); // No ambient validation markers — M_PTR0 is no longer set QVERIFY(!(result.meta[1].markerMask & (1u << M_PTR0))); QCOMPARE(result.meta[1].depth, 1); // Line 2 is root footer QCOMPARE(result.meta[2].lineKind, LineKind::Footer); } 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 root: isRootHeader overrides collapse, so children + footer still render // CommandRow + field + root footer = 3 QCOMPARE(result.meta.size(), 3); QCOMPARE(result.meta[1].lineKind, LineKind::Field); QCOMPARE(result.meta[1].depth, 1); QCOMPARE(result.meta[2].lineKind, LineKind::Footer); } void testUnreadablePointerNoRead() { // No ambient validation — neither M_ERR nor M_PTR0 set 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'); BufferProvider prov(data); ComposeResult result = compose(tree, prov); // CommandRow + ptr + root footer = 3 QCOMPARE(result.meta.size(), 3); // No ambient validation markers QVERIFY(!(result.meta[1].markerMask & (1u << M_ERR))); QVERIFY(!(result.meta[1].markerMask & (1u << M_PTR0))); QCOMPARE(result.meta[1].depth, 1); // Line 2 is root footer QCOMPARE(result.meta[2].lineKind, LineKind::Footer); } 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); // Child header (depth 1, fold head) — root header no longer emitted QCOMPARE(result.meta[1].foldLevel, 0x401 | 0x2000); QCOMPARE(result.meta[1].depth, 1); QVERIFY(result.meta[1].foldHead); // Leaf (depth 2, not head) 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); // CommandRow + flags + Inner header + x + y + Inner footer + root footer = 7 QCOMPARE(result.meta.size(), 7); // flags field (depth 1) QCOMPARE(result.meta[1].lineKind, LineKind::Field); QCOMPARE(result.meta[1].depth, 1); // Inner header (depth 1, fold head) 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); // Root 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); BufferProvider prov(data); ComposeResult result = compose(tree, prov); // CommandRow + magic + ptr(merged fold header) + fn1 + fn2 + ptr footer + Main footer = 7 // VTable standalone: header + fn1 + fn2 + footer = 4 // Total = 11 QCOMPARE(result.meta.size(), 11); // magic field (depth 1) QCOMPARE(result.meta[1].lineKind, LineKind::Field); QCOMPARE(result.meta[1].depth, 1); // Pointer as merged fold header: "VTable* ptr {" QCOMPARE(result.meta[2].lineKind, LineKind::Header); QCOMPARE(result.meta[2].depth, 1); QVERIFY(result.meta[2].foldHead); QCOMPARE(result.meta[2].nodeKind, NodeKind::Pointer64); // Expanded fields at depth 2 (struct header merged into pointer) QCOMPARE(result.meta[3].depth, 2); QCOMPARE(result.meta[4].depth, 2); // Pointer fold footer QCOMPARE(result.meta[5].lineKind, LineKind::Footer); QCOMPARE(result.meta[5].depth, 1); } 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'); BufferProvider prov(data); ComposeResult result = compose(tree, prov); // CommandRow + ptr(merged fold header) + target field + ptr footer + Main footer = 5 // Target standalone: header + field + footer = 3 // Total = 8 (null ptr still shows template preview) QCOMPARE(result.meta.size(), 8); // Pointer as merged fold header (expanded — shows template at offset 0) QCOMPARE(result.meta[1].lineKind, LineKind::Header); QCOMPARE(result.meta[1].depth, 1); QVERIFY(result.meta[1].foldHead); // Target field shown as template preview QCOMPARE(result.meta[2].lineKind, LineKind::Field); QCOMPARE(result.meta[2].depth, 2); // Pointer fold footer QCOMPARE(result.meta[3].lineKind, LineKind::Footer); } 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); BufferProvider prov(data); ComposeResult result = compose(tree, prov); // CommandRow + ptr(fold head, collapsed) + Main footer = 3 // Target standalone: header + field + footer = 3 // Total = 6 QCOMPARE(result.meta.size(), 6); // Pointer is fold head (depth 1) QVERIFY(result.meta[1].foldHead); QCOMPARE(result.meta[1].depth, 1); } 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 BufferProvider 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 // CommandRow + ptr merged header + data + self merged header // Second expansion blocked by cycle guard: no children under self // Then: self footer + ptr footer + Main footer + standalone Recursive rendering QVERIFY(result.meta[1].foldHead); // ptr merged fold head QCOMPARE(result.meta[1].lineKind, LineKind::Header); // ptr merged header QCOMPARE(result.meta[2].lineKind, LineKind::Field); // data field (first child of Recursive) } void testStructFooterSimple() { // Root footer is suppressed; test nested struct footer instead 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 inner; inner.kind = NodeKind::Struct; inner.name = "Inner"; inner.parentId = rootId; inner.offset = 0; int ii = tree.addNode(inner); uint64_t innerId = tree.nodes[ii].id; Node f1; f1.kind = NodeKind::UInt32; f1.name = "a"; f1.parentId = innerId; f1.offset = 0; tree.addNode(f1); NullProvider prov; ComposeResult result = compose(tree, prov); // Find a footer line (nested struct footer) int footerLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].lineKind == LineKind::Footer) { footerLine = i; break; } } QVERIFY2(footerLine >= 0, "Should have a footer for nested struct"); // Footer text should contain "};" (no sizeof) QStringList lines = result.text.split('\n'); QVERIFY(lines[footerLine].contains("};")); QVERIFY(!lines[footerLine].contains("sizeof")); } 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++) { // Skip CommandRow (synthetic line with sentinel nodeId) if (result.meta[i].lineKind == LineKind::CommandRow) { QCOMPARE(result.meta[i].nodeId, kCommandRowId); QCOMPARE(result.meta[i].nodeIdx, -1); continue; } 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); } } // ═════════════════════════════════════════════════════════════ // Array tests // ═════════════════════════════════════════════════════════════ void testArrayHeaderFormat() { // Array header must show "elemType[count]" text and proper metadata 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 arr; arr.kind = NodeKind::Array; arr.name = "data"; arr.parentId = rootId; arr.offset = 0; arr.elementKind = NodeKind::Int32; arr.arrayLen = 10; tree.addNode(arr); NullProvider prov; ComposeResult result = compose(tree, prov); // Find the array header line int headerLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].isArrayHeader) { headerLine = i; break; } } QVERIFY(headerLine >= 0); // Metadata must be correct const LineMeta& lm = result.meta[headerLine]; QCOMPARE(lm.lineKind, LineKind::Header); QVERIFY(lm.isArrayHeader); QCOMPARE(lm.elementKind, NodeKind::Int32); QCOMPARE(lm.arrayCount, 10); QVERIFY(lm.foldHead); QVERIFY(!lm.foldCollapsed); // Text must contain "int32_t[10]" and the name QStringList lines = result.text.split('\n'); QVERIFY(headerLine < lines.size()); QString text = lines[headerLine]; QVERIFY2(text.contains("int32_t[10]"), qPrintable("Header should contain 'int32_t[10]': " + text)); QVERIFY2(text.contains("data"), qPrintable("Header should contain 'data': " + text)); QVERIFY2(text.contains("{"), qPrintable("Expanded header should contain '{': " + text)); } void testArrayHeaderCharTypes() { // UInt8 array → "char[N]", UInt16 → "wchar_t[N]" 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 arr1; arr1.kind = NodeKind::Array; arr1.name = "str"; arr1.parentId = rootId; arr1.offset = 0; arr1.elementKind = NodeKind::UInt8; arr1.arrayLen = 64; tree.addNode(arr1); Node arr2; arr2.kind = NodeKind::Array; arr2.name = "wstr"; arr2.parentId = rootId; arr2.offset = 64; arr2.elementKind = NodeKind::UInt16; arr2.arrayLen = 32; tree.addNode(arr2); NullProvider prov; ComposeResult result = compose(tree, prov); QStringList lines = result.text.split('\n'); bool foundChar = false, foundWchar = false; for (int i = 0; i < result.meta.size(); i++) { if (!result.meta[i].isArrayHeader) continue; QString text = lines[i]; if (text.contains("char[64]")) foundChar = true; if (text.contains("wchar_t[32]")) foundWchar = true; } QVERIFY2(foundChar, "Should have 'char[64]' header"); QVERIFY2(foundWchar, "Should have 'wchar_t[32]' header"); } void testArraySpansClickable() { // Element type and count spans must cover the correct text regions 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 arr; arr.kind = NodeKind::Array; arr.name = "numbers"; arr.parentId = rootId; arr.offset = 0; arr.elementKind = NodeKind::UInt32; arr.arrayLen = 5; tree.addNode(arr); NullProvider prov; ComposeResult result = compose(tree, prov); int headerLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].isArrayHeader) { headerLine = i; break; } } QVERIFY(headerLine >= 0); QStringList lines = result.text.split('\n'); QString lineText = lines[headerLine]; const LineMeta& lm = result.meta[headerLine]; // Element type span must be valid and cover "uint32_t" ColumnSpan typeSpan = arrayElemTypeSpanFor(lm, lineText); QVERIFY2(typeSpan.valid, "arrayElemTypeSpanFor must return a valid span"); QVERIFY(typeSpan.start < typeSpan.end); QString typeText = lineText.mid(typeSpan.start, typeSpan.end - typeSpan.start); QVERIFY2(typeText.contains("uint32_t"), qPrintable("Type span should cover 'uint32_t', got: '" + typeText + "'")); // Element count span must be valid and cover "5" ColumnSpan countSpan = arrayElemCountSpanFor(lm, lineText); QVERIFY2(countSpan.valid, "arrayElemCountSpanFor must return a valid span"); QVERIFY(countSpan.start < countSpan.end); QString countText = lineText.mid(countSpan.start, countSpan.end - countSpan.start); QCOMPARE(countText, QString("5")); } void testArrayWithStructChildren() { // Array with struct children renders separators and child fields 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; // Array container Node arr; arr.kind = NodeKind::Array; arr.name = "items"; arr.parentId = rootId; arr.offset = 0; arr.elementKind = NodeKind::Int32; arr.arrayLen = 2; int ai = tree.addNode(arr); uint64_t arrId = tree.nodes[ai].id; // Two struct children inside the array (representing elements) Node elem0; elem0.kind = NodeKind::Struct; elem0.name = "Item"; elem0.parentId = arrId; elem0.offset = 0; int e0i = tree.addNode(elem0); uint64_t elem0Id = tree.nodes[e0i].id; Node f0; f0.kind = NodeKind::UInt32; f0.name = "value"; f0.parentId = elem0Id; f0.offset = 0; tree.addNode(f0); Node elem1; elem1.kind = NodeKind::Struct; elem1.name = "Item"; elem1.parentId = arrId; elem1.offset = 4; int e1i = tree.addNode(elem1); uint64_t elem1Id = tree.nodes[e1i].id; Node f1; f1.kind = NodeKind::UInt32; f1.name = "value"; f1.parentId = elem1Id; f1.offset = 0; tree.addNode(f1); NullProvider prov; ComposeResult result = compose(tree, prov); // Must have content between header and footer (not empty!) QVERIFY2(result.meta.size() > 4, qPrintable(QString("Array should have content, got %1 lines") .arg(result.meta.size()))); // Check for [0] and [1] separators bool found0 = false, found1 = false; int fieldCount = 0; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].lineKind == LineKind::ArrayElementSeparator) { if (result.meta[i].arrayElementIdx == 0) found0 = true; if (result.meta[i].arrayElementIdx == 1) found1 = true; } // Count fields belonging to array children if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth >= 2) fieldCount++; } QVERIFY2(found0, "Array should have [0] separator"); QVERIFY2(found1, "Array should have [1] separator"); QVERIFY2(fieldCount >= 2, "Array children should have field lines"); } void testArrayCollapsedNoChildren() { // Collapsed array: header only, no children or footer 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 arr; arr.kind = NodeKind::Array; arr.name = "data"; arr.parentId = rootId; arr.offset = 0; arr.elementKind = NodeKind::Float; arr.arrayLen = 100; arr.collapsed = true; int ai = tree.addNode(arr); uint64_t arrId = tree.nodes[ai].id; // Child that should NOT appear when collapsed Node child; child.kind = NodeKind::Float; child.name = "elem"; child.parentId = arrId; child.offset = 0; tree.addNode(child); NullProvider prov; ComposeResult result = compose(tree, prov); // CommandRow + Array header(collapsed) + root footer = 3 QCOMPARE(result.meta.size(), 3); // Array header is collapsed (at index 1) int arrLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].isArrayHeader) { arrLine = i; break; } } QVERIFY(arrLine >= 0); QCOMPARE(arrLine, 1); QVERIFY(result.meta[arrLine].foldCollapsed); // Header text should NOT contain "{" QStringList lines = result.text.split('\n'); QVERIFY2(!lines[arrLine].contains("{"), qPrintable("Collapsed header should not have '{': " + lines[arrLine])); } void testArrayCountRecompose() { // After changing arrayLen and recomposing, the text shows the new count 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 arr; arr.kind = NodeKind::Array; arr.name = "buf"; arr.parentId = rootId; arr.offset = 0; arr.elementKind = NodeKind::UInt8; arr.arrayLen = 10; int ai = tree.addNode(arr); NullProvider prov; // First compose: should show [10] ComposeResult r1 = compose(tree, prov); QStringList lines1 = r1.text.split('\n'); bool found10 = false; for (const QString& l : lines1) { if (l.contains("[10]")) { found10 = true; break; } } QVERIFY2(found10, "First compose should show [10]"); // Change count and recompose tree.nodes[ai].arrayLen = 42; ComposeResult r2 = compose(tree, prov); QStringList lines2 = r2.text.split('\n'); bool found42 = false; bool still10 = false; for (const QString& l : lines2) { if (l.contains("[42]")) found42 = true; if (l.contains("[10]")) still10 = true; } QVERIFY2(found42, "Recomposed text should show [42]"); QVERIFY2(!still10, "Recomposed text should NOT still show [10]"); // Spans must still work after recompose int headerLine = -1; for (int i = 0; i < r2.meta.size(); i++) { if (r2.meta[i].isArrayHeader) { headerLine = i; break; } } QVERIFY(headerLine >= 0); ColumnSpan countSpan = arrayElemCountSpanFor(r2.meta[headerLine], lines2[headerLine]); QVERIFY2(countSpan.valid, "Count span must be valid after recompose"); QString countText = lines2[headerLine].mid(countSpan.start, countSpan.end - countSpan.start); QCOMPARE(countText, QString("42")); } // ═════════════════════════════════════════════════════════════ // Pointer tests // ═════════════════════════════════════════════════════════════ void testPointerDefaultVoid() { // Pointer64 with no refId should display as "void*" 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 ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "ptr"; ptr.parentId = rootId; ptr.offset = 0; // refId defaults to 0 (void*) tree.addNode(ptr); NullProvider prov; ComposeResult result = compose(tree, prov); // Find the pointer line int ptrLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].nodeKind == NodeKind::Pointer64 && result.meta[i].lineKind == LineKind::Field) { ptrLine = i; break; } } QVERIFY(ptrLine >= 0); QStringList lines = result.text.split('\n'); QString text = lines[ptrLine]; QVERIFY2(text.contains("void*"), qPrintable("Pointer with no refId should show 'void*': " + text)); // pointerTargetName should be empty (void) QVERIFY(result.meta[ptrLine].pointerTargetName.isEmpty()); // Should NOT be a fold head (no deref expansion for void*) QVERIFY(!result.meta[ptrLine].foldHead); } void testPointer32DefaultVoid() { // Same for Pointer32 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 ptr; ptr.kind = NodeKind::Pointer32; ptr.name = "ptr32"; ptr.parentId = rootId; ptr.offset = 0; tree.addNode(ptr); NullProvider prov; ComposeResult result = compose(tree, prov); QStringList lines = result.text.split('\n'); bool foundPtr32 = false; for (const QString& l : lines) { if (l.contains("void*")) { foundPtr32 = true; break; } } QVERIFY2(foundPtr32, "Pointer32 with no refId should show 'void*'"); } void testPointerDisplaysTargetName() { // Pointer64 with refId displays "TargetName*" 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; // Target struct with a structTypeName Node target; target.kind = NodeKind::Struct; target.name = "PlayerData"; target.structTypeName = "PlayerData"; target.parentId = 0; target.offset = 200; int ti = tree.addNode(target); uint64_t targetId = tree.nodes[ti].id; Node tf; tf.kind = NodeKind::UInt32; tf.name = "health"; tf.parentId = targetId; tf.offset = 0; tree.addNode(tf); // Pointer referencing the target (collapsed to prevent expansion) Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "player"; ptr.parentId = rootId; ptr.offset = 0; ptr.refId = targetId; ptr.collapsed = true; tree.addNode(ptr); NullProvider prov; ComposeResult result = compose(tree, prov); // Find the pointer line (root children at depth 0 due to root suppression) int ptrLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].nodeKind == NodeKind::Pointer64 && result.meta[i].lineKind == LineKind::Field) { ptrLine = i; break; } } QVERIFY(ptrLine >= 0); QStringList lines = result.text.split('\n'); QVERIFY2(lines[ptrLine].contains("PlayerData*"), qPrintable("Should show 'PlayerData*': " + lines[ptrLine])); // pointerTargetName metadata QCOMPARE(result.meta[ptrLine].pointerTargetName, QString("PlayerData")); // Pointer with refId is a fold head (even if collapsed) QVERIFY(result.meta[ptrLine].foldHead); QVERIFY(result.meta[ptrLine].foldCollapsed); } void testPointerTargetUsesNameWhenNoTypeName() { // If target struct has no structTypeName, use its name field 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 target; target.kind = NodeKind::Struct; target.name = "MyStruct"; // structTypeName left empty target.parentId = 0; target.offset = 200; int ti = tree.addNode(target); uint64_t targetId = tree.nodes[ti].id; Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "sptr"; ptr.parentId = rootId; ptr.offset = 0; ptr.refId = targetId; ptr.collapsed = true; tree.addNode(ptr); NullProvider prov; ComposeResult result = compose(tree, prov); QStringList lines = result.text.split('\n'); bool found = false; for (const QString& l : lines) { if (l.contains("MyStruct*")) { found = true; break; } } QVERIFY2(found, "Should use struct name when structTypeName is empty"); } void testPointerSpans() { // pointerKindSpanFor and pointerTargetSpanFor must find correct regions 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 target; target.kind = NodeKind::Struct; target.name = "VTable"; target.structTypeName = "VTable"; target.parentId = 0; target.offset = 200; int ti = tree.addNode(target); uint64_t targetId = tree.nodes[ti].id; Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "vtbl"; ptr.parentId = rootId; ptr.offset = 0; ptr.refId = targetId; ptr.collapsed = true; tree.addNode(ptr); NullProvider prov; ComposeResult result = compose(tree, prov); int ptrLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].nodeKind == NodeKind::Pointer64 && result.meta[i].lineKind == LineKind::Field) { ptrLine = i; break; } } QVERIFY(ptrLine >= 0); QStringList lines = result.text.split('\n'); QString lineText = lines[ptrLine]; const LineMeta& lm = result.meta[ptrLine]; // Kind span: no longer applicable in "Type*" format ColumnSpan kindSpan = pointerKindSpanFor(lm, lineText); QVERIFY2(!kindSpan.valid, "pointerKindSpanFor should return invalid in Type* format"); // Target span: covers "VTable" (before the '*') ColumnSpan targetSpan = pointerTargetSpanFor(lm, lineText); QVERIFY2(targetSpan.valid, "pointerTargetSpanFor must return valid span"); QString targetText = lineText.mid(targetSpan.start, targetSpan.end - targetSpan.start).trimmed(); QCOMPARE(targetText, QString("VTable")); } void testPointerVoidSpans() { // void* pointer should have valid target span but no kind span 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 ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "vptr"; ptr.parentId = rootId; ptr.offset = 0; tree.addNode(ptr); NullProvider prov; ComposeResult result = compose(tree, prov); int ptrLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].nodeKind == NodeKind::Pointer64 && result.meta[i].lineKind == LineKind::Field) { ptrLine = i; break; } } QVERIFY(ptrLine >= 0); QStringList lines = result.text.split('\n'); QString lineText = lines[ptrLine]; const LineMeta& lm = result.meta[ptrLine]; // Kind span: no longer applicable in "Type*" format ColumnSpan kindSpan = pointerKindSpanFor(lm, lineText); QVERIFY2(!kindSpan.valid, "Kind span should be invalid in Type* format"); // Target span: "void" (before the '*') ColumnSpan targetSpan = pointerTargetSpanFor(lm, lineText); QVERIFY2(targetSpan.valid, "void* pointer should have valid target span"); QString targetText = lineText.mid(targetSpan.start, targetSpan.end - targetSpan.start).trimmed(); QCOMPARE(targetText, QString("void")); } void testPointerToPointerChain() { // StructB* → StructB { StructC* } → StructC { field } 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; // StructC (innermost target) Node structC; structC.kind = NodeKind::Struct; structC.name = "InnerData"; structC.structTypeName = "InnerData"; structC.parentId = 0; structC.offset = 300; int ci = tree.addNode(structC); uint64_t structCId = tree.nodes[ci].id; Node cf; cf.kind = NodeKind::UInt64; cf.name = "payload"; cf.parentId = structCId; cf.offset = 0; tree.addNode(cf); // StructB (middle target, contains ptr to C) Node structB; structB.kind = NodeKind::Struct; structB.name = "Wrapper"; structB.structTypeName = "Wrapper"; structB.parentId = 0; structB.offset = 200; int bi = tree.addNode(structB); uint64_t structBId = tree.nodes[bi].id; Node bf; bf.kind = NodeKind::UInt32; bf.name = "flags"; bf.parentId = structBId; bf.offset = 0; tree.addNode(bf); Node bptr; bptr.kind = NodeKind::Pointer64; bptr.name = "inner"; bptr.parentId = structBId; bptr.offset = 4; bptr.refId = structCId; // points to InnerData tree.addNode(bptr); // Root's pointer to StructB Node rptr; rptr.kind = NodeKind::Pointer64; rptr.name = "wrapper_ptr"; rptr.parentId = rootId; rptr.offset = 0; rptr.refId = structBId; tree.addNode(rptr); // Provider: rptr at 0 → addr 100, bptr at 100+4=104 → addr 150 QByteArray data(400, '\0'); uint64_t val1 = 100; memcpy(data.data(), &val1, 8); // rptr → 100 uint64_t val2 = 150; memcpy(data.data() + 104, &val2, 8); // bptr at 104 → 150 BufferProvider prov(data); ComposeResult result = compose(tree, prov); // Must finish (no infinite loop) QVERIFY(result.meta.size() > 0); QVERIFY(result.meta.size() < 200); // Check that Wrapper* and InnerData* both appear in text bool foundWrapper = false, foundInner = false; QStringList lines = result.text.split('\n'); for (const QString& l : lines) { if (l.contains("Wrapper*")) foundWrapper = true; if (l.contains("InnerData*")) foundInner = true; } QVERIFY2(foundWrapper, "Should display 'Wrapper*'"); QVERIFY2(foundInner, "Should display 'InnerData*'"); // The chain: Root → Wrapper*(fold head) → Wrapper expanded → // InnerData*(fold head) → InnerData expanded int foldHeadCount = 0; for (const LineMeta& lm : result.meta) { if (lm.foldHead && lm.nodeKind == NodeKind::Pointer64) foldHeadCount++; } // At least 2 fold-head pointers in the expansion chain (rptr + bptr) // Plus standalone renderings of StructB and StructC QVERIFY2(foldHeadCount >= 2, qPrintable(QString("Expected >=2 pointer fold heads, got %1") .arg(foldHeadCount))); } void testPointerMutualCycleAtoB() { // A→B→A: Main has ptr to StructB, StructB has ptr back to Main // Must not infinite-loop 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 mf; mf.kind = NodeKind::UInt32; mf.name = "tag"; mf.parentId = mainId; mf.offset = 0; tree.addNode(mf); // StructB Node structB; structB.kind = NodeKind::Struct; structB.name = "StructB"; structB.parentId = 0; structB.offset = 200; int bi = tree.addNode(structB); uint64_t structBId = tree.nodes[bi].id; Node bf; bf.kind = NodeKind::UInt32; bf.name = "data"; bf.parentId = structBId; bf.offset = 0; tree.addNode(bf); // Main → StructB pointer Node ptrToB; ptrToB.kind = NodeKind::Pointer64; ptrToB.name = "to_b"; ptrToB.parentId = mainId; ptrToB.offset = 4; ptrToB.refId = structBId; tree.addNode(ptrToB); // StructB → Main pointer (creates cycle!) Node ptrToMain; ptrToMain.kind = NodeKind::Pointer64; ptrToMain.name = "back"; ptrToMain.parentId = structBId; ptrToMain.offset = 4; ptrToMain.refId = mainId; tree.addNode(ptrToMain); // Provider: Main.to_b at offset 4 → addr 100 // StructB expanded at 100: back at 100+4=104 → addr 50 // Main expanded at 50: to_b at 50+4=54 → addr 100 (same as before → cycle!) QByteArray data(300, '\0'); uint64_t val1 = 100; memcpy(data.data() + 4, &val1, 8); // Main.to_b → 100 uint64_t val2 = 50; memcpy(data.data() + 104, &val2, 8); // StructB.back at 104 → 50 uint64_t val3 = 100; memcpy(data.data() + 54, &val3, 8); // Main.to_b at 54 → 100 (cycle) BufferProvider prov(data); ComposeResult result = compose(tree, prov); // MUST terminate with bounded output QVERIFY(result.meta.size() > 0); QVERIFY2(result.meta.size() < 100, qPrintable(QString("Cycle should be bounded, got %1 lines") .arg(result.meta.size()))); // Both StructB* and Main* should appear bool foundToB = false, foundToMain = false; QStringList lines = result.text.split('\n'); for (const QString& l : lines) { if (l.contains("StructB*")) foundToB = true; if (l.contains("Main*")) foundToMain = true; } QVERIFY2(foundToB, "Should display 'StructB*'"); QVERIFY2(foundToMain, "Should display 'Main*'"); // The first expansion of each pointer works; // the cycle is caught on the second attempt. // Main root header is suppressed, and pointer deref uses isArrayChild=true // (which also skips headers), so we verify cycle detection by bounded output above. } void testAllStructsResolvedAsPointerTargets() { // Multiple structs in the tree; pointers to each should display the name 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; // Create several structs QStringList structNames = {"Alpha", "Bravo", "Charlie", "Delta"}; QVector structIds; for (int i = 0; i < structNames.size(); i++) { Node s; s.kind = NodeKind::Struct; s.name = structNames[i]; s.structTypeName = structNames[i]; s.parentId = 0; s.offset = 1000 + i * 100; int si = tree.addNode(s); structIds << tree.nodes[si].id; // Give each struct a field Node f; f.kind = NodeKind::UInt32; f.name = "x"; f.parentId = tree.nodes[si].id; f.offset = 0; tree.addNode(f); } // Create a pointer to each struct for (int i = 0; i < structIds.size(); i++) { Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QString("ptr_%1").arg(structNames[i].toLower()); ptr.parentId = rootId; ptr.offset = i * 8; ptr.refId = structIds[i]; ptr.collapsed = true; // don't expand tree.addNode(ptr); } NullProvider prov; ComposeResult result = compose(tree, prov); // Every struct name should appear in a "Name*" format QStringList lines = result.text.split('\n'); for (const QString& sname : structNames) { QString expected = QString("%1*").arg(sname); bool found = false; for (const QString& l : lines) { if (l.contains(expected)) { found = true; break; } } QVERIFY2(found, qPrintable(QString("Should display '%1'").arg(expected))); } } void testPointerRefIdToDeletedStruct() { // If refId points to a non-existent node, degrade to void* 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 ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "dangling"; ptr.parentId = rootId; ptr.offset = 0; ptr.refId = 99999; // non-existent ID tree.addNode(ptr); NullProvider prov; ComposeResult result = compose(tree, prov); // Should not crash, and degrade to void QStringList lines = result.text.split('\n'); bool foundVoid = false; for (const QString& l : lines) { if (l.contains("void*")) { foundVoid = true; break; } } QVERIFY2(foundVoid, "Dangling refId should degrade to void*"); } void testPointerCollapsedNoExpansion() { // Collapsed pointer with valid non-null target must NOT expand 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 target; target.kind = NodeKind::Struct; target.name = "Heavy"; target.parentId = 0; target.offset = 200; int ti = tree.addNode(target); uint64_t targetId = tree.nodes[ti].id; // Many children in target - would inflate output if expanded for (int i = 0; i < 10; i++) { Node f; f.kind = NodeKind::UInt64; f.name = QString("f%1").arg(i); f.parentId = targetId; f.offset = i * 8; tree.addNode(f); } Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "heavy_ptr"; ptr.parentId = rootId; ptr.offset = 0; ptr.refId = targetId; ptr.collapsed = true; // COLLAPSED tree.addNode(ptr); // Non-null pointer value QByteArray data(300, '\0'); uint64_t ptrVal = 100; memcpy(data.data(), &ptrVal, 8); BufferProvider prov(data); ComposeResult result = compose(tree, prov); // Count lines belonging to depth > 1 inside Root // (There should be NONE because the pointer is collapsed) int expandedLines = 0; for (const LineMeta& lm : result.meta) { // Lines at depth >= 2 would be inside the pointer expansion if (lm.depth >= 2 && lm.nodeIdx >= 0 && tree.nodes[lm.nodeIdx].parentId == targetId) expandedLines++; } // Standalone Heavy rendering adds lines at depth 1, // but pointer expansion at depth >= 2 should be zero QCOMPARE(expandedLines, 0); } void testPointerWidthComputation() { // Type column must be wide enough for "LongStructName*" 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 target; target.kind = NodeKind::Struct; target.name = "VeryLongStructNameForTesting"; target.structTypeName = "VeryLongStructNameForTesting"; target.parentId = 0; target.offset = 200; int ti = tree.addNode(target); uint64_t targetId = tree.nodes[ti].id; Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "lptr"; ptr.parentId = rootId; ptr.offset = 0; ptr.refId = targetId; ptr.collapsed = true; tree.addNode(ptr); NullProvider prov; ComposeResult result = compose(tree, prov); // The text must contain the FULL target name, not truncated QStringList lines = result.text.split('\n'); bool foundFull = false; for (const QString& l : lines) { if (l.contains("VeryLongStructNameForTesting*")) { foundFull = true; break; } } QVERIFY2(foundFull, "Type column should be wide enough for long pointer target names"); // Layout type width should accommodate the long name // "VeryLongStructNameForTesting*" = 29 chars QVERIFY2(result.layout.typeW >= 29, qPrintable(QString("typeW=%1, should be >= 29").arg(result.layout.typeW))); } // ═════════════════════════════════════════════════════════════ // Class keyword + alignment tests // ═════════════════════════════════════════════════════════════ void testClassKeywordJsonRoundTrip() { NodeTree tree; tree.baseAddress = 0; Node root; root.kind = NodeKind::Struct; root.name = "Root"; root.parentId = 0; root.classKeyword = "class"; 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); // Save and reload QJsonObject json = tree.toJson(); NodeTree tree2 = NodeTree::fromJson(json); // Find the root struct in the reloaded tree bool found = false; for (const auto& n : tree2.nodes) { if (n.kind == NodeKind::Struct && n.name == "Root") { QCOMPARE(n.classKeyword, QString("class")); QCOMPARE(n.resolvedClassKeyword(), QString("class")); found = true; break; } } QVERIFY2(found, "Root struct should exist after JSON round-trip"); } void testClassKeywordDefaultsToStruct() { NodeTree tree; Node root; root.kind = NodeKind::Struct; root.name = "Root"; root.parentId = 0; // classKeyword left empty tree.addNode(root); QJsonObject json = tree.toJson(); NodeTree tree2 = NodeTree::fromJson(json); for (const auto& n : tree2.nodes) { if (n.kind == NodeKind::Struct) { QVERIFY(n.classKeyword.isEmpty()); QCOMPARE(n.resolvedClassKeyword(), QString("struct")); break; } } } void testComputeStructAlignment() { 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; // Int32 has alignment 4 Node f1; f1.kind = NodeKind::Int32; f1.name = "a"; f1.parentId = rootId; f1.offset = 0; tree.addNode(f1); QCOMPARE(tree.computeStructAlignment(rootId), 4); // Add Hex64 (alignment 8) — max should become 8 Node f2; f2.kind = NodeKind::Hex64; f2.name = "b"; f2.parentId = rootId; f2.offset = 8; tree.addNode(f2); QCOMPARE(tree.computeStructAlignment(rootId), 8); } void testComputeStructAlignmentEmpty() { NodeTree tree; Node root; root.kind = NodeKind::Struct; root.name = "Empty"; root.parentId = 0; int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; // Empty struct → alignment 1 QCOMPARE(tree.computeStructAlignment(rootId), 1); } void testCommandRowRootNameSpan() { // Name span should cover the class name in the merged command row QString text = "source\u25BE \u00B7 0x0 \u00B7 struct\u25BE MyClass {"; ColumnSpan nameSpan = commandRowRootNameSpan(text); QVERIFY(nameSpan.valid); QString nameText = text.mid(nameSpan.start, nameSpan.end - nameSpan.start); QVERIFY2(nameText.trimmed() == "MyClass", qPrintable("Name span should be 'MyClass', got: '" + nameText.trimmed() + "'")); } void testTextIsNonEmpty() { // Verify composed text is actually generated (not empty) NodeTree tree; tree.baseAddress = 0x1000; Node root; root.kind = NodeKind::Struct; root.name = "TestStruct"; root.parentId = 0; int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; // Mix of types including pointers and arrays Node f1; f1.kind = NodeKind::UInt64; f1.name = "id"; f1.parentId = rootId; f1.offset = 0; tree.addNode(f1); Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = "next"; ptr.parentId = rootId; ptr.offset = 8; tree.addNode(ptr); Node arr; arr.kind = NodeKind::Array; arr.name = "buf"; arr.parentId = rootId; arr.offset = 16; arr.elementKind = NodeKind::Hex8; arr.arrayLen = 16; arr.collapsed = true; tree.addNode(arr); NullProvider prov; ComposeResult result = compose(tree, prov); QVERIFY2(!result.text.isEmpty(), "Composed text must not be empty"); QVERIFY2(result.meta.size() >= 5, qPrintable(QString("Expected >= 5 lines, got %1").arg(result.meta.size()))); // Every line should have text content QStringList lines = result.text.split('\n'); QCOMPARE(lines.size(), result.meta.size()); for (int i = 0; i < lines.size(); i++) { QVERIFY2(!lines[i].isEmpty(), qPrintable(QString("Line %1 is empty").arg(i))); } } }; QTEST_MAIN(TestCompose) #include "test_compose.moc"