Step 557: add exception and stack trace capture model
This commit is contained in:
@@ -3892,4 +3892,13 @@ target_link_libraries(step556_test PRIVATE
|
|||||||
tree_sitter_javascript tree_sitter_typescript
|
tree_sitter_javascript tree_sitter_typescript
|
||||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
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)
|
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||||
|
|||||||
71
editor/src/ExceptionStackTraceCapture.h
Normal file
71
editor/src/ExceptionStackTraceCapture.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
// Step 557: Exception + Stack Trace Capture
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<StackFrame> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ExceptionCaptureResult {
|
||||||
|
bool valid = false;
|
||||||
|
ExceptionEvent event;
|
||||||
|
std::vector<std::string> errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExceptionStackTraceCapture {
|
||||||
|
public:
|
||||||
|
static ExceptionCaptureResult capture(const std::string& type,
|
||||||
|
const std::string& message,
|
||||||
|
const std::string& threadId,
|
||||||
|
const std::vector<StackFrame>& 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<StackFrame> normalizeFrames(const std::vector<StackFrame>& frames,
|
||||||
|
std::vector<std::string>& errors) {
|
||||||
|
std::vector<StackFrame> 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
149
editor/tests/step557_test.cpp
Normal file
149
editor/tests/step557_test.cpp
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// Step 557: Exception + Stack Trace Capture (12 tests)
|
||||||
|
|
||||||
|
#include "ExceptionStackTraceCapture.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 {}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
34
progress.md
34
progress.md
@@ -9871,3 +9871,37 @@ state transitions.
|
|||||||
- `editor/src/StepEngine.h` within header-size limit (`135` <= `600`)
|
- `editor/src/StepEngine.h` within header-size limit (`135` <= `600`)
|
||||||
- `editor/tests/step556_test.cpp` within test-file size guidance (`154` lines)
|
- `editor/tests/step556_test.cpp` within test-file size guidance (`154` lines)
|
||||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
- 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`
|
||||||
|
|||||||
Reference in New Issue
Block a user