mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
460 lines
14 KiB
C++
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"
|