Files
archived-Reclass/tests/test_context_menu.cpp

460 lines
14 KiB
C++

#include <QtTest/QTest>
#include <QApplication>
#include <QSplitter>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "core.h"
using namespace rcx;
static void buildTree(NodeTree& tree) {
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "Player";
root.name = "Player";
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::Int32, "health");
field(4, NodeKind::Int32, "armor");
field(8, NodeKind::Float, "speed");
field(12, NodeKind::Hex32, "flags");
}
static QByteArray makeBuffer() {
QByteArray data(128, '\0');
int32_t health = 100;
memcpy(data.data() + 0, &health, 4);
int32_t armor = 50;
memcpy(data.data() + 4, &armor, 4);
float speed = 3.5f;
memcpy(data.data() + 8, &speed, 4);
uint32_t flags = 0xFF00FF00;
memcpy(data.data() + 12, &flags, 4);
return data;
}
class TestContextMenu : public QObject {
Q_OBJECT
private:
RcxDocument* m_doc = nullptr;
RcxController* m_ctrl = nullptr;
QSplitter* m_splitter = nullptr;
RcxEditor* m_editor = nullptr;
int findNode(const QString& name) const {
for (int i = 0; i < m_doc->tree.nodes.size(); i++)
if (m_doc->tree.nodes[i].name == name) return i;
return -1;
}
int countNodes() const { return m_doc->tree.nodes.size(); }
private slots:
void init() {
m_doc = new RcxDocument();
buildTree(m_doc->tree);
m_doc->provider = std::make_unique<BufferProvider>(makeBuffer());
m_splitter = new QSplitter();
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 m_ctrl; m_ctrl = nullptr;
m_editor = nullptr;
delete m_splitter; m_splitter = nullptr;
delete m_doc; m_doc = nullptr;
}
// ── Insert adds exactly one node ──
void testInsertAddsOneNode() {
int before = countNodes();
uint64_t rootId = m_doc->tree.nodes[0].id;
m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "inserted");
QApplication::processEvents();
QCOMPARE(countNodes(), before + 1);
int idx = findNode("inserted");
QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::Hex64);
QCOMPARE(m_doc->tree.nodes[idx].offset, 16);
QCOMPARE(m_doc->tree.nodes[idx].parentId, rootId);
}
// ── Insert at auto-offset places after last sibling ──
void testInsertAutoOffset() {
uint64_t rootId = m_doc->tree.nodes[0].id;
// Last child is "flags" at offset 12, size 4 → end = 16
m_ctrl->insertNode(rootId, -1, NodeKind::Hex64, "autoPlaced");
QApplication::processEvents();
int idx = findNode("autoPlaced");
QVERIFY(idx >= 0);
// Hex64 is 8-byte aligned, next aligned offset after 16 is 16
QCOMPARE(m_doc->tree.nodes[idx].offset, 16);
}
// ── Duplicate creates exactly one copy ──
void testDuplicateAddsOneNode() {
int flagsIdx = findNode("flags");
QVERIFY(flagsIdx >= 0);
int before = countNodes();
m_ctrl->duplicateNode(flagsIdx);
QApplication::processEvents();
QCOMPARE(countNodes(), before + 1);
int copyIdx = findNode("flags_copy");
QVERIFY2(copyIdx >= 0, "Expected a node named 'flags_copy'");
QCOMPARE(m_doc->tree.nodes[copyIdx].kind, NodeKind::Hex32);
QCOMPARE(m_doc->tree.nodes[copyIdx].offset, 16); // flags(12) + 4 = 16
}
// ── Duplicate preserves original node unchanged ──
void testDuplicatePreservesOriginal() {
int flagsIdx = findNode("flags");
QVERIFY(flagsIdx >= 0);
NodeKind origKind = m_doc->tree.nodes[flagsIdx].kind;
int origOffset = m_doc->tree.nodes[flagsIdx].offset;
QString origName = m_doc->tree.nodes[flagsIdx].name;
m_ctrl->duplicateNode(flagsIdx);
QApplication::processEvents();
// Original should be unchanged (re-find in case index shifted)
flagsIdx = findNode("flags");
QVERIFY(flagsIdx >= 0);
QCOMPARE(m_doc->tree.nodes[flagsIdx].kind, origKind);
QCOMPARE(m_doc->tree.nodes[flagsIdx].offset, origOffset);
QCOMPARE(m_doc->tree.nodes[flagsIdx].name, origName);
}
// ── Duplicate undo removes the copy ──
void testDuplicateUndo() {
int before = countNodes();
int flagsIdx = findNode("flags");
QVERIFY(flagsIdx >= 0);
m_ctrl->duplicateNode(flagsIdx);
QApplication::processEvents();
QCOMPARE(countNodes(), before + 1);
m_doc->undoStack.undo();
QApplication::processEvents();
QCOMPARE(countNodes(), before);
QCOMPARE(findNode("flags_copy"), -1);
}
// ── Duplicate on struct is no-op ──
void testDuplicateStructNoOp() {
int rootIdx = findNode("Player");
QVERIFY(rootIdx >= 0);
int before = countNodes();
m_ctrl->duplicateNode(rootIdx);
QApplication::processEvents();
QCOMPARE(countNodes(), before);
}
// ── Insert at root level (parentId=0) ──
void testInsertAtRootLevel() {
int before = countNodes();
m_ctrl->insertNode(0, -1, NodeKind::Hex64, "rootField");
QApplication::processEvents();
QCOMPARE(countNodes(), before + 1);
int idx = findNode("rootField");
QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].parentId, (uint64_t)0);
}
// ── Append 128 bytes adds exactly 16 Hex64 nodes ──
void testAppend128Bytes() {
int before = countNodes();
// Simulate what "Append 128 bytes" does
m_ctrl->document()->undoStack.beginMacro("Append 128 bytes");
for (int i = 0; i < 16; i++)
m_ctrl->insertNode(0, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(i));
m_ctrl->document()->undoStack.endMacro();
QApplication::processEvents();
QCOMPARE(countNodes(), before + 16);
// All should be root-level Hex64
int foundCount = 0;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
const auto& n = m_doc->tree.nodes[i];
if (n.name.startsWith("field_") && n.parentId == 0
&& n.kind == NodeKind::Hex64) {
foundCount++;
}
}
QCOMPARE(foundCount, 16);
}
// ── Append 128 bytes undo removes all 16 at once ──
void testAppend128BytesUndo() {
int before = countNodes();
m_ctrl->document()->undoStack.beginMacro("Append 128 bytes");
for (int i = 0; i < 16; i++)
m_ctrl->insertNode(0, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(i));
m_ctrl->document()->undoStack.endMacro();
QApplication::processEvents();
QCOMPARE(countNodes(), before + 16);
// Single undo undoes the entire macro
m_doc->undoStack.undo();
QApplication::processEvents();
QCOMPARE(countNodes(), before);
}
// ── Insert child into struct ──
void testInsertChildIntoStruct() {
uint64_t rootId = m_doc->tree.nodes[0].id;
int before = countNodes();
m_ctrl->insertNode(rootId, 0, NodeKind::Hex64, "childField");
QApplication::processEvents();
QCOMPARE(countNodes(), before + 1);
int idx = findNode("childField");
QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].parentId, rootId);
QCOMPARE(m_doc->tree.nodes[idx].offset, 0);
}
// ── Remove node then undo restores it ──
void testRemoveAndUndoNode() {
int flagsIdx = findNode("flags");
QVERIFY(flagsIdx >= 0);
int before = countNodes();
m_ctrl->removeNode(flagsIdx);
QApplication::processEvents();
QCOMPARE(countNodes(), before - 1);
QCOMPARE(findNode("flags"), -1);
m_doc->undoStack.undo();
QApplication::processEvents();
QCOMPARE(countNodes(), before);
QVERIFY(findNode("flags") >= 0);
}
// ── Multiple duplicates each add exactly one ──
void testMultipleDuplicates() {
int before = countNodes();
int healthIdx = findNode("health");
QVERIFY(healthIdx >= 0);
m_ctrl->duplicateNode(healthIdx);
QApplication::processEvents();
QCOMPARE(countNodes(), before + 1);
int copyIdx = findNode("health_copy");
QVERIFY(copyIdx >= 0);
m_ctrl->duplicateNode(copyIdx);
QApplication::processEvents();
QCOMPARE(countNodes(), before + 2);
int copy2Idx = findNode("health_copy_copy");
QVERIFY(copy2Idx >= 0);
}
// ── Duplicate copy has correct parent ──
void testDuplicateCopyParent() {
int healthIdx = findNode("health");
QVERIFY(healthIdx >= 0);
uint64_t parentId = m_doc->tree.nodes[healthIdx].parentId;
m_ctrl->duplicateNode(healthIdx);
QApplication::processEvents();
int copyIdx = findNode("health_copy");
QVERIFY(copyIdx >= 0);
QCOMPARE(m_doc->tree.nodes[copyIdx].parentId, parentId);
}
// ── Insert struct at root then add children ──
void testInsertStructAndChildren() {
int before = countNodes();
m_ctrl->insertNode(0, -1, NodeKind::Struct, "NewClass");
QApplication::processEvents();
QCOMPARE(countNodes(), before + 1);
int structIdx = findNode("NewClass");
QVERIFY(structIdx >= 0);
uint64_t structId = m_doc->tree.nodes[structIdx].id;
m_ctrl->insertNode(structId, 0, NodeKind::Int32, "x");
m_ctrl->insertNode(structId, -1, NodeKind::Int32, "y");
QApplication::processEvents();
QCOMPARE(countNodes(), before + 3);
int xIdx = findNode("x");
int yIdx = findNode("y");
QVERIFY(xIdx >= 0);
QVERIFY(yIdx >= 0);
QCOMPARE(m_doc->tree.nodes[xIdx].parentId, structId);
QCOMPARE(m_doc->tree.nodes[yIdx].parentId, structId);
}
// ── Batch remove deletes multiple nodes ──
void testBatchRemove() {
int healthIdx = findNode("health");
int armorIdx = findNode("armor");
QVERIFY(healthIdx >= 0);
QVERIFY(armorIdx >= 0);
int before = countNodes();
m_ctrl->batchRemoveNodes({healthIdx, armorIdx});
QApplication::processEvents();
QCOMPARE(countNodes(), before - 2);
QCOMPARE(findNode("health"), -1);
QCOMPARE(findNode("armor"), -1);
// Undo restores both
m_doc->undoStack.undo();
QApplication::processEvents();
QCOMPARE(countNodes(), before);
QVERIFY(findNode("health") >= 0);
QVERIFY(findNode("armor") >= 0);
}
// ── Insert with invalid parent still works (root-level) ──
void testInsertInvalidParent() {
int before = countNodes();
// parentId=999 doesn't exist, but insertNode doesn't validate parent
m_ctrl->insertNode(999, 0, NodeKind::Hex32, "orphan");
QApplication::processEvents();
QCOMPARE(countNodes(), before + 1);
}
// ── Duplicate out-of-range index is no-op ──
void testDuplicateInvalidIndex() {
int before = countNodes();
m_ctrl->duplicateNode(-1);
m_ctrl->duplicateNode(9999);
QApplication::processEvents();
QCOMPARE(countNodes(), before);
}
// ── Remove out-of-range index is no-op ──
void testRemoveInvalidIndex() {
int before = countNodes();
m_ctrl->removeNode(-1);
m_ctrl->removeNode(9999);
QApplication::processEvents();
QCOMPARE(countNodes(), before);
}
// ── Change to Ptr* creates class and sets refId ──
void testChangeToPtrStarCreatesClassAndSetsRef() {
// Add a Hex64 node to the root struct
uint64_t rootId = m_doc->tree.nodes[0].id;
m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "ptrField");
QApplication::processEvents();
int ptrIdx = findNode("ptrField");
QVERIFY(ptrIdx >= 0);
uint64_t ptrNodeId = m_doc->tree.nodes[ptrIdx].id;
int before = countNodes();
// Convert to typed pointer
m_ctrl->convertToTypedPointer(ptrNodeId);
QApplication::processEvents();
// Re-find after tree mutation
ptrIdx = -1;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
}
QVERIFY(ptrIdx >= 0);
// Verify: node kind changed to Pointer64
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Pointer64);
// Verify: node.refId != 0
uint64_t refId = m_doc->tree.nodes[ptrIdx].refId;
QVERIFY(refId != 0);
// Verify: a new Struct node exists with the refId as its id
int structIdx = m_doc->tree.indexOfId(refId);
QVERIFY(structIdx >= 0);
QCOMPARE(m_doc->tree.nodes[structIdx].kind, NodeKind::Struct);
// Verify: the new struct has children (Hex64 fields)
auto children = m_doc->tree.childrenOf(refId);
QVERIFY(children.size() == 16);
for (int ci : children)
QCOMPARE(m_doc->tree.nodes[ci].kind, NodeKind::Hex64);
// Verify: total nodes increased by 1 struct + 16 children = 17
QCOMPARE(countNodes(), before + 17);
// Verify: undo restores the original Hex64 kind and refId==0
m_doc->undoStack.undo();
QApplication::processEvents();
ptrIdx = -1;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
}
QVERIFY(ptrIdx >= 0);
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Hex64);
QCOMPARE(m_doc->tree.nodes[ptrIdx].refId, (uint64_t)0);
QCOMPARE(countNodes(), before);
}
};
QTEST_MAIN(TestContextMenu)
#include "test_context_menu.moc"