mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Fix 13 logic bugs and UI issues across editor, controller, and core
Round 1: Fix updateCommandRow offset, structTypeName undo, changeNodeKind macro, shift-click kCommandRowId leak, type filter byte-vs-column bug. Round 2: Move kFooterIdBit to core.h, add RcxEditor destructor for cursor cleanup, defer refresh during batch ops, use newline separator in type picker, narrow selection on double-click edit, clear hover on keyboard scroll, guard 0x prefix from deletion, cap array count at 100k.
This commit is contained in:
454
tests/test_controller.cpp
Normal file
454
tests/test_controller.cpp
Normal file
@@ -0,0 +1,454 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QtTest/QSignalSpy>
|
||||
#include <QApplication>
|
||||
#include <QSplitter>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include "controller.h"
|
||||
#include "core.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
// Small tree: one root struct with a few typed fields at known offsets.
|
||||
// Keeps tests fast and deterministic (no giant PEB tree).
|
||||
static void buildSmallTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "TestStruct";
|
||||
root.name = "root";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
field(0, NodeKind::UInt32, "field_u32"); // 4 bytes
|
||||
field(4, NodeKind::Float, "field_float"); // 4 bytes
|
||||
field(8, NodeKind::UInt8, "field_u8"); // 1 byte
|
||||
field(9, NodeKind::Padding, "pad0"); // 3 bytes padding
|
||||
// Set padding arrayLen = 3 for 3-byte padding
|
||||
tree.nodes.last().arrayLen = 3;
|
||||
field(12, NodeKind::Hex32, "field_hex"); // 4 bytes
|
||||
}
|
||||
|
||||
// 64-byte buffer with recognizable pattern
|
||||
static QByteArray makeSmallBuffer() {
|
||||
QByteArray data(64, '\0');
|
||||
// field_u32 at offset 0 = 0xDEADBEEF
|
||||
uint32_t v32 = 0xDEADBEEF;
|
||||
memcpy(data.data() + 0, &v32, 4);
|
||||
// field_float at offset 4 = 3.14f
|
||||
float vf = 3.14f;
|
||||
memcpy(data.data() + 4, &vf, 4);
|
||||
// field_u8 at offset 8 = 0x42
|
||||
data[8] = 0x42;
|
||||
// pad0 at offset 9 = 0x00 0x00 0x00
|
||||
// field_hex at offset 12 = 0xCAFEBABE
|
||||
uint32_t vhex = 0xCAFEBABE;
|
||||
memcpy(data.data() + 12, &vhex, 4);
|
||||
return data;
|
||||
}
|
||||
|
||||
class TestController : public QObject {
|
||||
Q_OBJECT
|
||||
private:
|
||||
RcxDocument* m_doc = nullptr;
|
||||
RcxController* m_ctrl = nullptr;
|
||||
QSplitter* m_splitter = nullptr;
|
||||
RcxEditor* m_editor = nullptr;
|
||||
|
||||
private slots:
|
||||
void init() {
|
||||
m_doc = new RcxDocument();
|
||||
buildSmallTree(m_doc->tree);
|
||||
m_doc->provider = std::make_unique<BufferProvider>(makeSmallBuffer());
|
||||
|
||||
m_splitter = new QSplitter();
|
||||
// Pass nullptr as parent so controller is not auto-deleted with splitter
|
||||
m_ctrl = new RcxController(m_doc, nullptr);
|
||||
m_editor = m_ctrl->addSplitEditor(m_splitter);
|
||||
|
||||
m_splitter->resize(800, 600);
|
||||
m_splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(m_splitter));
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
// Delete controller first (disconnects from editor signals)
|
||||
delete m_ctrl;
|
||||
m_ctrl = nullptr;
|
||||
m_editor = nullptr; // owned by splitter
|
||||
delete m_splitter;
|
||||
m_splitter = nullptr;
|
||||
delete m_doc;
|
||||
m_doc = nullptr;
|
||||
}
|
||||
|
||||
// ── Test: setNodeValue writes bytes to provider ──
|
||||
void testSetNodeValueWritesData() {
|
||||
// Find field_u32 (index 1, child of root at index 0)
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
|
||||
// Verify original value in provider
|
||||
uint64_t addr = m_doc->tree.computeOffset(idx);
|
||||
QByteArray origBytes = m_doc->provider->readBytes(addr, 4);
|
||||
uint32_t origVal;
|
||||
memcpy(&origVal, origBytes.data(), 4);
|
||||
QCOMPARE(origVal, (uint32_t)0xDEADBEEF);
|
||||
|
||||
// Write new value "42" (decimal)
|
||||
m_ctrl->setNodeValue(idx, 0, "42");
|
||||
QApplication::processEvents();
|
||||
|
||||
// Read back: should be 42 in little-endian
|
||||
QByteArray newBytes = m_doc->provider->readBytes(addr, 4);
|
||||
uint32_t newVal;
|
||||
memcpy(&newVal, newBytes.data(), 4);
|
||||
QCOMPARE(newVal, (uint32_t)42);
|
||||
}
|
||||
|
||||
// ── Test: setNodeValue undo/redo restores data ──
|
||||
void testSetNodeValueUndoRedo() {
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
uint64_t addr = m_doc->tree.computeOffset(idx);
|
||||
|
||||
// Original: 0xDEADBEEF
|
||||
QByteArray orig = m_doc->provider->readBytes(addr, 4);
|
||||
uint32_t origVal;
|
||||
memcpy(&origVal, orig.data(), 4);
|
||||
QCOMPARE(origVal, (uint32_t)0xDEADBEEF);
|
||||
|
||||
// Write new value
|
||||
m_ctrl->setNodeValue(idx, 0, "99");
|
||||
QApplication::processEvents();
|
||||
|
||||
uint32_t newVal;
|
||||
QByteArray after = m_doc->provider->readBytes(addr, 4);
|
||||
memcpy(&newVal, after.data(), 4);
|
||||
QCOMPARE(newVal, (uint32_t)99);
|
||||
|
||||
// Undo → should restore original
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
|
||||
QByteArray undone = m_doc->provider->readBytes(addr, 4);
|
||||
uint32_t undoneVal;
|
||||
memcpy(&undoneVal, undone.data(), 4);
|
||||
QCOMPARE(undoneVal, (uint32_t)0xDEADBEEF);
|
||||
|
||||
// Redo → should restore new value
|
||||
m_doc->undoStack.redo();
|
||||
QApplication::processEvents();
|
||||
|
||||
QByteArray redone = m_doc->provider->readBytes(addr, 4);
|
||||
uint32_t redoneVal;
|
||||
memcpy(&redoneVal, redone.data(), 4);
|
||||
QCOMPARE(redoneVal, (uint32_t)99);
|
||||
}
|
||||
|
||||
// ── Test: setNodeValue on Float field ──
|
||||
void testSetNodeValueFloat() {
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_float") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
uint64_t addr = m_doc->tree.computeOffset(idx);
|
||||
|
||||
// Original: 3.14f
|
||||
QByteArray orig = m_doc->provider->readBytes(addr, 4);
|
||||
float origVal;
|
||||
memcpy(&origVal, orig.data(), 4);
|
||||
QVERIFY(qAbs(origVal - 3.14f) < 0.01f);
|
||||
|
||||
// Write "1.5"
|
||||
m_ctrl->setNodeValue(idx, 0, "1.5");
|
||||
QApplication::processEvents();
|
||||
|
||||
QByteArray after = m_doc->provider->readBytes(addr, 4);
|
||||
float newVal;
|
||||
memcpy(&newVal, after.data(), 4);
|
||||
QCOMPARE(newVal, 1.5f);
|
||||
|
||||
// Undo
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QByteArray undone = m_doc->provider->readBytes(addr, 4);
|
||||
float undoneVal;
|
||||
memcpy(&undoneVal, undone.data(), 4);
|
||||
QVERIFY(qAbs(undoneVal - 3.14f) < 0.01f);
|
||||
}
|
||||
|
||||
// ── Test: renameNode changes name and undo restores ──
|
||||
void testRenameNode() {
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].name, QString("field_u32"));
|
||||
|
||||
m_ctrl->renameNode(idx, "myRenamedField");
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes[idx].name, QString("myRenamedField"));
|
||||
|
||||
// Undo
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[idx].name, QString("field_u32"));
|
||||
|
||||
// Redo
|
||||
m_doc->undoStack.redo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[idx].name, QString("myRenamedField"));
|
||||
}
|
||||
|
||||
// ── Test: changeNodeKind changes type and undo restores ──
|
||||
void testChangeNodeKind() {
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
|
||||
|
||||
m_ctrl->changeNodeKind(idx, NodeKind::Float);
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::Float);
|
||||
|
||||
// Undo
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
|
||||
}
|
||||
|
||||
// ── Test: insertNode adds a node, removeNode removes it, undo restores ──
|
||||
void testInsertAndRemoveNode() {
|
||||
int origSize = m_doc->tree.nodes.size();
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
// Insert a new Hex64 at offset 16
|
||||
m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "newHex");
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
|
||||
// Find the inserted node
|
||||
int newIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "newHex") { newIdx = i; break; }
|
||||
}
|
||||
QVERIFY(newIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::Hex64);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].offset, 16);
|
||||
|
||||
// Remove it
|
||||
m_ctrl->removeNode(newIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize);
|
||||
|
||||
// Undo remove → node restored
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
|
||||
// Find again
|
||||
newIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "newHex") { newIdx = i; break; }
|
||||
}
|
||||
QVERIFY(newIdx >= 0);
|
||||
}
|
||||
|
||||
// ── Test: Padding value edit is effectively blocked at controller level ──
|
||||
void testPaddingValueEditIsBlocked() {
|
||||
// Find the padding node
|
||||
int padIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].kind == NodeKind::Padding) { padIdx = i; break; }
|
||||
}
|
||||
QVERIFY(padIdx >= 0);
|
||||
uint64_t addr = m_doc->tree.computeOffset(padIdx);
|
||||
|
||||
// Read original data at padding offset
|
||||
int padSize = m_doc->tree.nodes[padIdx].byteSize();
|
||||
QByteArray origData = m_doc->provider->readBytes(addr, padSize);
|
||||
|
||||
// The context menu blocks Padding editing, so the controller's setNodeValue
|
||||
// would only be called if the editing UI somehow allows it. But let's verify
|
||||
// the editor correctly blocks it.
|
||||
// Find padding line in composed output
|
||||
ComposeResult result = m_doc->compose();
|
||||
int paddingLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
paddingLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(paddingLine >= 0);
|
||||
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// beginInlineEdit(Value) on Padding line must be rejected
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine));
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
|
||||
// Data must be unchanged
|
||||
QByteArray afterData = m_doc->provider->readBytes(addr, padSize);
|
||||
QCOMPARE(afterData, origData);
|
||||
}
|
||||
|
||||
// ── Test: setNodeValue with Hex32 (space-separated hex bytes) ──
|
||||
void testSetNodeValueHex() {
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_hex") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
uint64_t addr = m_doc->tree.computeOffset(idx);
|
||||
|
||||
// Original: 0xCAFEBABE
|
||||
QByteArray orig = m_doc->provider->readBytes(addr, 4);
|
||||
uint32_t origVal;
|
||||
memcpy(&origVal, orig.data(), 4);
|
||||
QCOMPARE(origVal, (uint32_t)0xCAFEBABE);
|
||||
|
||||
// Write space-separated hex bytes "AA BB CC DD"
|
||||
m_ctrl->setNodeValue(idx, 0, "AA BB CC DD");
|
||||
QApplication::processEvents();
|
||||
|
||||
QByteArray after = m_doc->provider->readBytes(addr, 4);
|
||||
QCOMPARE((uint8_t)after[0], (uint8_t)0xAA);
|
||||
QCOMPARE((uint8_t)after[1], (uint8_t)0xBB);
|
||||
QCOMPARE((uint8_t)after[2], (uint8_t)0xCC);
|
||||
QCOMPARE((uint8_t)after[3], (uint8_t)0xDD);
|
||||
|
||||
// Undo
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QByteArray undone = m_doc->provider->readBytes(addr, 4);
|
||||
uint32_t undoneVal;
|
||||
memcpy(&undoneVal, undone.data(), 4);
|
||||
QCOMPARE(undoneVal, (uint32_t)0xCAFEBABE);
|
||||
}
|
||||
|
||||
// ── Test: full inline edit round-trip (type in editor → commit → verify provider) ──
|
||||
void testInlineEditRoundTrip() {
|
||||
// Refresh to get composed output
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find field_u8 line (UInt8 at offset 8, value = 0x42 = 66)
|
||||
ComposeResult result = m_doc->compose();
|
||||
int fieldLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::UInt8 &&
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
fieldLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(fieldLine >= 0);
|
||||
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Select this node so edit is allowed
|
||||
uint64_t nodeId = result.meta[fieldLine].nodeId;
|
||||
QSet<uint64_t> sel;
|
||||
sel.insert(nodeId);
|
||||
m_editor->applySelectionOverlay(sel);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Begin value edit
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Value, fieldLine);
|
||||
QVERIFY2(ok, "Should be able to begin value edit on UInt8 field");
|
||||
QVERIFY(m_editor->isEditing());
|
||||
|
||||
// UInt8 values display in hex (e.g., "0x42"). beginInlineEdit selects
|
||||
// from after "0x" to end. Type "FF" to replace the hex digits.
|
||||
for (QChar c : QString("FF")) {
|
||||
QKeyEvent key(QEvent::KeyPress, 0, Qt::NoModifier, QString(c));
|
||||
QApplication::sendEvent(m_editor->scintilla(), &key);
|
||||
}
|
||||
QApplication::processEvents();
|
||||
|
||||
// Commit
|
||||
QSignalSpy spy(m_editor, &RcxEditor::inlineEditCommitted);
|
||||
QKeyEvent enter(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
|
||||
QApplication::sendEvent(m_editor->scintilla(), &enter);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QList<QVariant> args = spy.first();
|
||||
int nodeIdx = args.at(0).toInt();
|
||||
QString text = args.at(3).toString().trimmed();
|
||||
// The committed text should contain "0xFF" (hex format for UInt8)
|
||||
QVERIFY2(!text.isEmpty(), "Committed text should not be empty");
|
||||
|
||||
// Now simulate what controller does: setNodeValue
|
||||
m_ctrl->setNodeValue(nodeIdx, 0, text);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify provider data changed
|
||||
int u8Idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_u8") { u8Idx = i; break; }
|
||||
}
|
||||
QVERIFY(u8Idx >= 0);
|
||||
uint64_t addr = m_doc->tree.computeOffset(u8Idx);
|
||||
QByteArray bytes = m_doc->provider->readBytes(addr, 1);
|
||||
QCOMPARE((uint8_t)bytes[0], (uint8_t)0xFF);
|
||||
}
|
||||
|
||||
// ── Test: toggleCollapse + undo ──
|
||||
void testToggleCollapse() {
|
||||
// Root is index 0, a Struct node
|
||||
QCOMPARE(m_doc->tree.nodes[0].kind, NodeKind::Struct);
|
||||
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
|
||||
|
||||
m_ctrl->toggleCollapse(0);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[0].collapsed, true);
|
||||
|
||||
m_ctrl->toggleCollapse(0);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
|
||||
|
||||
// Undo twice: uncollapse → collapse → original (false)
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[0].collapsed, true);
|
||||
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestController)
|
||||
#include "test_controller.moc"
|
||||
Reference in New Issue
Block a user