#include #include #include #include #include #include #include #include "editor.h" #include "core.h" using namespace rcx; // 0x7D0 bytes of PEB-like data with recognizable values at key offsets static BufferProvider makeTestProvider() { QByteArray data(0x7D0, '\0'); auto w8 = [&](int off, uint8_t v) { data[off] = (char)v; }; auto w16 = [&](int off, uint16_t v) { memcpy(data.data()+off, &v, 2); }; auto w32 = [&](int off, uint32_t v) { memcpy(data.data()+off, &v, 4); }; auto w64 = [&](int off, uint64_t v) { memcpy(data.data()+off, &v, 8); }; w8 (0x002, 1); // BeingDebugged w8 (0x003, 0x04); // BitField w64(0x008, 0xFFFFFFFFFFFFFFFFULL); // Mutant (-1) w64(0x010, 0x00007FF6DE120000ULL); // ImageBaseAddress w64(0x018, 0x00007FFE3B8B53C0ULL); // Ldr w64(0x020, 0x000001A4C3E20F90ULL); // ProcessParameters w64(0x028, 0x0000000000000000ULL); // SubSystemData w64(0x030, 0x000001A4C3D40000ULL); // ProcessHeap w64(0x038, 0x00007FFE3B8D4260ULL); // FastPebLock w64(0x040, 0x0000000000000000ULL); // AtlThunkSListPtr w64(0x048, 0x0000000000000000ULL); // IFEOKey w32(0x050, 0x01); // CrossProcessFlags w64(0x058, 0x00007FFE3B720000ULL); // KernelCallbackTable w32(0x060, 0); // SystemReserved w32(0x064, 0); // AtlThunkSListPtr32 w64(0x068, 0x00007FFE3E570000ULL); // ApiSetMap w32(0x070, 0); // TlsExpansionCounter w64(0x078, 0x00007FFE3B8D3F50ULL); // TlsBitmap w32(0x080, 0x00000003); // TlsBitmapBits[0] w32(0x084, 0x00000000); // TlsBitmapBits[1] w64(0x088, 0x00007FFE38800000ULL); // ReadOnlySharedMemoryBase w64(0x090, 0x00007FFE38820000ULL); // SharedData w64(0x098, 0x00007FFE388A0000ULL); // ReadOnlyStaticServerData w64(0x0A0, 0x00007FFE3B8D1000ULL); // AnsiCodePageData w64(0x0A8, 0x00007FFE3B8D2040ULL); // OemCodePageData w64(0x0B0, 0x00007FFE3B8CE020ULL); // UnicodeCaseTableData w32(0x0B8, 8); // NumberOfProcessors w32(0x0BC, 0x70); // NtGlobalFlag w64(0x0C0, 0xFFFFFFFF7C91E000ULL); // CriticalSectionTimeout w64(0x0C8, 0x0000000000100000ULL); // HeapSegmentReserve w64(0x0D0, 0x0000000000002000ULL); // HeapSegmentCommit w64(0x0D8, 0x0000000000040000ULL); // HeapDeCommitTotalFreeThreshold w64(0x0E0, 0x0000000000001000ULL); // HeapDeCommitFreeBlockThreshold w32(0x0E8, 4); // NumberOfHeaps w32(0x0EC, 16); // MaximumNumberOfHeaps w64(0x0F0, 0x000001A4C3D40688ULL); // ProcessHeaps w64(0x0F8, 0x00007FFE388B0000ULL); // GdiSharedHandleTable w64(0x100, 0x0000000000000000ULL); // ProcessStarterHelper w32(0x108, 0); // GdiDCAttributeList w64(0x110, 0x00007FFE3B8D42E8ULL); // LoaderLock w32(0x118, 10); // OSMajorVersion w32(0x11C, 0); // OSMinorVersion w16(0x120, 19045); // OSBuildNumber w16(0x122, 0); // OSCSDVersion w32(0x124, 2); // OSPlatformId w32(0x128, 3); // ImageSubsystem (CUI) w32(0x12C, 10); // ImageSubsystemMajorVersion w32(0x130, 0); // ImageSubsystemMinorVersion w64(0x138, 0x00000000000000FFULL); // ActiveProcessAffinityMask w64(0x230, 0x0000000000000000ULL); // PostProcessInitRoutine w64(0x238, 0x00007FFE3B8D3F70ULL); // TlsExpansionBitmap w32(0x2C0, 1); // SessionId w64(0x2C8, 0x0000000000000000ULL); // AppCompatFlags w64(0x2D0, 0x0000000000000000ULL); // AppCompatFlagsUser w64(0x2D8, 0x0000000000000000ULL); // pShimData w64(0x2E0, 0x0000000000000000ULL); // AppCompatInfo w16(0x2E8, 0); // CSDVersion.Length w16(0x2EA, 0); // CSDVersion.MaximumLength w64(0x2F0, 0x0000000000000000ULL); // CSDVersion.Buffer w64(0x2F8, 0x000001A4C3E21000ULL); // ActivationContextData w64(0x300, 0x000001A4C3E22000ULL); // ProcessAssemblyStorageMap w64(0x308, 0x00007FFE38840000ULL); // SystemDefaultActivationContextData w64(0x310, 0x00007FFE38850000ULL); // SystemAssemblyStorageMap w64(0x318, 0x0000000000002000ULL); // MinimumStackCommit w64(0x330, 0x0000000000000000ULL); // PatchLoaderData w64(0x338, 0x0000000000000000ULL); // ChpeV2ProcessInfo w32(0x340, 0); // AppModelFeatureState w16(0x34C, 1252); // ActiveCodePage w16(0x34E, 437); // OemCodePage w16(0x350, 0); // UseCaseMapping w16(0x352, 0); // UnusedNlsField w64(0x358, 0x000001A4C3E30000ULL); // WerRegistrationData w64(0x360, 0x0000000000000000ULL); // WerShipAssertPtr w64(0x368, 0x0000000000000000ULL); // EcCodeBitMap w64(0x370, 0x0000000000000000ULL); // pImageHeaderHash w32(0x378, 0); // TracingFlags w64(0x380, 0x00007FFE38890000ULL); // CsrServerReadOnlySharedMemoryBase w64(0x388, 0x0000000000000000ULL); // TppWorkerpListLock w64(0x390, 0x000000D87B5E5390ULL); // TppWorkerpList.Flink (self) w64(0x398, 0x000000D87B5E5390ULL); // TppWorkerpList.Blink (self) w64(0x7A0, 0x0000000000000000ULL); // TelemetryCoverageHeader w32(0x7A8, 0); // CloudFileFlags w32(0x7AC, 0); // CloudFileDiagFlags w8 (0x7B0, 0); // PlaceholderCompatibilityMode w64(0x7B8, 0x00007FFE38860000ULL); // LeapSecondData w32(0x7C0, 0); // LeapSecondFlags w32(0x7C4, 0); // NtGlobalFlag2 w64(0x7C8, 0x0000000000000000ULL); // ExtendedFeatureDisableMask return BufferProvider(data, "peb_snapshot.bin"); } // Build the full _PEB64 tree (0x7D0 bytes), unions mapped to first member static NodeTree makeTestTree() { NodeTree tree; tree.baseAddress = 0x000000D87B5E5000ULL; // Root struct Node root; root.kind = NodeKind::Struct; root.structTypeName = "_PEB64"; root.name = "Peb"; root.parentId = 0; root.offset = 0; int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; // Helpers auto field = [&](int off, NodeKind k, const char* name) { Node n; n.kind = k; n.name = name; n.parentId = rootId; n.offset = off; tree.addNode(n); }; auto pad = [&](int off, int len, const char* name) { Node n; n.kind = NodeKind::Padding; n.name = name; n.parentId = rootId; n.offset = off; n.arrayLen = len; tree.addNode(n); }; auto arr = [&](int off, NodeKind ek, int len, const char* name) { Node n; n.kind = NodeKind::Array; n.name = name; n.parentId = rootId; n.offset = off; n.arrayLen = len; n.elementKind = ek; tree.addNode(n); }; auto sub = [&](int off, const char* ty, const char* name) -> uint64_t { Node n; n.kind = NodeKind::Struct; n.structTypeName = ty; n.name = name; n.parentId = rootId; n.offset = off; int idx = tree.addNode(n); return tree.nodes[idx].id; }; // ── 0x000 – 0x007 ── field(0x000, NodeKind::UInt8, "InheritedAddressSpace"); field(0x001, NodeKind::UInt8, "ReadImageFileExecOptions"); field(0x002, NodeKind::UInt8, "BeingDebugged"); field(0x003, NodeKind::UInt8, "BitField"); // union → first member pad (0x004, 4, "Padding0"); // ── 0x008 – 0x04F ── field(0x008, NodeKind::Pointer64, "Mutant"); field(0x010, NodeKind::Pointer64, "ImageBaseAddress"); field(0x018, NodeKind::Pointer64, "Ldr"); field(0x020, NodeKind::Pointer64, "ProcessParameters"); field(0x028, NodeKind::Pointer64, "SubSystemData"); field(0x030, NodeKind::Pointer64, "ProcessHeap"); field(0x038, NodeKind::Pointer64, "FastPebLock"); field(0x040, NodeKind::Pointer64, "AtlThunkSListPtr"); field(0x048, NodeKind::Pointer64, "IFEOKey"); // ── 0x050 – 0x07F ── field(0x050, NodeKind::UInt32, "CrossProcessFlags"); // union → first member pad (0x054, 4, "Padding1"); field(0x058, NodeKind::Pointer64, "KernelCallbackTable"); // union → first member field(0x060, NodeKind::UInt32, "SystemReserved"); field(0x064, NodeKind::UInt32, "AtlThunkSListPtr32"); field(0x068, NodeKind::Pointer64, "ApiSetMap"); field(0x070, NodeKind::UInt32, "TlsExpansionCounter"); pad (0x074, 4, "Padding2"); field(0x078, NodeKind::Pointer64, "TlsBitmap"); arr (0x080, NodeKind::UInt32, 2, "TlsBitmapBits"); // ── 0x088 – 0x0BF ── field(0x088, NodeKind::Pointer64, "ReadOnlySharedMemoryBase"); field(0x090, NodeKind::Pointer64, "SharedData"); field(0x098, NodeKind::Pointer64, "ReadOnlyStaticServerData"); field(0x0A0, NodeKind::Pointer64, "AnsiCodePageData"); field(0x0A8, NodeKind::Pointer64, "OemCodePageData"); field(0x0B0, NodeKind::Pointer64, "UnicodeCaseTableData"); field(0x0B8, NodeKind::UInt32, "NumberOfProcessors"); field(0x0BC, NodeKind::Hex32, "NtGlobalFlag"); // ── 0x0C0 – 0x0EF ── field(0x0C0, NodeKind::UInt64, "CriticalSectionTimeout"); // _LARGE_INTEGER union field(0x0C8, NodeKind::UInt64, "HeapSegmentReserve"); field(0x0D0, NodeKind::UInt64, "HeapSegmentCommit"); field(0x0D8, NodeKind::UInt64, "HeapDeCommitTotalFreeThreshold"); field(0x0E0, NodeKind::UInt64, "HeapDeCommitFreeBlockThreshold"); field(0x0E8, NodeKind::UInt32, "NumberOfHeaps"); field(0x0EC, NodeKind::UInt32, "MaximumNumberOfHeaps"); // ── 0x0F0 – 0x13F ── field(0x0F0, NodeKind::Pointer64, "ProcessHeaps"); field(0x0F8, NodeKind::Pointer64, "GdiSharedHandleTable"); field(0x100, NodeKind::Pointer64, "ProcessStarterHelper"); field(0x108, NodeKind::UInt32, "GdiDCAttributeList"); pad (0x10C, 4, "Padding3"); field(0x110, NodeKind::Pointer64, "LoaderLock"); field(0x118, NodeKind::UInt32, "OSMajorVersion"); field(0x11C, NodeKind::UInt32, "OSMinorVersion"); field(0x120, NodeKind::UInt16, "OSBuildNumber"); field(0x122, NodeKind::UInt16, "OSCSDVersion"); field(0x124, NodeKind::UInt32, "OSPlatformId"); field(0x128, NodeKind::UInt32, "ImageSubsystem"); field(0x12C, NodeKind::UInt32, "ImageSubsystemMajorVersion"); field(0x130, NodeKind::UInt32, "ImageSubsystemMinorVersion"); pad (0x134, 4, "Padding4"); field(0x138, NodeKind::UInt64, "ActiveProcessAffinityMask"); // ── 0x140 – 0x22F ── arr (0x140, NodeKind::UInt32, 60, "GdiHandleBuffer"); // ── 0x230 – 0x2BF ── field(0x230, NodeKind::Pointer64, "PostProcessInitRoutine"); field(0x238, NodeKind::Pointer64, "TlsExpansionBitmap"); arr (0x240, NodeKind::UInt32, 32, "TlsExpansionBitmapBits"); // ── 0x2C0 – 0x2E7 ── field(0x2C0, NodeKind::UInt32, "SessionId"); pad (0x2C4, 4, "Padding5"); field(0x2C8, NodeKind::UInt64, "AppCompatFlags"); // _ULARGE_INTEGER union field(0x2D0, NodeKind::UInt64, "AppCompatFlagsUser"); // _ULARGE_INTEGER union field(0x2D8, NodeKind::Pointer64, "pShimData"); field(0x2E0, NodeKind::Pointer64, "AppCompatInfo"); // ── 0x2E8 – 0x2F7: _STRING64 CSDVersion (nested struct) ── { uint64_t sid = sub(0x2E8, "_STRING64", "CSDVersion"); Node n; n.parentId = sid; n.kind = NodeKind::UInt16; n.name = "Length"; n.offset = 0; tree.addNode(n); n.kind = NodeKind::UInt16; n.name = "MaximumLength"; n.offset = 2; tree.addNode(n); n.kind = NodeKind::Padding; n.name = "Pad"; n.offset = 4; n.arrayLen = 4; tree.addNode(n); n.kind = NodeKind::Pointer64; n.name = "Buffer"; n.offset = 8; n.arrayLen = 1; tree.addNode(n); } // ── 0x2F8 – 0x31F ── field(0x2F8, NodeKind::Pointer64, "ActivationContextData"); field(0x300, NodeKind::Pointer64, "ProcessAssemblyStorageMap"); field(0x308, NodeKind::Pointer64, "SystemDefaultActivationContextData"); field(0x310, NodeKind::Pointer64, "SystemAssemblyStorageMap"); field(0x318, NodeKind::UInt64, "MinimumStackCommit"); // ── 0x320 – 0x34B ── arr (0x320, NodeKind::UInt64, 2, "SparePointers"); field(0x330, NodeKind::Pointer64, "PatchLoaderData"); field(0x338, NodeKind::Pointer64, "ChpeV2ProcessInfo"); field(0x340, NodeKind::UInt32, "AppModelFeatureState"); arr (0x344, NodeKind::UInt32, 2, "SpareUlongs"); field(0x34C, NodeKind::UInt16, "ActiveCodePage"); field(0x34E, NodeKind::UInt16, "OemCodePage"); field(0x350, NodeKind::UInt16, "UseCaseMapping"); field(0x352, NodeKind::UInt16, "UnusedNlsField"); // ── 0x354 – 0x37F (implicit padding + fields) ── pad (0x354, 4, "Pad354"); field(0x358, NodeKind::Pointer64, "WerRegistrationData"); field(0x360, NodeKind::Pointer64, "WerShipAssertPtr"); field(0x368, NodeKind::Pointer64, "EcCodeBitMap"); field(0x370, NodeKind::Pointer64, "pImageHeaderHash"); field(0x378, NodeKind::UInt32, "TracingFlags"); // union → first member pad (0x37C, 4, "Padding6"); // ── 0x380 – 0x39F ── field(0x380, NodeKind::Pointer64, "CsrServerReadOnlySharedMemoryBase"); field(0x388, NodeKind::UInt64, "TppWorkerpListLock"); // ── 0x390 – 0x39F: LIST_ENTRY64 TppWorkerpList (nested struct) ── { uint64_t sid = sub(0x390, "LIST_ENTRY64", "TppWorkerpList"); Node n; n.parentId = sid; n.kind = NodeKind::Pointer64; n.name = "Flink"; n.offset = 0; tree.addNode(n); n.kind = NodeKind::Pointer64; n.name = "Blink"; n.offset = 8; tree.addNode(n); } // ── 0x3A0 – 0x79F ── arr (0x3A0, NodeKind::UInt64, 128, "WaitOnAddressHashTable"); // ── 0x7A0 – 0x7CF ── field(0x7A0, NodeKind::Pointer64, "TelemetryCoverageHeader"); field(0x7A8, NodeKind::UInt32, "CloudFileFlags"); field(0x7AC, NodeKind::UInt32, "CloudFileDiagFlags"); field(0x7B0, NodeKind::Int8, "PlaceholderCompatibilityMode"); arr (0x7B1, NodeKind::Int8, 7, "PlaceholderCompatibilityModeReserved"); field(0x7B8, NodeKind::Pointer64, "LeapSecondData"); field(0x7C0, NodeKind::UInt32, "LeapSecondFlags"); // union → first member field(0x7C4, NodeKind::UInt32, "NtGlobalFlag2"); field(0x7C8, NodeKind::UInt64, "ExtendedFeatureDisableMask"); 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(); BufferProvider prov = makeTestProvider(); m_result = compose(tree, prov); m_editor->applyDocument(m_result); } void cleanupTestCase() { delete m_editor; } // ── Test: CommandRow at line 0 rejects non-ADDR edits ── void testCommandRowLineRejectsEdits() { m_editor->applyDocument(m_result); // Line 0 should be the CommandRow const LineMeta* lm = m_editor->metaForLine(0); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::CommandRow); QCOMPARE(lm->nodeId, kCommandRowId); QCOMPARE(lm->nodeIdx, -1); // Type/Name/Value should be rejected on CommandRow QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, 0)); QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, 0)); QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, 0)); QVERIFY(!m_editor->isEditing()); // Set CommandRow text with an ADDR value (simulates controller.updateCommandRow) m_editor->setCommandRowText( QStringLiteral(" File Address: 0xD87B5E5000")); // BaseAddress should be ALLOWED on CommandRow (ADDR field) bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); QVERIFY2(ok, "BaseAddress edit should be allowed on CommandRow"); QVERIFY(m_editor->isEditing()); m_editor->cancelInlineEdit(); // Source should be ALLOWED on CommandRow (SRC field) ok = m_editor->beginInlineEdit(EditTarget::Source, 0); QVERIFY2(ok, "Source edit should be allowed on CommandRow"); QVERIFY(m_editor->isEditing()); m_editor->cancelInlineEdit(); QApplication::processEvents(); // flush deferred showSourcePicker timer } // ── Test: inline edit lifecycle (begin → commit → re-edit) ── void testInlineEditReEntry() { // Move cursor to line 2 (first field inside struct; line 0=CommandRow, 1=header) m_editor->scintilla()->setCursorPosition(2, 0); // Should not be editing QVERIFY(!m_editor->isEditing()); // Begin edit on Name column bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2); 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, 2); 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(2, 0); // Begin value edit bool ok = m_editor->beginInlineEdit(EditTarget::Value, 2); 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, 2); 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, 2); 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: 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, 2); 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 and Type valid, Value invalid) ── void testHeaderLineEdit() { m_editor->applyDocument(m_result); // Line 1 should be the struct header (line 0 is CommandRow) const LineMeta* lm = m_editor->metaForLine(1); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Header); // Type edit on header should succeed (has typename _PEB64) bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1); QVERIFY(ok); QVERIFY(m_editor->isEditing()); m_editor->cancelInlineEdit(); // Name edit on header should succeed ok = m_editor->beginInlineEdit(EditTarget::Name, 1); 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: 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 (raw byte order, no endian conversion) b = fmt::parseValue(NodeKind::Hex32, "DE AD BE EF", &ok); QVERIFY(ok); QCOMPARE(b.size(), 4); QCOMPARE((uint8_t)b[0], (uint8_t)0xDE); QCOMPARE((uint8_t)b[1], (uint8_t)0xAD); QCOMPARE((uint8_t)b[2], (uint8_t)0xBE); QCOMPARE((uint8_t)b[3], (uint8_t)0xEF); // 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); QCOMPARE((uint8_t)b[0], (uint8_t)0x4D); QCOMPARE((uint8_t)b[1], (uint8_t)0x5A); QCOMPARE((uint8_t)b[7], (uint8_t)0x00); // Hex64 continuous - stores as native-endian (numeric value preserved) b = fmt::parseValue(NodeKind::Hex64, "4D5A900000000000", &ok); QVERIFY(ok); uint64_t v64; memcpy(&v64, b.data(), 8); QCOMPARE(v64, (uint64_t)0x4D5A900000000000); // 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, 2); QVERIFY(ok); // Autocomplete is deferred via QTimer::singleShot(0) — poll until active QTRY_VERIFY2(m_editor->scintilla()->SendScintilla( QsciScintillaBase::SCI_AUTOCACTIVE) != 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 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, 2); 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) // First field at line 2 is InheritedAddressSpace (UInt8 → "uint8_t") QList args = commitSpy.first(); QString committedText = args.at(3).toString(); QVERIFY2(committedText == "uint8_t", qPrintable("Expected 'uint8_t', got: " + committedText)); m_editor->applyDocument(m_result); } // ── Test: column span hit-testing for cursor shape ── void testColumnSpanHitTest() { m_editor->applyDocument(m_result); // Line 2 is a field line (UInt8), verify spans are valid (line 0=CommandRow, 1=header) const LineMeta* lm = m_editor->metaForLine(2); 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)2); 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 (line 2; 0=CommandRow, 1=header) m_editor->scintilla()->setCursorPosition(2, 0); QSet sel = m_editor->selectedNodeIndices(); QCOMPARE(sel.size(), 1); // The node index should match the first field const LineMeta* lm = m_editor->metaForLine(2); QVERIFY(lm); QVERIFY(sel.contains(lm->nodeIdx)); } // ── Test: header line no longer contains "// base:" ── void testBaseAddressDisplay() { NodeTree tree = makeTestTree(); tree.baseAddress = 0x10; BufferProvider prov = makeTestProvider(); ComposeResult result = compose(tree, prov); m_editor->applyDocument(result); // Line 1 should be the struct header (line 0 is CommandRow) const LineMeta* lm = m_editor->metaForLine(1); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Header); QVERIFY(lm->isRootHeader); // Get header line text — should NOT contain "// base:" (consolidated into cmd bar) QString lineText; int len = (int)m_editor->scintilla()->SendScintilla( QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1); if (len > 0) { QByteArray buf(len + 1, '\0'); m_editor->scintilla()->SendScintilla( QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data()); lineText = QString::fromUtf8(buf.constData(), len).trimmed(); } QVERIFY2(!lineText.contains("// base:"), qPrintable("Header should no longer contain '// base:', got: " + lineText)); QVERIFY2(lineText.contains("struct"), qPrintable("Header should contain 'struct', got: " + lineText)); m_editor->applyDocument(m_result); } // ── Test: CommandRow ADDR span is valid ── void testBaseAddressSpan() { m_editor->applyDocument(m_result); // Set CommandRow text with ADDR value (simulates controller) m_editor->setCommandRowText( QStringLiteral(" File Address: 0xD87B5E5000")); // Line 0 is CommandRow const LineMeta* lm = m_editor->metaForLine(0); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::CommandRow); // Get CommandRow line text QString lineText; int len = (int)m_editor->scintilla()->SendScintilla( QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0); if (len > 0) { QByteArray buf(len + 1, '\0'); m_editor->scintilla()->SendScintilla( QsciScintillaBase::SCI_GETLINE, (unsigned long)0, (void*)buf.data()); lineText = QString::fromUtf8(buf.constData(), len); while (lineText.endsWith('\n') || lineText.endsWith('\r')) lineText.chop(1); } // ADDR span should be valid (uses commandRowAddrSpan) ColumnSpan as = commandRowAddrSpan(lineText); QVERIFY2(as.valid, "ADDR span should be valid on CommandRow"); QVERIFY(as.start < as.end); // The span should cover the hex address QString spanText = lineText.mid(as.start, as.end - as.start); QVERIFY2(spanText.contains("0x") || spanText.startsWith("0X"), qPrintable("Span should contain hex address, got: " + spanText)); m_editor->applyDocument(m_result); } // ── Test: base address edit begins on CommandRow (line 0) ── void testBaseAddressEditBegins() { m_editor->applyDocument(m_result); // Set CommandRow text with ADDR value (simulates controller) m_editor->setCommandRowText( QStringLiteral(" File Address: 0xD87B5E5000")); // Begin base address edit on line 0 (CommandRow ADDR field) bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); QVERIFY2(ok, "Should be able to begin base address edit on CommandRow"); QVERIFY(m_editor->isEditing()); // Cancel and reset m_editor->cancelInlineEdit(); m_editor->applyDocument(m_result); } }; QTEST_MAIN(TestEditor) #include "test_editor.moc"