Step 565: add memory inspector UI model
This commit is contained in:
@@ -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)
|
||||
|
||||
194
editor/src/MemoryInspectorUiModel.h
Normal file
194
editor/src/MemoryInspectorUiModel.h
Normal file
@@ -0,0 +1,194 @@
|
||||
#pragma once
|
||||
// Step 565: Memory Inspector UI Model
|
||||
|
||||
#include "MemorySnapshotModel.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<MemoryValueNode> 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<MemoryInspectorRow> collectVisibleRows(const MemoryValueNode& root,
|
||||
std::size_t maxRows) {
|
||||
std::vector<MemoryInspectorRow> 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<std::uint64_t>(value);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void appendNodeRows(const MemoryValueNode& node,
|
||||
int depth,
|
||||
std::size_t maxRows,
|
||||
std::vector<MemoryInspectorRow>* 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
188
editor/tests/step565_test.cpp
Normal file
188
editor/tests/step565_test.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
// Step 565: Memory Inspector UI Model (12 tests)
|
||||
|
||||
#include "MemoryInspectorUiModel.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
34
progress.md
34
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`
|
||||
|
||||
Reference in New Issue
Block a user