mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: type selector overhaul, fuzzy search, address parser, value tracking
Redesign type selector popup with fuzzy subsequence matching, per-category icons, field summary tooltips, compact chips, and pointer target primitives. Add address expression parser with arithmetic and register support. Enable track value changes by default.
This commit is contained in:
@@ -213,6 +213,186 @@ private slots:
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x600ULL);
|
||||
}
|
||||
|
||||
// -- Identifier resolution --
|
||||
|
||||
void identBase() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "base");
|
||||
return *ok ? 0x140000000ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x140000000ULL);
|
||||
}
|
||||
|
||||
void identFieldName() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x140000000ULL; }
|
||||
if (name == "e_lfanew") { *ok = true; return 0xE8ULL; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base + e_lfanew", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x1400000E8ULL);
|
||||
}
|
||||
|
||||
void identUnknown() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString&, bool* ok) -> uint64_t {
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("unknown_var", 8, &cbs);
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("unknown identifier"));
|
||||
}
|
||||
|
||||
// -- Hex vs identifier disambiguation --
|
||||
|
||||
void hexDisambigDEAD() {
|
||||
// "DEAD" is all hex digits → should parse as hex number 0xDEAD
|
||||
auto r = AddressParser::evaluate("DEAD");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xDEADULL);
|
||||
}
|
||||
|
||||
void hexDisambigBase() {
|
||||
// "base" has 's' (non-hex) → identifier
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "base"); return *ok ? 42ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 42ULL);
|
||||
}
|
||||
|
||||
void hexDisambigABCwithUnderscore() {
|
||||
// "ABC_field" has '_' → identifier, not hex
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "ABC_field"); return *ok ? 99ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("ABC_field", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 99ULL);
|
||||
}
|
||||
|
||||
// -- Bitwise operators --
|
||||
|
||||
void bitwiseAnd() {
|
||||
auto r = AddressParser::evaluate("0xFF & 0x0F");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x0FULL);
|
||||
}
|
||||
|
||||
void bitwiseOr() {
|
||||
auto r = AddressParser::evaluate("0xA0 | 0x0B");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xABULL);
|
||||
}
|
||||
|
||||
void bitwiseXor() {
|
||||
auto r = AddressParser::evaluate("0xA ^ 0x5");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xFULL);
|
||||
}
|
||||
|
||||
void shiftLeft() {
|
||||
auto r = AddressParser::evaluate("1 << 4");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x10ULL);
|
||||
}
|
||||
|
||||
void shiftRight() {
|
||||
auto r = AddressParser::evaluate("0xFF00 >> 8");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xFFULL);
|
||||
}
|
||||
|
||||
// -- Unary bitwise NOT --
|
||||
|
||||
void unaryNot() {
|
||||
auto r = AddressParser::evaluate("~0");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xFFFFFFFFFFFFFFFFULL);
|
||||
}
|
||||
|
||||
void unaryNotMask() {
|
||||
// ~0xFFF = 0xFFFFFFFFFFFFF000
|
||||
auto r = AddressParser::evaluate("~0xFFF");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xFFFFFFFFFFFFF000ULL);
|
||||
}
|
||||
|
||||
// -- Operator precedence --
|
||||
|
||||
void shiftPrecedence() {
|
||||
// C precedence: shift binds looser than addition
|
||||
// 1 + 2 << 3 = (1 + 2) << 3 = 3 << 3 = 24 = 0x18
|
||||
auto r = AddressParser::evaluate("1 + 2 << 3");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x18ULL);
|
||||
}
|
||||
|
||||
void andOrPrecedence() {
|
||||
// & binds tighter than |
|
||||
// 0xFF | 0x100 & 0xF00 = 0xFF | (0x100 & 0xF00) = 0xFF | 0x100 = 0x1FF
|
||||
auto r = AddressParser::evaluate("0xFF | 0x100 & 0xF00");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x1FFULL);
|
||||
}
|
||||
|
||||
void xorPrecedence() {
|
||||
// ^ between & and |: a | b ^ c & d = a | (b ^ (c & d))
|
||||
// 0xF0 | 0x0F ^ 0xFF & 0x0F = 0xF0 | (0x0F ^ (0xFF & 0x0F))
|
||||
// = 0xF0 | (0x0F ^ 0x0F) = 0xF0 | 0x00 = 0xF0
|
||||
auto r = AddressParser::evaluate("0xF0 | 0x0F ^ 0xFF & 0x0F");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xF0ULL);
|
||||
}
|
||||
|
||||
// -- E_lfanew end-to-end --
|
||||
|
||||
void elfanewScenario() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x140000000ULL; }
|
||||
if (name == "e_lfanew") { *ok = true; return 0xE8ULL; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
// base + e_lfanew = 0x140000000 + 0xE8 = 0x1400000E8
|
||||
auto r = AddressParser::evaluate("base + e_lfanew", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x1400000E8ULL);
|
||||
}
|
||||
|
||||
void pageAlignedExpr() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x140000000ULL; }
|
||||
if (name == "e_lfanew") { *ok = true; return 0xE8ULL; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
// (base + e_lfanew) & ~0xFFF = 0x1400000E8 & ~0xFFF = 0x140000000
|
||||
auto r = AddressParser::evaluate("(base + e_lfanew) & ~0xFFF", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x140000000ULL);
|
||||
}
|
||||
|
||||
// -- Validate with new syntax --
|
||||
|
||||
void validateIdentifier() {
|
||||
QCOMPARE(AddressParser::validate("base + e_lfanew"), QString());
|
||||
}
|
||||
|
||||
void validateBitwiseOps() {
|
||||
QCOMPARE(AddressParser::validate("0xFF & 0x0F"), QString());
|
||||
QCOMPARE(AddressParser::validate("1 << 4"), QString());
|
||||
QCOMPARE(AddressParser::validate("~0xFFF"), QString());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestAddressParser)
|
||||
|
||||
@@ -2435,6 +2435,198 @@ private slots:
|
||||
QCOMPARE(n.byteSize(), 8);
|
||||
}
|
||||
|
||||
// ── Helper node compose tests ──
|
||||
|
||||
void testHelperSeparatorLine() {
|
||||
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;
|
||||
|
||||
// Regular field
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "field_a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Helper node
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = "my_helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(helper);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Separator with "helpers" text and box-drawing chars should appear
|
||||
QVERIFY2(result.text.contains(QStringLiteral("helpers")),
|
||||
qPrintable("Expected 'helpers' separator in:\n" + result.text));
|
||||
QVERIFY2(result.text.contains(QStringLiteral("\u2500")),
|
||||
qPrintable("Expected box-drawing separator char in:\n" + result.text));
|
||||
}
|
||||
|
||||
void testHelperDoesNotAffectStructSize() {
|
||||
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 f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Struct span without helper
|
||||
int spanBefore = tree.structSpan(rootId);
|
||||
|
||||
// Add helper
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Struct;
|
||||
helper.name = "helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + 100");
|
||||
tree.addNode(helper);
|
||||
|
||||
int spanAfter = tree.structSpan(rootId);
|
||||
QCOMPARE(spanAfter, spanBefore);
|
||||
}
|
||||
|
||||
void testHelperIsHelperLineFlag() {
|
||||
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 f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "field_a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = "my_helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(helper);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// At least one line should have isHelperLine set
|
||||
bool foundHelper = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.isHelperLine) {
|
||||
foundHelper = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundHelper, "Expected at least one LineMeta with isHelperLine=true");
|
||||
}
|
||||
|
||||
void testHelperCollapsedByDefault() {
|
||||
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;
|
||||
|
||||
// Helper struct with a child (should still appear collapsed)
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Struct;
|
||||
helper.name = "inner";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
helper.collapsed = true;
|
||||
int hi = tree.addNode(helper);
|
||||
uint64_t helperId = tree.nodes[hi].id;
|
||||
|
||||
Node hChild;
|
||||
hChild.kind = NodeKind::UInt32;
|
||||
hChild.name = "x";
|
||||
hChild.parentId = helperId;
|
||||
hChild.offset = 0;
|
||||
tree.addNode(hChild);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// The helper's child should NOT have a visible line (it's collapsed)
|
||||
bool foundChildLine = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.nodeIdx >= 0 && lm.nodeIdx < tree.nodes.size()
|
||||
&& tree.nodes[lm.nodeIdx].name == QStringLiteral("x")
|
||||
&& tree.nodes[lm.nodeIdx].parentId == helperId) {
|
||||
foundChildLine = true;
|
||||
}
|
||||
}
|
||||
QVERIFY2(!foundChildLine,
|
||||
"Helper's children should not be visible when collapsed");
|
||||
}
|
||||
|
||||
void testHelperExpressionShownInText() {
|
||||
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 helper;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = "my_helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + 0x10");
|
||||
tree.addNode(helper);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// The composed text should contain the expression and arrow
|
||||
QVERIFY2(result.text.contains(QStringLiteral("base + 0x10")),
|
||||
qPrintable("Expected expression in text:\n" + result.text));
|
||||
QVERIFY2(result.text.contains(QStringLiteral("\u2192")),
|
||||
qPrintable("Expected arrow (\u2192) in text:\n" + result.text));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCompose)
|
||||
|
||||
@@ -668,6 +668,181 @@ private slots:
|
||||
QVERIFY(newIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32);
|
||||
}
|
||||
// ── Helper node controller tests ──
|
||||
|
||||
void testAddHelper() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int origSize = m_doc->tree.nodes.size();
|
||||
|
||||
// Simulate "Add Helper" — same code as context menu action
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
const auto& h = m_doc->tree.nodes.back();
|
||||
QCOMPARE(h.isHelper, true);
|
||||
QCOMPARE(h.offsetExpr, QStringLiteral("base"));
|
||||
QCOMPARE(h.name, QStringLiteral("helper"));
|
||||
QCOMPARE(h.parentId, rootId);
|
||||
}
|
||||
|
||||
void testAddHelperUndo() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int origSize = m_doc->tree.nodes.size();
|
||||
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
|
||||
// Undo: helper should be gone
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize);
|
||||
|
||||
// Redo: helper should be back
|
||||
m_doc->undoStack.redo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
QCOMPARE(m_doc->tree.nodes.back().isHelper, true);
|
||||
}
|
||||
|
||||
void testChangeHelperExpression() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
// Add a helper
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t helperId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Change expression
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::ChangeOffsetExpr{helperId, QStringLiteral("base"), QStringLiteral("base + 0x10")}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(helperId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + 0x10"));
|
||||
|
||||
// Undo: old expression restored
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
idx = m_doc->tree.indexOfId(helperId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base"));
|
||||
}
|
||||
|
||||
void testDeleteHelperPreservesStructSize() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int spanBefore = m_doc->tree.structSpan(rootId);
|
||||
|
||||
// Add a helper
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Struct size unchanged after adding helper
|
||||
QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore);
|
||||
|
||||
// Remove helper
|
||||
uint64_t helperId = m_doc->tree.nodes.back().id;
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Remove{helperId}));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Struct size still unchanged
|
||||
QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore);
|
||||
}
|
||||
|
||||
void testHelperRenamePreservesExpression() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
// Add a helper
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("my_helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + field_u32");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t helperId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Rename the helper
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::Rename{helperId, QStringLiteral("my_helper"), QStringLiteral("renamed_helper")}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(helperId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].name, QStringLiteral("renamed_helper"));
|
||||
// Expression should be preserved
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + field_u32"));
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isHelper, true);
|
||||
}
|
||||
|
||||
void testHelperTypeChangePreservesFlags() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t helperId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Change kind to UInt32
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::ChangeKind{helperId, NodeKind::Hex64, NodeKind::UInt32}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(helperId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
|
||||
// Helper flags must survive type change
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isHelper, true);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestController)
|
||||
|
||||
@@ -671,6 +671,114 @@ private slots:
|
||||
QCOMPARE(h.count, 4); // 4 transitions
|
||||
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
|
||||
}
|
||||
|
||||
// ── Helper node serialization ──
|
||||
|
||||
void testHelperJsonRoundTrip() {
|
||||
rcx::NodeTree tree;
|
||||
tree.baseAddress = 0x14000000;
|
||||
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "DOS_HEADER";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node field;
|
||||
field.kind = rcx::NodeKind::UInt32;
|
||||
field.name = "e_lfanew";
|
||||
field.parentId = rootId;
|
||||
field.offset = 0x3C;
|
||||
tree.addNode(field);
|
||||
|
||||
rcx::Node helper;
|
||||
helper.kind = rcx::NodeKind::Struct;
|
||||
helper.name = "nt_hdr";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + e_lfanew");
|
||||
tree.addNode(helper);
|
||||
|
||||
QJsonObject json = tree.toJson();
|
||||
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json);
|
||||
|
||||
QCOMPARE(tree2.nodes.size(), 3);
|
||||
const auto& h = tree2.nodes[2];
|
||||
QCOMPARE(h.isHelper, true);
|
||||
QCOMPARE(h.offsetExpr, QStringLiteral("base + e_lfanew"));
|
||||
QCOMPARE(h.name, QStringLiteral("nt_hdr"));
|
||||
}
|
||||
|
||||
void testHelperJsonBackwardCompat() {
|
||||
// Old JSON without isHelper/offsetExpr should load with defaults
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
|
||||
QJsonObject json = tree.toJson();
|
||||
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json);
|
||||
|
||||
QCOMPARE(tree2.nodes[0].isHelper, false);
|
||||
QCOMPARE(tree2.nodes[0].offsetExpr, QString());
|
||||
}
|
||||
|
||||
void testStructSpanExcludesHelpers() {
|
||||
using namespace rcx;
|
||||
NodeTree tree;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Regular field: offset 0, size 4
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Regular field: offset 4, size 8
|
||||
Node f2;
|
||||
f2.kind = NodeKind::UInt64;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 4;
|
||||
tree.addNode(f2);
|
||||
|
||||
// Helper: should NOT affect span
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Struct;
|
||||
helper.name = "helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(helper);
|
||||
|
||||
// Span should be max(0+4, 4+8) = 12, same as without helper
|
||||
QCOMPARE(tree.structSpan(rootId), 12);
|
||||
}
|
||||
|
||||
void testHelperExprSpanFor() {
|
||||
using namespace rcx;
|
||||
// Simulate a helper header line: " ▸ struct NT_HEADERS nt_hdr = base + e_lfanew → 0x1400000E8"
|
||||
LineMeta lm;
|
||||
lm.isHelperLine = true;
|
||||
QString lineText = QStringLiteral(" \u25B8 struct NT_HEADERS nt_hdr = base + e_lfanew \u2192 0x1400000E8");
|
||||
ColumnSpan span = helperExprSpanFor(lm, lineText);
|
||||
QVERIFY(span.valid);
|
||||
QString expr = lineText.mid(span.start, span.end - span.start);
|
||||
QCOMPARE(expr.trimmed(), QStringLiteral("base + e_lfanew"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCore)
|
||||
|
||||
@@ -4,62 +4,92 @@
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
|
||||
int main()
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
const char* connStr = "tcp:Port=5057,Server=localhost";
|
||||
const char* connStr = "tcp:Port=5055,Server=localhost";
|
||||
if (argc > 1) connStr = argv[1];
|
||||
|
||||
// Initialize COM — required for DbgEng remote transport (TCP/named-pipe)
|
||||
HRESULT hrCom = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||
printf("CoInitializeEx: 0x%08lX\n", hrCom);
|
||||
fflush(stdout);
|
||||
|
||||
printf("Attempting DebugConnect(\"%s\")...\n", connStr);
|
||||
fflush(stdout);
|
||||
|
||||
IDebugClient* client = nullptr;
|
||||
HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client);
|
||||
printf("DebugConnect returned: 0x%08lX\n", hr);
|
||||
fflush(stdout);
|
||||
|
||||
if (SUCCEEDED(hr) && client) {
|
||||
printf("Connected! Getting IDebugDataSpaces...\n");
|
||||
printf("Connected! Getting interfaces...\n");
|
||||
fflush(stdout);
|
||||
|
||||
IDebugDataSpaces* ds = nullptr;
|
||||
hr = client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds);
|
||||
printf("QueryInterface(IDebugDataSpaces) = 0x%08lX\n", hr);
|
||||
fflush(stdout);
|
||||
|
||||
if (ds) {
|
||||
IDebugControl* ctrl = nullptr;
|
||||
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
|
||||
IDebugControl* ctrl = nullptr;
|
||||
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
|
||||
|
||||
if (ctrl) {
|
||||
printf("Waiting for event...\n");
|
||||
hr = ctrl->WaitForEvent(0, 5000);
|
||||
printf("WaitForEvent = 0x%08lX\n", hr);
|
||||
ctrl->Release();
|
||||
}
|
||||
if (ctrl) {
|
||||
printf("Calling WaitForEvent(5000ms)...\n");
|
||||
fflush(stdout);
|
||||
hr = ctrl->WaitForEvent(0, 5000);
|
||||
printf("WaitForEvent = 0x%08lX\n", hr);
|
||||
fflush(stdout);
|
||||
|
||||
// Try to read 2 bytes
|
||||
IDebugSymbols* sym = nullptr;
|
||||
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
|
||||
if (sym) {
|
||||
ULONG numMods = 0, numUnloaded = 0;
|
||||
hr = sym->GetNumberModules(&numMods, &numUnloaded);
|
||||
printf("GetNumberModules = 0x%08lX, numMods=%lu\n", hr, numMods);
|
||||
|
||||
if (numMods > 0) {
|
||||
ULONG64 base = 0;
|
||||
hr = sym->GetModuleByIndex(0, &base);
|
||||
printf("Module[0] base = 0x%llX (hr=0x%08lX)\n", base, hr);
|
||||
|
||||
if (SUCCEEDED(hr) && base) {
|
||||
uint8_t buf[4] = {};
|
||||
ULONG got = 0;
|
||||
hr = ds->ReadVirtual(base, buf, 4, &got);
|
||||
printf("ReadVirtual(%llX, 4) = 0x%08lX, got=%lu, data=[%02X %02X %02X %02X]\n",
|
||||
base, hr, got, buf[0], buf[1], buf[2], buf[3]);
|
||||
}
|
||||
}
|
||||
sym->Release();
|
||||
}
|
||||
ds->Release();
|
||||
ULONG debugClass = 0, debugQual = 0;
|
||||
hr = ctrl->GetDebuggeeType(&debugClass, &debugQual);
|
||||
printf("GetDebuggeeType = 0x%08lX, class=%lu, qualifier=%lu\n",
|
||||
hr, debugClass, debugQual);
|
||||
printf(" -> %s\n", debugQual >= 1024 ? "DUMP" : "LIVE");
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
IDebugSymbols* sym = nullptr;
|
||||
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
|
||||
|
||||
if (sym) {
|
||||
ULONG numMods = 0, numUnloaded = 0;
|
||||
hr = sym->GetNumberModules(&numMods, &numUnloaded);
|
||||
printf("GetNumberModules = 0x%08lX, loaded=%lu, unloaded=%lu\n",
|
||||
hr, numMods, numUnloaded);
|
||||
fflush(stdout);
|
||||
|
||||
if (numMods > 0) {
|
||||
ULONG64 base = 0;
|
||||
hr = sym->GetModuleByIndex(0, &base);
|
||||
printf("Module[0] base = 0x%llX (hr=0x%08lX)\n", base, hr);
|
||||
fflush(stdout);
|
||||
|
||||
if (SUCCEEDED(hr) && base && ds) {
|
||||
uint8_t buf[4] = {};
|
||||
ULONG got = 0;
|
||||
hr = ds->ReadVirtual(base, buf, 4, &got);
|
||||
printf("ReadVirtual(0x%llX, 4) = 0x%08lX, got=%lu, data=[%02X %02X %02X %02X]\n",
|
||||
base, hr, got, buf[0], buf[1], buf[2], buf[3]);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
sym->Release();
|
||||
}
|
||||
|
||||
if (ds) ds->Release();
|
||||
if (ctrl) ctrl->Release();
|
||||
|
||||
printf("Disconnecting...\n");
|
||||
fflush(stdout);
|
||||
client->EndSession(DEBUG_END_DISCONNECT);
|
||||
client->Release();
|
||||
printf("Done.\n");
|
||||
} else {
|
||||
printf("DebugConnect FAILED. hr=0x%08lX\n", hr);
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
if (SUCCEEDED(hrCom)) CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -758,6 +758,121 @@ private slots:
|
||||
QVERIFY(!result.contains("struct _LIST_ENTRY\n{"));
|
||||
QVERIFY(!result.contains("uint8_t _pad"));
|
||||
}
|
||||
// ── Helper node generator tests ──
|
||||
|
||||
void testHelperNotInStructBody() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "MyStruct";
|
||||
root.structTypeName = "MyStruct";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node f1;
|
||||
f1.kind = rcx::NodeKind::UInt32;
|
||||
f1.name = "e_lfanew";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
rcx::Node helper;
|
||||
helper.kind = rcx::NodeKind::Struct;
|
||||
helper.name = "nt_hdr";
|
||||
helper.structTypeName = "IMAGE_NT_HEADERS";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + e_lfanew");
|
||||
tree.addNode(helper);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// Helper should NOT appear as a member in the struct body
|
||||
QVERIFY2(!result.contains("IMAGE_NT_HEADERS nt_hdr;"),
|
||||
qPrintable("Helper should not be in struct body:\n" + result));
|
||||
|
||||
// Helper SHOULD appear as a comment
|
||||
QVERIFY2(result.contains("// helper:"),
|
||||
qPrintable("Helper comment missing:\n" + result));
|
||||
QVERIFY2(result.contains("nt_hdr"),
|
||||
qPrintable("Helper name missing from comment:\n" + result));
|
||||
QVERIFY2(result.contains("base + e_lfanew"),
|
||||
qPrintable("Helper expression missing from comment:\n" + result));
|
||||
}
|
||||
|
||||
void testHelperCommentFormat() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node f1;
|
||||
f1.kind = rcx::NodeKind::UInt64;
|
||||
f1.name = "base_field";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
rcx::Node helper;
|
||||
helper.kind = rcx::NodeKind::Hex64;
|
||||
helper.name = "ptr";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + 0xFF");
|
||||
tree.addNode(helper);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// The regular field should be in the struct body
|
||||
QVERIFY(result.contains("uint64_t base_field;"));
|
||||
|
||||
// Helper emitted as comment after struct body
|
||||
QVERIFY(result.contains("// helper:"));
|
||||
QVERIFY(result.contains("@ base + 0xFF"));
|
||||
}
|
||||
|
||||
void testStructSizeUnchangedByHelper() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Small";
|
||||
root.structTypeName = "Small";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node f1;
|
||||
f1.kind = rcx::NodeKind::UInt32;
|
||||
f1.name = "x";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
rcx::Node helper;
|
||||
helper.kind = rcx::NodeKind::Struct;
|
||||
helper.name = "big_helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(helper);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId, nullptr, true);
|
||||
|
||||
// static_assert should use only the regular field size (4 bytes)
|
||||
QVERIFY2(result.contains("sizeof(Small) == 0x4"),
|
||||
qPrintable("Expected sizeof(Small) == 0x4:\n" + result));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestGenerator)
|
||||
|
||||
@@ -861,10 +861,11 @@ private slots:
|
||||
void testPopupWidthScalesWithFont() {
|
||||
TypeSelectorPopup popup;
|
||||
|
||||
// Use a very long name so even font-9 exceeds the minimum popup width
|
||||
TypeEntry comp;
|
||||
comp.entryKind = TypeEntry::Composite;
|
||||
comp.structId = 100;
|
||||
comp.displayName = QStringLiteral("MyLongStructName");
|
||||
comp.displayName = QStringLiteral("MyExtremelyLongStructNameThatExceedsMinWidth");
|
||||
comp.classKeyword = QStringLiteral("struct");
|
||||
popup.setTypes({comp});
|
||||
|
||||
@@ -1465,6 +1466,191 @@ private slots:
|
||||
QVERIFY2(!result.text.contains("hex64*"),
|
||||
qPrintable("Should not show 'hex64*', got: " + result.text));
|
||||
}
|
||||
// ── Category chips and three-group filtering ──
|
||||
|
||||
void testCategoryEnumOnEntry() {
|
||||
// Verify that Category enum values exist and are distinct
|
||||
TypeEntry prim;
|
||||
prim.category = TypeEntry::CatPrimitive;
|
||||
QCOMPARE(prim.category, TypeEntry::CatPrimitive);
|
||||
|
||||
TypeEntry typ;
|
||||
typ.category = TypeEntry::CatType;
|
||||
QCOMPARE(typ.category, TypeEntry::CatType);
|
||||
|
||||
TypeEntry en;
|
||||
en.category = TypeEntry::CatEnum;
|
||||
QCOMPARE(en.category, TypeEntry::CatEnum);
|
||||
|
||||
QVERIFY(TypeEntry::CatPrimitive != TypeEntry::CatType);
|
||||
QVERIFY(TypeEntry::CatType != TypeEntry::CatEnum);
|
||||
}
|
||||
|
||||
void testCategoryDefaultIsPrimitive() {
|
||||
TypeEntry e;
|
||||
QCOMPARE(e.category, TypeEntry::CatPrimitive);
|
||||
}
|
||||
|
||||
void testCompositesCategorizedInController() {
|
||||
// Build tree with struct and enum types
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node st;
|
||||
st.kind = NodeKind::Struct;
|
||||
st.name = "Ball";
|
||||
st.structTypeName = "Ball";
|
||||
st.parentId = 0;
|
||||
int si = tree.addNode(st);
|
||||
uint64_t stId = tree.nodes[si].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "x"; n.parentId = stId;
|
||||
n.offset = 0; tree.addNode(n); }
|
||||
|
||||
Node en;
|
||||
en.kind = NodeKind::Struct;
|
||||
en.name = "Color";
|
||||
en.structTypeName = "Color";
|
||||
en.classKeyword = QStringLiteral("enum");
|
||||
en.parentId = 0;
|
||||
tree.addNode(en);
|
||||
|
||||
// Simulate controller logic: tag composites
|
||||
QVector<TypeEntry> entries;
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.parentId != 0 || n.kind != NodeKind::Struct) continue;
|
||||
TypeEntry e;
|
||||
e.entryKind = TypeEntry::Composite;
|
||||
e.structId = n.id;
|
||||
e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
||||
e.classKeyword = n.resolvedClassKeyword();
|
||||
e.category = (e.classKeyword == QStringLiteral("enum"))
|
||||
? TypeEntry::CatEnum : TypeEntry::CatType;
|
||||
entries.append(e);
|
||||
}
|
||||
|
||||
QCOMPARE(entries.size(), 2);
|
||||
// Ball → CatType, Color → CatEnum
|
||||
bool foundType = false, foundEnum = false;
|
||||
for (const auto& e : entries) {
|
||||
if (e.displayName == "Ball") {
|
||||
QCOMPARE(e.category, TypeEntry::CatType);
|
||||
foundType = true;
|
||||
}
|
||||
if (e.displayName == "Color") {
|
||||
QCOMPARE(e.category, TypeEntry::CatEnum);
|
||||
foundEnum = true;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundType);
|
||||
QVERIFY(foundEnum);
|
||||
}
|
||||
|
||||
void testThreeGroupSections() {
|
||||
// Create popup and set types with mixed categories
|
||||
TypeSelectorPopup popup;
|
||||
popup.setMode(TypePopupMode::FieldType);
|
||||
|
||||
QVector<TypeEntry> types;
|
||||
|
||||
// A primitive
|
||||
TypeEntry prim;
|
||||
prim.entryKind = TypeEntry::Primitive;
|
||||
prim.primitiveKind = NodeKind::Int32;
|
||||
prim.displayName = QStringLiteral("int32_t");
|
||||
prim.category = TypeEntry::CatPrimitive;
|
||||
types.append(prim);
|
||||
|
||||
// A struct type
|
||||
TypeEntry st;
|
||||
st.entryKind = TypeEntry::Composite;
|
||||
st.structId = 1;
|
||||
st.displayName = QStringLiteral("Player");
|
||||
st.classKeyword = QStringLiteral("struct");
|
||||
st.category = TypeEntry::CatType;
|
||||
types.append(st);
|
||||
|
||||
// An enum type
|
||||
TypeEntry en;
|
||||
en.entryKind = TypeEntry::Composite;
|
||||
en.structId = 2;
|
||||
en.displayName = QStringLiteral("Color");
|
||||
en.classKeyword = QStringLiteral("enum");
|
||||
en.category = TypeEntry::CatEnum;
|
||||
types.append(en);
|
||||
|
||||
popup.setTypes(types);
|
||||
|
||||
// The popup should have three sections in field mode:
|
||||
// primitives → types → enums
|
||||
// We can access via the internal model
|
||||
auto* model = popup.findChild<QStringListModel*>();
|
||||
QVERIFY(model != nullptr);
|
||||
QStringList items = model->stringList();
|
||||
|
||||
// Should contain section headers
|
||||
bool hasPrimSection = false, hasTypeSection = false, hasEnumSection = false;
|
||||
for (const auto& item : items) {
|
||||
if (item == QStringLiteral("primitives")) hasPrimSection = true;
|
||||
if (item == QStringLiteral("types")) hasTypeSection = true;
|
||||
if (item == QStringLiteral("enums")) hasEnumSection = true;
|
||||
}
|
||||
QVERIFY2(hasPrimSection, "Missing 'primitives' section header");
|
||||
QVERIFY2(hasTypeSection, "Missing 'types' section header");
|
||||
QVERIFY2(hasEnumSection, "Missing 'enums' section header");
|
||||
}
|
||||
|
||||
// ── Test: struct embed auto-selects the current composite in popup ──
|
||||
|
||||
void testStructEmbedAutoSelectsCurrent() {
|
||||
TypeSelectorPopup popup;
|
||||
popup.setMode(TypePopupMode::FieldType);
|
||||
QFont font(QStringLiteral("Consolas"), 10);
|
||||
popup.setFont(font);
|
||||
|
||||
// Build entries: a primitive + two composites
|
||||
QVector<TypeEntry> types;
|
||||
|
||||
TypeEntry prim;
|
||||
prim.entryKind = TypeEntry::Primitive;
|
||||
prim.primitiveKind = NodeKind::Int32;
|
||||
prim.displayName = QStringLiteral("int32_t");
|
||||
types.append(prim);
|
||||
|
||||
TypeEntry alpha;
|
||||
alpha.entryKind = TypeEntry::Composite;
|
||||
alpha.structId = 100;
|
||||
alpha.displayName = QStringLiteral("Alpha");
|
||||
alpha.classKeyword = QStringLiteral("struct");
|
||||
alpha.category = TypeEntry::CatType;
|
||||
types.append(alpha);
|
||||
|
||||
TypeEntry bravo;
|
||||
bravo.entryKind = TypeEntry::Composite;
|
||||
bravo.structId = 200;
|
||||
bravo.displayName = QStringLiteral("Bravo");
|
||||
bravo.classKeyword = QStringLiteral("struct");
|
||||
bravo.category = TypeEntry::CatType;
|
||||
types.append(bravo);
|
||||
|
||||
// Set Bravo as the current type (simulates struct embed field with refId=200)
|
||||
popup.setTypes(types, &bravo);
|
||||
popup.popup(QPoint(-9999, -9999));
|
||||
QApplication::processEvents();
|
||||
|
||||
// The list view should auto-select the row matching Bravo
|
||||
auto* listView = popup.findChild<QListView*>();
|
||||
QVERIFY(listView != nullptr);
|
||||
QModelIndex sel = listView->currentIndex();
|
||||
QVERIFY2(sel.isValid(), "No item selected — auto-select failed");
|
||||
|
||||
// The selected row text should contain "Bravo"
|
||||
QString selectedText = sel.data().toString();
|
||||
QVERIFY2(selectedText.contains(QStringLiteral("Bravo")),
|
||||
qPrintable(QString("Expected 'Bravo' in selected text, got '%1'").arg(selectedText)));
|
||||
|
||||
popup.hide();
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestTypeSelector)
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <QtConcurrent>
|
||||
#include <QFuture>
|
||||
#include <cstring>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include "providers/provider.h"
|
||||
#include "../plugins/WinDbgMemory/WinDbgMemoryPlugin.h"
|
||||
@@ -87,20 +90,40 @@ private slots:
|
||||
// ── Fixture ──
|
||||
|
||||
/// Try a quick DebugConnect to see if the port is already serving.
|
||||
static bool canConnect(const QString& connStr)
|
||||
/// Runs in a detached thread with a timeout because DebugConnect can
|
||||
/// hang indefinitely with WinDbg Preview servers.
|
||||
static bool canConnect(const QString& connStr, int timeoutMs = 8000)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
IDebugClient* probe = nullptr;
|
||||
QByteArray utf8 = connStr.toUtf8();
|
||||
HRESULT hr = DebugConnect(utf8.constData(), IID_IDebugClient, (void**)&probe);
|
||||
if (SUCCEEDED(hr) && probe) {
|
||||
probe->EndSession(DEBUG_END_DISCONNECT);
|
||||
probe->Release();
|
||||
return true;
|
||||
std::atomic<int> state{0}; // 0=pending, 1=connected, -1=failed
|
||||
std::thread t([&state, utf8]() {
|
||||
CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||
IDebugClient* probe = nullptr;
|
||||
HRESULT hr = DebugConnect(utf8.constData(), IID_IDebugClient, (void**)&probe);
|
||||
if (SUCCEEDED(hr) && probe) {
|
||||
probe->EndSession(DEBUG_END_DISCONNECT);
|
||||
probe->Release();
|
||||
state.store(1);
|
||||
} else {
|
||||
state.store(-1);
|
||||
}
|
||||
CoUninitialize();
|
||||
});
|
||||
t.detach(); // Don't block on join — DebugConnect may hang forever
|
||||
|
||||
auto deadline = std::chrono::steady_clock::now()
|
||||
+ std::chrono::milliseconds(timeoutMs);
|
||||
while (state.load() == 0) {
|
||||
if (std::chrono::steady_clock::now() >= deadline) {
|
||||
qDebug() << "canConnect: DebugConnect timed out after" << timeoutMs << "ms";
|
||||
return false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
return false;
|
||||
return state.load() == 1;
|
||||
#else
|
||||
Q_UNUSED(connStr);
|
||||
Q_UNUSED(connStr); Q_UNUSED(timeoutMs);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
@@ -116,13 +139,18 @@ private slots:
|
||||
return;
|
||||
}
|
||||
|
||||
// No server running — launch cdb ourselves
|
||||
// No server running — try to launch cdb ourselves.
|
||||
// If cdb isn't available, user-mode tests will be skipped but
|
||||
// kernel/dump tests can still run via WINDBG_KERNEL_CONN.
|
||||
m_notepadPid = findProcess(L"notepad.exe");
|
||||
if (m_notepadPid == 0) {
|
||||
m_notepadPid = launchNotepad();
|
||||
m_weSpawnedNotepad = true;
|
||||
}
|
||||
QVERIFY2(m_notepadPid != 0, "Need notepad.exe running");
|
||||
if (m_notepadPid == 0) {
|
||||
qDebug() << "No notepad.exe and could not launch — user-mode tests will skip";
|
||||
return;
|
||||
}
|
||||
qDebug() << "Using notepad.exe PID:" << m_notepadPid;
|
||||
|
||||
m_cdbProcess = new QProcess(this);
|
||||
@@ -135,7 +163,12 @@ private slots:
|
||||
m_cdbProcess->setArguments(args);
|
||||
m_cdbProcess->start();
|
||||
|
||||
QVERIFY2(m_cdbProcess->waitForStarted(5000), "Failed to start cdb.exe");
|
||||
if (!m_cdbProcess->waitForStarted(5000)) {
|
||||
qDebug() << "Failed to start cdb.exe — user-mode tests will skip";
|
||||
delete m_cdbProcess;
|
||||
m_cdbProcess = nullptr;
|
||||
return;
|
||||
}
|
||||
QThread::sleep(3);
|
||||
|
||||
qDebug() << "cdb.exe debug server started on port" << DBG_PORT;
|
||||
@@ -448,47 +481,47 @@ private slots:
|
||||
delete raw;
|
||||
}
|
||||
|
||||
// ── Kernel session tests ──
|
||||
// Requires a WinDbg instance with a kernel dump loaded and
|
||||
// .server tcp:port=5055 running. Skipped automatically if
|
||||
// no server is available. Override with WINDBG_KERNEL_CONN env var.
|
||||
// ── Kernel/dump session tests ──
|
||||
// Set WINDBG_KERNEL_CONN to a target string:
|
||||
// "dump:F:/path/to/file.dmp" — open dump directly
|
||||
// "tcp:Port=5055,Server=localhost" — connect to debug server
|
||||
// Set WINDBG_KERNEL_ADDR to a readable hex address (e.g. kernel base).
|
||||
|
||||
static QString kernelTarget()
|
||||
{
|
||||
return qEnvironmentVariable("WINDBG_KERNEL_CONN", "");
|
||||
}
|
||||
|
||||
void provider_kernel_connect()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available (set WINDBG_KERNEL_CONN)");
|
||||
QString target = kernelTarget();
|
||||
if (target.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_CONN (e.g. dump:F:/file.dmp)");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY2(prov.isValid(), "Should connect to kernel debug server");
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("Should connect, lastError: " + prov.lastError()));
|
||||
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
|
||||
|
||||
qDebug() << "Kernel provider name:" << prov.name();
|
||||
qDebug() << "Kernel provider base:" << QString("0x%1").arg(prov.base(), 0, 16);
|
||||
qDebug() << "Kernel provider isLive:" << prov.isLive();
|
||||
|
||||
// Name should not be an arbitrary user-mode DLL
|
||||
QVERIFY2(!prov.name().contains("WS2_32", Qt::CaseInsensitive),
|
||||
qPrintable("Name should not be 'WS2_32', got: " + prov.name()));
|
||||
}
|
||||
|
||||
void provider_kernel_read_base()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
QString target = kernelTarget();
|
||||
if (target.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_CONN");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
// Provider no longer auto-selects a base. Use a known kernel address
|
||||
// from env, or skip.
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
if (addrStr.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
||||
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("lastError: " + prov.lastError()));
|
||||
|
||||
bool ok = false;
|
||||
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
||||
@@ -502,20 +535,21 @@ private slots:
|
||||
if (buf[i] != 0) { allZero = false; break; }
|
||||
}
|
||||
QVERIFY2(!allZero, "Kernel read returned all zeros");
|
||||
|
||||
qDebug() << "Read 16 bytes at" << QString("0x%1").arg(addr, 0, 16)
|
||||
<< "first 4:" << QString("%1 %2 %3 %4")
|
||||
.arg(buf[0], 2, 16, QChar('0'))
|
||||
.arg(buf[1], 2, 16, QChar('0'))
|
||||
.arg(buf[2], 2, 16, QChar('0'))
|
||||
.arg(buf[3], 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
void provider_kernel_read_high_address()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
QString target = kernelTarget();
|
||||
if (target.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_CONN");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
// Use env var for a specific kernel address (e.g. _EPROCESS),
|
||||
// otherwise fall back to the provider's base.
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
uint64_t addr = 0;
|
||||
if (!addrStr.isEmpty()) {
|
||||
@@ -523,7 +557,14 @@ private slots:
|
||||
addr = addrStr.toULongLong(&ok, 16);
|
||||
if (!ok) addr = 0;
|
||||
}
|
||||
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("lastError: " + prov.lastError()));
|
||||
|
||||
if (addr == 0) addr = prov.base();
|
||||
if (addr == 0)
|
||||
QSKIP("No kernel address available (set WINDBG_KERNEL_ADDR)");
|
||||
|
||||
uint8_t buf[64] = {};
|
||||
bool ok = prov.read(addr, buf, 64);
|
||||
@@ -550,10 +591,9 @@ private slots:
|
||||
|
||||
void provider_kernel_read_backgroundThread()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
QString target = kernelTarget();
|
||||
if (target.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_CONN");
|
||||
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
if (addrStr.isEmpty())
|
||||
@@ -563,8 +603,9 @@ private slots:
|
||||
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("lastError: " + prov.lastError()));
|
||||
|
||||
// Simulate the controller's async refresh pattern
|
||||
QFuture<QByteArray> future = QtConcurrent::run([&prov, addr]() -> QByteArray {
|
||||
|
||||
Reference in New Issue
Block a user