diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index e1132e8..0c34f5a 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3928,4 +3928,13 @@ target_link_libraries(step560_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step561_test tests/step561_test.cpp) +target_include_directories(step561_test PRIVATE src) +target_link_libraries(step561_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/TraceTimelineModel.h b/editor/src/TraceTimelineModel.h new file mode 100644 index 0000000..d74ac58 --- /dev/null +++ b/editor/src/TraceTimelineModel.h @@ -0,0 +1,58 @@ +#pragma once +// Step 561: Trace Timeline Model + +#include +#include +#include + +struct TraceEvent { + long long ts = 0; + std::string type; // pause/step/continue/exception/resume/stop + std::string sessionId; + std::string detail; +}; + +class TraceTimelineModel { +public: + bool append(const TraceEvent& event) { + if (!isValid(event)) return false; + events_.push_back(event); + std::sort(events_.begin(), events_.end(), + [](const TraceEvent& a, const TraceEvent& b) { + if (a.ts != b.ts) return a.ts < b.ts; + return a.type < b.type; + }); + return true; + } + + std::vector all() const { return events_; } + + std::vector byType(const std::string& type) const { + std::vector out; + for (const auto& e : events_) if (e.type == type) out.push_back(e); + return out; + } + + std::vector bySession(const std::string& sessionId) const { + std::vector out; + for (const auto& e : events_) if (e.sessionId == sessionId) out.push_back(e); + return out; + } + + std::vector window(long long startTs, long long endTs) const { + std::vector out; + for (const auto& e : events_) { + if (e.ts >= startTs && e.ts <= endTs) out.push_back(e); + } + return out; + } + +private: + std::vector events_; + + static bool isValid(const TraceEvent& event) { + if (event.ts < 0) return false; + if (event.type.empty() || event.sessionId.empty()) return false; + return true; + } +}; diff --git a/editor/tests/step561_test.cpp b/editor/tests/step561_test.cpp new file mode 100644 index 0000000..5276c0e --- /dev/null +++ b/editor/tests/step561_test.cpp @@ -0,0 +1,144 @@ +// Step 561: Trace Timeline Model (12 tests) + +#include "TraceTimelineModel.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_append_valid_event() { + TEST(append_valid_event); + TraceTimelineModel m; + CHECK(m.append({10, "pause", "s1", "hit bp"}), "append should succeed"); + CHECK(m.all().size() == 1, "one event expected"); + PASS(); +} + +void test_reject_event_with_negative_timestamp() { + TEST(reject_event_with_negative_timestamp); + TraceTimelineModel m; + CHECK(!m.append({-1, "pause", "s1", "bad"}), "negative ts should fail"); + PASS(); +} + +void test_reject_event_with_empty_type() { + TEST(reject_event_with_empty_type); + TraceTimelineModel m; + CHECK(!m.append({1, "", "s1", "bad"}), "empty type should fail"); + PASS(); +} + +void test_reject_event_with_empty_session() { + TEST(reject_event_with_empty_session); + TraceTimelineModel m; + CHECK(!m.append({1, "pause", "", "bad"}), "empty session should fail"); + PASS(); +} + +void test_events_are_sorted_by_timestamp() { + TEST(events_are_sorted_by_timestamp); + TraceTimelineModel m; + CHECK(m.append({30, "step", "s1", "c"}), "append 30"); + CHECK(m.append({10, "pause", "s1", "a"}), "append 10"); + CHECK(m.append({20, "continue", "s1", "b"}), "append 20"); + auto all = m.all(); + CHECK(all[0].ts == 10 && all[1].ts == 20 && all[2].ts == 30, "timestamp ordering mismatch"); + PASS(); +} + +void test_query_by_type_filters_events() { + TEST(query_by_type_filters_events); + TraceTimelineModel m; + m.append({10, "pause", "s1", "a"}); + m.append({20, "step", "s1", "b"}); + m.append({30, "pause", "s1", "c"}); + auto pauses = m.byType("pause"); + CHECK(pauses.size() == 2, "two pause events expected"); + PASS(); +} + +void test_query_by_session_filters_events() { + TEST(query_by_session_filters_events); + TraceTimelineModel m; + m.append({10, "pause", "s1", "a"}); + m.append({20, "step", "s2", "b"}); + m.append({30, "pause", "s1", "c"}); + auto s1 = m.bySession("s1"); + CHECK(s1.size() == 2, "two s1 events expected"); + PASS(); +} + +void test_window_query_inclusive_bounds() { + TEST(window_query_inclusive_bounds); + TraceTimelineModel m; + m.append({10, "pause", "s1", "a"}); + m.append({20, "step", "s1", "b"}); + m.append({30, "pause", "s1", "c"}); + auto w = m.window(10, 20); + CHECK(w.size() == 2, "window should include bounds"); + PASS(); +} + +void test_window_query_empty_when_no_matches() { + TEST(window_query_empty_when_no_matches); + TraceTimelineModel m; + m.append({10, "pause", "s1", "a"}); + auto w = m.window(100, 200); + CHECK(w.empty(), "no matches expected"); + PASS(); +} + +void test_same_timestamp_orders_by_type_name() { + TEST(same_timestamp_orders_by_type_name); + TraceTimelineModel m; + m.append({10, "step", "s1", "b"}); + m.append({10, "pause", "s1", "a"}); + auto all = m.all(); + CHECK(all[0].type == "pause" && all[1].type == "step", "type tiebreak ordering mismatch"); + PASS(); +} + +void test_detail_payload_preserved() { + TEST(detail_payload_preserved); + TraceTimelineModel m; + m.append({10, "exception", "s1", "RuntimeError: boom"}); + auto all = m.all(); + CHECK(all[0].detail == "RuntimeError: boom", "detail payload mismatch"); + PASS(); +} + +void test_mixed_event_types_supported() { + TEST(mixed_event_types_supported); + TraceTimelineModel m; + m.append({10, "pause", "s1", "a"}); + m.append({15, "step", "s1", "b"}); + m.append({20, "exception", "s1", "c"}); + m.append({25, "resume", "s1", "d"}); + m.append({30, "stop", "s1", "e"}); + CHECK(m.all().size() == 5, "five mixed events expected"); + PASS(); +} + +int main() { + std::cout << "Step 561: Trace Timeline Model\n"; + + test_append_valid_event(); // 1 + test_reject_event_with_negative_timestamp();// 2 + test_reject_event_with_empty_type(); // 3 + test_reject_event_with_empty_session(); // 4 + test_events_are_sorted_by_timestamp(); // 5 + test_query_by_type_filters_events(); // 6 + test_query_by_session_filters_events(); // 7 + test_window_query_inclusive_bounds(); // 8 + test_window_query_empty_when_no_matches(); // 9 + test_same_timestamp_orders_by_type_name(); // 10 + test_detail_payload_preserved(); // 11 + test_mixed_event_types_supported(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 2465dc4..e7a6514 100644 --- a/progress.md +++ b/progress.md @@ -10010,3 +10010,37 @@ locals display fields and watch lifecycle management. - `editor/src/LocalsWatchesPanelModel.h` within header-size limit (`90` <= `600`) - `editor/tests/step560_test.cpp` within test-file size guidance (`138` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 561: Trace Timeline Model +**Status:** PASS (12/12 tests) + +Implements a runtime trace timeline data model for ordered pause/step/exception +state transitions with session/type/window query support. + +**Files added:** +- `editor/src/TraceTimelineModel.h` - trace timeline module: + - event append with payload validation + - deterministic timeline ordering + - query helpers by event type, session, and time window + - support for mixed runtime event categories +- `editor/tests/step561_test.cpp` - 12 tests covering: + - event validation behavior + - ordering behavior + - type/session/window query behavior + - timestamp tie-break behavior + - detail payload preservation + - mixed-event support behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step561_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step561_test step560_test` - PASS +- `./editor/build-native/step561_test` - PASS (12/12) +- `./editor/build-native/step560_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/TraceTimelineModel.h` within header-size limit (`58` <= `600`) +- `editor/tests/step561_test.cpp` within test-file size guidance (`144` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`