From ac78cff071b729b5122fd2672065ae5d5b8002df Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 10:32:34 -0700 Subject: [PATCH] Step 557: add exception and stack trace capture model --- editor/CMakeLists.txt | 9 ++ editor/src/ExceptionStackTraceCapture.h | 71 +++++++++++ editor/tests/step557_test.cpp | 149 ++++++++++++++++++++++++ progress.md | 34 ++++++ 4 files changed, 263 insertions(+) create mode 100644 editor/src/ExceptionStackTraceCapture.h create mode 100644 editor/tests/step557_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 857d41d..c760c84 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3892,4 +3892,13 @@ target_link_libraries(step556_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step557_test tests/step557_test.cpp) +target_include_directories(step557_test PRIVATE src) +target_link_libraries(step557_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/ExceptionStackTraceCapture.h b/editor/src/ExceptionStackTraceCapture.h new file mode 100644 index 0000000..5032983 --- /dev/null +++ b/editor/src/ExceptionStackTraceCapture.h @@ -0,0 +1,71 @@ +#pragma once +// Step 557: Exception + Stack Trace Capture + +#include +#include + +struct StackFrame { + std::string functionName; + std::string filePath; + int line = 0; + std::string navigationAnchor; +}; + +struct ExceptionEvent { + std::string type; + std::string message; + std::string threadId; + std::vector frames; +}; + +struct ExceptionCaptureResult { + bool valid = false; + ExceptionEvent event; + std::vector errors; +}; + +class ExceptionStackTraceCapture { +public: + static ExceptionCaptureResult capture(const std::string& type, + const std::string& message, + const std::string& threadId, + const std::vector& rawFrames) { + ExceptionCaptureResult out; + + if (type.empty()) out.errors.push_back("missing_type"); + if (message.empty()) out.errors.push_back("missing_message"); + if (threadId.empty()) out.errors.push_back("missing_thread_id"); + + out.event.type = type; + out.event.message = message; + out.event.threadId = threadId; + out.event.frames = normalizeFrames(rawFrames, out.errors); + + if (out.event.frames.empty()) out.errors.push_back("missing_stack_frames"); + out.valid = out.errors.empty(); + return out; + } + +private: + static std::vector normalizeFrames(const std::vector& frames, + std::vector& errors) { + std::vector out; + int index = 0; + for (const auto& f : frames) { + if (f.functionName.empty() || f.filePath.empty() || f.line <= 0) { + errors.push_back("invalid_frame:" + std::to_string(index)); + ++index; + continue; + } + StackFrame normalized = f; + normalized.navigationAnchor = makeAnchor(f.filePath, f.line); + out.push_back(normalized); + ++index; + } + return out; + } + + static std::string makeAnchor(const std::string& filePath, int line) { + return filePath + ":" + std::to_string(line); + } +}; diff --git a/editor/tests/step557_test.cpp b/editor/tests/step557_test.cpp new file mode 100644 index 0000000..bb00a00 --- /dev/null +++ b/editor/tests/step557_test.cpp @@ -0,0 +1,149 @@ +// Step 557: Exception + Stack Trace Capture (12 tests) + +#include "ExceptionStackTraceCapture.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 {} + +static bool hasError(const ExceptionCaptureResult& r, const std::string& code) { + for (const auto& e : r.errors) { + if (e == code || e.find(code) == 0) return true; + } + return false; +} + +void test_valid_exception_capture_passes() { + TEST(valid_exception_capture_passes); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "boom", "thread-1", + { {"foo", "main.cpp", 12, ""}, {"bar", "lib.cpp", 44, ""} }); + CHECK(r.valid, "valid exception should pass"); + CHECK(r.event.frames.size() == 2, "two frames expected"); + PASS(); +} + +void test_missing_type_rejected() { + TEST(missing_type_rejected); + auto r = ExceptionStackTraceCapture::capture( + "", "boom", "thread-1", {{"foo", "main.cpp", 12, ""}}); + CHECK(!r.valid, "missing type should fail"); + CHECK(hasError(r, "missing_type"), "missing_type expected"); + PASS(); +} + +void test_missing_message_rejected() { + TEST(missing_message_rejected); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "", "thread-1", {{"foo", "main.cpp", 12, ""}}); + CHECK(!r.valid, "missing message should fail"); + CHECK(hasError(r, "missing_message"), "missing_message expected"); + PASS(); +} + +void test_missing_thread_id_rejected() { + TEST(missing_thread_id_rejected); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "boom", "", {{"foo", "main.cpp", 12, ""}}); + CHECK(!r.valid, "missing thread id should fail"); + CHECK(hasError(r, "missing_thread_id"), "missing_thread_id expected"); + PASS(); +} + +void test_missing_frames_rejected() { + TEST(missing_frames_rejected); + auto r = ExceptionStackTraceCapture::capture("RuntimeError", "boom", "thread-1", {}); + CHECK(!r.valid, "missing frames should fail"); + CHECK(hasError(r, "missing_stack_frames"), "missing_stack_frames expected"); + PASS(); +} + +void test_invalid_frame_function_rejected() { + TEST(invalid_frame_function_rejected); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "boom", "thread-1", {{"", "main.cpp", 12, ""}}); + CHECK(!r.valid, "invalid frame should fail"); + CHECK(hasError(r, "invalid_frame:0"), "invalid_frame expected"); + PASS(); +} + +void test_invalid_frame_file_rejected() { + TEST(invalid_frame_file_rejected); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "boom", "thread-1", {{"foo", "", 12, ""}}); + CHECK(!r.valid, "invalid frame should fail"); + CHECK(hasError(r, "invalid_frame:0"), "invalid_frame expected"); + PASS(); +} + +void test_invalid_frame_line_rejected() { + TEST(invalid_frame_line_rejected); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "boom", "thread-1", {{"foo", "main.cpp", 0, ""}}); + CHECK(!r.valid, "invalid frame should fail"); + CHECK(hasError(r, "invalid_frame:0"), "invalid_frame expected"); + PASS(); +} + +void test_navigation_anchor_is_generated() { + TEST(navigation_anchor_is_generated); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "boom", "thread-1", {{"foo", "main.cpp", 12, ""}}); + CHECK(r.valid, "capture should pass"); + CHECK(r.event.frames[0].navigationAnchor == "main.cpp:12", "anchor mismatch"); + PASS(); +} + +void test_multiple_invalid_frames_reported() { + TEST(multiple_invalid_frames_reported); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "boom", "thread-1", + {{"", "main.cpp", 10, ""}, {"ok", "ok.cpp", 2, ""}, {"bad", "", 2, ""}}); + CHECK(!r.valid, "invalid frames should fail"); + CHECK(hasError(r, "invalid_frame:0"), "first invalid frame expected"); + CHECK(hasError(r, "invalid_frame:2"), "third invalid frame expected"); + CHECK(r.event.frames.size() == 1, "only one valid frame expected"); + PASS(); +} + +void test_event_fields_persist() { + TEST(event_fields_persist); + auto r = ExceptionStackTraceCapture::capture( + "TypeError", "bad arg", "thread-9", {{"foo", "main.cpp", 12, ""}}); + CHECK(r.event.type == "TypeError", "type mismatch"); + CHECK(r.event.message == "bad arg", "message mismatch"); + CHECK(r.event.threadId == "thread-9", "thread id mismatch"); + PASS(); +} + +void test_single_valid_frame_is_sufficient() { + TEST(single_valid_frame_is_sufficient); + auto r = ExceptionStackTraceCapture::capture( + "RuntimeError", "boom", "thread-1", {{"foo", "main.cpp", 1, ""}}); + CHECK(r.valid, "single valid frame should pass"); + PASS(); +} + +int main() { + std::cout << "Step 557: Exception + Stack Trace Capture\n"; + + test_valid_exception_capture_passes(); // 1 + test_missing_type_rejected(); // 2 + test_missing_message_rejected(); // 3 + test_missing_thread_id_rejected(); // 4 + test_missing_frames_rejected(); // 5 + test_invalid_frame_function_rejected(); // 6 + test_invalid_frame_file_rejected(); // 7 + test_invalid_frame_line_rejected(); // 8 + test_navigation_anchor_is_generated(); // 9 + test_multiple_invalid_frames_reported(); // 10 + test_event_fields_persist(); // 11 + test_single_valid_frame_is_sufficient(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index c897f9f..97d8857 100644 --- a/progress.md +++ b/progress.md @@ -9871,3 +9871,37 @@ state transitions. - `editor/src/StepEngine.h` within header-size limit (`135` <= `600`) - `editor/tests/step556_test.cpp` within test-file size guidance (`154` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 557: Exception + Stack Trace Capture +**Status:** PASS (12/12 tests) + +Implements structured exception event capture with validated stack traces and +editor navigation anchors for each stack frame. + +**Files added:** +- `editor/src/ExceptionStackTraceCapture.h` - exception capture module: + - structured exception event model (`type/message/thread/frames`) + - frame validation and normalization + - navigation anchor generation (`file:line`) for editor linking + - explicit error reporting for invalid/missing payload fields +- `editor/tests/step557_test.cpp` - 12 tests covering: + - valid capture behavior + - missing required field behavior + - invalid frame validation behavior + - navigation anchor generation + - mixed valid/invalid frame behavior + - event field persistence behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step557_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step557_test step556_test` - PASS +- `./editor/build-native/step557_test` - PASS (12/12) +- `./editor/build-native/step556_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ExceptionStackTraceCapture.h` within header-size limit (`71` <= `600`) +- `editor/tests/step557_test.cpp` within test-file size guidance (`149` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`