diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 3db096c..a4fef9f 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3964,4 +3964,13 @@ target_link_libraries(step564_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step565_test tests/step565_test.cpp) +target_include_directories(step565_test PRIVATE src) +target_link_libraries(step565_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/MemoryInspectorUiModel.h b/editor/src/MemoryInspectorUiModel.h new file mode 100644 index 0000000..f2e236e --- /dev/null +++ b/editor/src/MemoryInspectorUiModel.h @@ -0,0 +1,194 @@ +#pragma once +// Step 565: Memory Inspector UI Model + +#include "MemorySnapshotModel.h" + +#include +#include +#include + +enum class MemoryValueKind { + Integer, + Floating, + Boolean, + String, + Pointer, + Container, + Struct +}; + +struct MemoryValueNode { + std::string nodeId; + std::string label; + MemoryValueKind kind = MemoryValueKind::Integer; + std::string valueText; + bool expanded = false; + std::vector children; +}; + +struct MemoryInspectorRow { + std::string nodeId; + std::string label; + std::string typeName; + std::string valueText; + int depth = 0; + bool expandable = false; + bool expanded = false; +}; + +class MemoryInspectorUiModel { +public: + static bool createLeaf(const std::string& nodeId, + const std::string& label, + MemoryValueKind kind, + const std::string& valueText, + MemoryValueNode* outNode, + std::string* error) { + if (!outNode || !error) return false; + error->clear(); + if (nodeId.empty() || label.empty()) { + *error = "node_id_or_label_missing"; + return false; + } + if (isCompositeKind(kind)) { + *error = "leaf_kind_must_be_primitive"; + return false; + } + + MemoryValueNode node; + node.nodeId = nodeId; + node.label = label; + node.kind = kind; + node.valueText = valueText; + *outNode = node; + return true; + } + + static bool createComposite(const std::string& nodeId, + const std::string& label, + MemoryValueKind kind, + MemoryValueNode* outNode, + std::string* error) { + if (!outNode || !error) return false; + error->clear(); + if (nodeId.empty() || label.empty()) { + *error = "node_id_or_label_missing"; + return false; + } + if (!isCompositeKind(kind)) { + *error = "composite_kind_required"; + return false; + } + + MemoryValueNode node; + node.nodeId = nodeId; + node.label = label; + node.kind = kind; + node.expanded = true; + *outNode = node; + return true; + } + + static bool appendChild(MemoryValueNode* parent, + const MemoryValueNode& child, + std::string* error) { + if (!parent || !error) return false; + error->clear(); + if (!isCompositeKind(parent->kind)) { + *error = "parent_is_not_composite"; + return false; + } + if (child.nodeId.empty()) { + *error = "child_node_id_missing"; + return false; + } + parent->children.push_back(child); + return true; + } + + static std::vector collectVisibleRows(const MemoryValueNode& root, + std::size_t maxRows) { + std::vector rows; + if (maxRows == 0) return rows; + appendNodeRows(root, 0, maxRows, &rows); + return rows; + } + + static std::string typeNameForKind(MemoryValueKind kind) { + switch (kind) { + case MemoryValueKind::Integer: return "int"; + case MemoryValueKind::Floating: return "float"; + case MemoryValueKind::Boolean: return "bool"; + case MemoryValueKind::String: return "string"; + case MemoryValueKind::Pointer: return "pointer"; + case MemoryValueKind::Container: return "container"; + case MemoryValueKind::Struct: return "struct"; + } + return "unknown"; + } + + static bool resolvePointerRegion(const MemorySnapshot& snapshot, + const MemoryValueNode& pointerNode, + const MemoryRegionRecord** outRegion, + std::string* error) { + if (!outRegion || !error) return false; + *outRegion = nullptr; + error->clear(); + if (pointerNode.kind != MemoryValueKind::Pointer) { + *error = "node_is_not_pointer"; + return false; + } + + std::uint64_t address = 0; + if (!parseAddress(pointerNode.valueText, &address)) { + *error = "pointer_value_parse_failed"; + return false; + } + + *outRegion = MemorySnapshotModel::findRegionByAddress(snapshot, address); + return *outRegion != nullptr; + } + +private: + static bool isCompositeKind(MemoryValueKind kind) { + return kind == MemoryValueKind::Container || kind == MemoryValueKind::Struct; + } + + static bool parseAddress(const std::string& text, std::uint64_t* outAddress) { + if (!outAddress || text.empty()) return false; + try { + size_t parsed = 0; + int base = 10; + if (text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) base = 16; + const auto value = std::stoull(text, &parsed, base); + if (parsed != text.size() || value == 0) return false; + *outAddress = static_cast(value); + return true; + } catch (...) { + return false; + } + } + + static void appendNodeRows(const MemoryValueNode& node, + int depth, + std::size_t maxRows, + std::vector* rows) { + if (rows->size() >= maxRows) return; + + MemoryInspectorRow row; + row.nodeId = node.nodeId; + row.label = node.label; + row.typeName = typeNameForKind(node.kind); + row.valueText = node.valueText; + row.depth = depth; + row.expandable = isCompositeKind(node.kind); + row.expanded = node.expanded; + rows->push_back(row); + + if (!row.expandable || !node.expanded) return; + for (const auto& child : node.children) { + appendNodeRows(child, depth + 1, maxRows, rows); + if (rows->size() >= maxRows) return; + } + } +}; diff --git a/editor/tests/step565_test.cpp b/editor/tests/step565_test.cpp new file mode 100644 index 0000000..d55134b --- /dev/null +++ b/editor/tests/step565_test.cpp @@ -0,0 +1,188 @@ +// Step 565: Memory Inspector UI Model (12 tests) + +#include "MemoryInspectorUiModel.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +void test_create_leaf_success() { + TEST(create_leaf_success); + MemoryValueNode n; + std::string error; + const bool ok = MemoryInspectorUiModel::createLeaf("n1", "counter", MemoryValueKind::Integer, "42", &n, &error); + CHECK(ok, "create leaf should succeed"); + CHECK(n.nodeId == "n1", "id mismatch"); + CHECK(n.label == "counter", "label mismatch"); + CHECK(n.kind == MemoryValueKind::Integer, "kind mismatch"); + CHECK(n.valueText == "42", "value mismatch"); + PASS(); +} + +void test_create_leaf_rejects_composite_kind() { + TEST(create_leaf_rejects_composite_kind); + MemoryValueNode n; + std::string error; + const bool ok = MemoryInspectorUiModel::createLeaf("n1", "arr", MemoryValueKind::Container, "[]", &n, &error); + CHECK(!ok, "create leaf should fail for composite kind"); + CHECK(error == "leaf_kind_must_be_primitive", "wrong error"); + PASS(); +} + +void test_create_composite_success() { + TEST(create_composite_success); + MemoryValueNode n; + std::string error; + const bool ok = MemoryInspectorUiModel::createComposite("n2", "record", MemoryValueKind::Struct, &n, &error); + CHECK(ok, "create composite should succeed"); + CHECK(n.expanded, "composite defaults to expanded"); + CHECK(n.kind == MemoryValueKind::Struct, "kind mismatch"); + PASS(); +} + +void test_create_composite_rejects_primitive_kind() { + TEST(create_composite_rejects_primitive_kind); + MemoryValueNode n; + std::string error; + const bool ok = MemoryInspectorUiModel::createComposite("n2", "count", MemoryValueKind::Integer, &n, &error); + CHECK(!ok, "create composite should fail for primitive kind"); + CHECK(error == "composite_kind_required", "wrong error"); + PASS(); +} + +void test_append_child_success() { + TEST(append_child_success); + MemoryValueNode parent; + MemoryValueNode child; + std::string error; + CHECK(MemoryInspectorUiModel::createComposite("p", "root", MemoryValueKind::Struct, &parent, &error), "parent create failed"); + CHECK(MemoryInspectorUiModel::createLeaf("c", "x", MemoryValueKind::Integer, "3", &child, &error), "child create failed"); + CHECK(MemoryInspectorUiModel::appendChild(&parent, child, &error), "append child should succeed"); + CHECK(parent.children.size() == 1, "expected one child"); + CHECK(parent.children[0].nodeId == "c", "child id mismatch"); + PASS(); +} + +void test_append_child_rejects_non_composite_parent() { + TEST(append_child_rejects_non_composite_parent); + MemoryValueNode parent; + MemoryValueNode child; + std::string error; + CHECK(MemoryInspectorUiModel::createLeaf("p", "value", MemoryValueKind::Integer, "1", &parent, &error), "parent create failed"); + CHECK(MemoryInspectorUiModel::createLeaf("c", "x", MemoryValueKind::Integer, "3", &child, &error), "child create failed"); + CHECK(!MemoryInspectorUiModel::appendChild(&parent, child, &error), "append should fail"); + CHECK(error == "parent_is_not_composite", "wrong error"); + PASS(); +} + +void test_collect_visible_rows_honors_expanded_state() { + TEST(collect_visible_rows_honors_expanded_state); + MemoryValueNode root; + MemoryValueNode child; + std::string error; + CHECK(MemoryInspectorUiModel::createComposite("root", "Root", MemoryValueKind::Struct, &root, &error), "root create failed"); + CHECK(MemoryInspectorUiModel::createLeaf("child", "counter", MemoryValueKind::Integer, "9", &child, &error), "child create failed"); + CHECK(MemoryInspectorUiModel::appendChild(&root, child, &error), "append failed"); + root.expanded = false; + + const auto rows = MemoryInspectorUiModel::collectVisibleRows(root, 10); + CHECK(rows.size() == 1, "collapsed root should hide children"); + CHECK(rows[0].nodeId == "root", "root row expected"); + PASS(); +} + +void test_collect_visible_rows_depth_and_limit() { + TEST(collect_visible_rows_depth_and_limit); + MemoryValueNode root; + MemoryValueNode childA; + MemoryValueNode childB; + std::string error; + CHECK(MemoryInspectorUiModel::createComposite("root", "Root", MemoryValueKind::Container, &root, &error), "root create failed"); + CHECK(MemoryInspectorUiModel::createLeaf("a", "a", MemoryValueKind::Integer, "1", &childA, &error), "a create failed"); + CHECK(MemoryInspectorUiModel::createLeaf("b", "b", MemoryValueKind::Integer, "2", &childB, &error), "b create failed"); + CHECK(MemoryInspectorUiModel::appendChild(&root, childA, &error), "append a failed"); + CHECK(MemoryInspectorUiModel::appendChild(&root, childB, &error), "append b failed"); + + const auto limitedRows = MemoryInspectorUiModel::collectVisibleRows(root, 2); + CHECK(limitedRows.size() == 2, "row limit should apply"); + CHECK(limitedRows[1].depth == 1, "child row depth should be one"); + PASS(); +} + +void test_type_name_mapping_includes_pointer_and_struct() { + TEST(type_name_mapping_includes_pointer_and_struct); + CHECK(MemoryInspectorUiModel::typeNameForKind(MemoryValueKind::Pointer) == "pointer", "pointer type mismatch"); + CHECK(MemoryInspectorUiModel::typeNameForKind(MemoryValueKind::Struct) == "struct", "struct type mismatch"); + PASS(); +} + +void test_resolve_pointer_region_success() { + TEST(resolve_pointer_region_success); + MemorySnapshot snapshot; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap", "sess", "thread", 1, &snapshot, &error), "snapshot create failed"); + CHECK(MemorySnapshotModel::addRegion(&snapshot, {"heap-1", MemoryRegionKind::Heap, 0x1000, 64, false}, &error), "region add failed"); + + MemoryValueNode pointerNode; + CHECK(MemoryInspectorUiModel::createLeaf("ptr", "next", MemoryValueKind::Pointer, "0x1010", &pointerNode, &error), "pointer node create failed"); + + const MemoryRegionRecord* resolved = nullptr; + const bool ok = MemoryInspectorUiModel::resolvePointerRegion(snapshot, pointerNode, &resolved, &error); + CHECK(ok, "pointer region resolve should succeed"); + CHECK(resolved != nullptr && resolved->id == "heap-1", "resolved region mismatch"); + PASS(); +} + +void test_resolve_pointer_region_rejects_non_pointer_node() { + TEST(resolve_pointer_region_rejects_non_pointer_node); + MemorySnapshot snapshot; + MemoryValueNode valueNode; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap", "sess", "thread", 1, &snapshot, &error), "snapshot create failed"); + CHECK(MemoryInspectorUiModel::createLeaf("v", "x", MemoryValueKind::Integer, "7", &valueNode, &error), "value node create failed"); + + const MemoryRegionRecord* resolved = nullptr; + const bool ok = MemoryInspectorUiModel::resolvePointerRegion(snapshot, valueNode, &resolved, &error); + CHECK(!ok, "resolve should fail for non-pointer"); + CHECK(error == "node_is_not_pointer", "wrong error"); + PASS(); +} + +void test_resolve_pointer_region_rejects_bad_pointer_text() { + TEST(resolve_pointer_region_rejects_bad_pointer_text); + MemorySnapshot snapshot; + MemoryValueNode pointerNode; + std::string error; + CHECK(MemorySnapshotModel::createSnapshot("snap", "sess", "thread", 1, &snapshot, &error), "snapshot create failed"); + CHECK(MemoryInspectorUiModel::createLeaf("ptr", "next", MemoryValueKind::Pointer, "not-an-address", &pointerNode, &error), "pointer node create failed"); + + const MemoryRegionRecord* resolved = nullptr; + const bool ok = MemoryInspectorUiModel::resolvePointerRegion(snapshot, pointerNode, &resolved, &error); + CHECK(!ok, "resolve should fail for parse error"); + CHECK(error == "pointer_value_parse_failed", "wrong error"); + PASS(); +} + +int main() { + std::cout << "Step 565: Memory Inspector UI Model\n"; + + test_create_leaf_success(); // 1 + test_create_leaf_rejects_composite_kind(); // 2 + test_create_composite_success(); // 3 + test_create_composite_rejects_primitive_kind(); // 4 + test_append_child_success(); // 5 + test_append_child_rejects_non_composite_parent(); // 6 + test_collect_visible_rows_honors_expanded_state(); // 7 + test_collect_visible_rows_depth_and_limit(); // 8 + test_type_name_mapping_includes_pointer_and_struct(); // 9 + test_resolve_pointer_region_success(); // 10 + test_resolve_pointer_region_rejects_non_pointer_node();// 11 + test_resolve_pointer_region_rejects_bad_pointer_text();// 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 1f4cd37..45781e8 100644 --- a/progress.md +++ b/progress.md @@ -10190,3 +10190,37 @@ validation, and query helpers for stack/heap/global memory reflection. - `editor/src/MemorySnapshotModel.h` within header-size limit (`149` <= `600`) - `editor/tests/step564_test.cpp` within test-file size guidance (`179` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 565: Memory Inspector UI Model +**Status:** PASS (12/12 tests) + +Implements a memory inspector UI data model with typed primitive/composite nodes, +visible-row flattening, and pointer-to-region correlation against memory snapshots. + +**Files added:** +- `editor/src/MemoryInspectorUiModel.h` - memory inspector UI model: + - typed value-node schema (primitive + container/struct) + - primitive/composite creation guards + - composite child-attachment behavior + - visible row flattening with expansion + row limit handling + - pointer text parsing and region-resolution against `MemorySnapshot` +- `editor/tests/step565_test.cpp` - 12 tests covering: + - primitive/composite creation success/failure behavior + - child attachment behavior and parent-kind guards + - visible-row expansion/limit/depth behavior + - type renderer naming behavior + - pointer-region resolve success and failure behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step565_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step565_test step564_test` - PASS +- `./editor/build-native/step565_test` - PASS (12/12) +- `./editor/build-native/step564_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/MemoryInspectorUiModel.h` within header-size limit (`194` <= `600`) +- `editor/tests/step565_test.cpp` within test-file size guidance (`188` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`