Step 571: add time travel debug event buffer
This commit is contained in:
@@ -4018,4 +4018,13 @@ target_link_libraries(step570_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step571_test tests/step571_test.cpp)
|
||||
target_include_directories(step571_test PRIVATE src)
|
||||
target_link_libraries(step571_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)
|
||||
|
||||
86
editor/src/TimeTravelDebugEventBuffer.h
Normal file
86
editor/src/TimeTravelDebugEventBuffer.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
// Step 571: Time-Travel Debug Event Buffer
|
||||
|
||||
#include "DebugValidationUtil.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct DebugReplayEvent {
|
||||
std::string eventId;
|
||||
std::string sessionId;
|
||||
std::string type;
|
||||
std::uint64_t instructionPointer = 0;
|
||||
std::uint64_t timestamp = 0;
|
||||
std::string payload;
|
||||
};
|
||||
|
||||
class TimeTravelDebugEventBuffer {
|
||||
public:
|
||||
explicit TimeTravelDebugEventBuffer(std::size_t capacity = 128)
|
||||
: capacity_(capacity == 0 ? 1 : capacity) {}
|
||||
|
||||
bool append(const DebugReplayEvent& event, std::string* error) {
|
||||
if (!error) return false;
|
||||
error->clear();
|
||||
if (!hasRequiredDebugIds(event.eventId, event.sessionId)) return fail(error, "event_or_session_missing");
|
||||
if (event.type.empty()) return fail(error, "event_type_missing");
|
||||
if (event.timestamp == 0) return fail(error, "event_timestamp_invalid");
|
||||
if (event.instructionPointer == 0) return fail(error, "event_ip_invalid");
|
||||
if (containsId(event.eventId)) return fail(error, "event_duplicate");
|
||||
|
||||
events_.push_back(event);
|
||||
while (events_.size() > capacity_) {
|
||||
events_.pop_front();
|
||||
++dropped_;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t size() const { return events_.size(); }
|
||||
std::size_t capacity() const { return capacity_; }
|
||||
std::size_t droppedCount() const { return dropped_; }
|
||||
|
||||
std::vector<DebugReplayEvent> latest(std::size_t count) const {
|
||||
std::vector<DebugReplayEvent> out;
|
||||
if (count == 0 || events_.empty()) return out;
|
||||
const std::size_t begin = events_.size() > count ? events_.size() - count : 0;
|
||||
for (std::size_t i = begin; i < events_.size(); ++i) out.push_back(events_[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<DebugReplayEvent> forSession(const std::string& sessionId) const {
|
||||
std::vector<DebugReplayEvent> out;
|
||||
for (const auto& event : events_) {
|
||||
if (sessionId.empty() || event.sessionId == sessionId) out.push_back(event);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<DebugReplayEvent> replayWindow(std::size_t startIndex, std::size_t count) const {
|
||||
std::vector<DebugReplayEvent> out;
|
||||
if (count == 0 || startIndex >= events_.size()) return out;
|
||||
const std::size_t end = (startIndex + count) > events_.size() ? events_.size() : (startIndex + count);
|
||||
for (std::size_t i = startIndex; i < end; ++i) out.push_back(events_[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t capacity_ = 128;
|
||||
std::size_t dropped_ = 0;
|
||||
std::deque<DebugReplayEvent> events_;
|
||||
|
||||
static bool fail(std::string* error, const char* code) {
|
||||
*error = code;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool containsId(const std::string& eventId) const {
|
||||
for (const auto& event : events_) {
|
||||
if (event.eventId == eventId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
174
editor/tests/step571_test.cpp
Normal file
174
editor/tests/step571_test.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
// Step 571: Time-Travel Debug Event Buffer (12 tests)
|
||||
|
||||
#include "TimeTravelDebugEventBuffer.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 DebugReplayEvent event(const std::string& id,
|
||||
const std::string& session,
|
||||
const std::string& type,
|
||||
std::uint64_t ip,
|
||||
std::uint64_t ts) {
|
||||
DebugReplayEvent e;
|
||||
e.eventId = id;
|
||||
e.sessionId = session;
|
||||
e.type = type;
|
||||
e.instructionPointer = ip;
|
||||
e.timestamp = ts;
|
||||
e.payload = "payload-" + id;
|
||||
return e;
|
||||
}
|
||||
|
||||
void test_append_success() {
|
||||
TEST(append_success);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append should succeed");
|
||||
CHECK(buffer.size() == 1, "size mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_append_rejects_missing_ids() {
|
||||
TEST(append_rejects_missing_ids);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(!buffer.append(event("", "sess-1", "step", 0x1000, 10), &error), "append should fail");
|
||||
CHECK(error == "event_or_session_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_append_rejects_missing_type() {
|
||||
TEST(append_rejects_missing_type);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(!buffer.append(event("e1", "sess-1", "", 0x1000, 10), &error), "append should fail");
|
||||
CHECK(error == "event_type_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_append_rejects_invalid_timestamp() {
|
||||
TEST(append_rejects_invalid_timestamp);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(!buffer.append(event("e1", "sess-1", "step", 0x1000, 0), &error), "append should fail");
|
||||
CHECK(error == "event_timestamp_invalid", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_append_rejects_invalid_ip() {
|
||||
TEST(append_rejects_invalid_ip);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(!buffer.append(event("e1", "sess-1", "step", 0, 10), &error), "append should fail");
|
||||
CHECK(error == "event_ip_invalid", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_append_rejects_duplicate_event_id() {
|
||||
TEST(append_rejects_duplicate_event_id);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "first append failed");
|
||||
CHECK(!buffer.append(event("e1", "sess-1", "step", 0x1001, 11), &error), "duplicate append should fail");
|
||||
CHECK(error == "event_duplicate", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_capacity_eviction_tracks_dropped_count() {
|
||||
TEST(capacity_eviction_tracks_dropped_count);
|
||||
TimeTravelDebugEventBuffer buffer(2);
|
||||
std::string error;
|
||||
CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed");
|
||||
CHECK(buffer.append(event("e2", "sess-1", "step", 0x1001, 11), &error), "append e2 failed");
|
||||
CHECK(buffer.append(event("e3", "sess-1", "step", 0x1002, 12), &error), "append e3 failed");
|
||||
CHECK(buffer.size() == 2, "size should cap at capacity");
|
||||
CHECK(buffer.droppedCount() == 1, "dropped count mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_latest_returns_most_recent_events() {
|
||||
TEST(latest_returns_most_recent_events);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed");
|
||||
CHECK(buffer.append(event("e2", "sess-1", "step", 0x1001, 11), &error), "append e2 failed");
|
||||
CHECK(buffer.append(event("e3", "sess-1", "step", 0x1002, 12), &error), "append e3 failed");
|
||||
const auto latest = buffer.latest(2);
|
||||
CHECK(latest.size() == 2, "latest count mismatch");
|
||||
CHECK(latest[0].eventId == "e2", "latest[0] mismatch");
|
||||
CHECK(latest[1].eventId == "e3", "latest[1] mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_for_session_filters_events() {
|
||||
TEST(for_session_filters_events);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed");
|
||||
CHECK(buffer.append(event("e2", "sess-2", "step", 0x1001, 11), &error), "append e2 failed");
|
||||
const auto filtered = buffer.forSession("sess-2");
|
||||
CHECK(filtered.size() == 1, "filter count mismatch");
|
||||
CHECK(filtered[0].eventId == "e2", "filter event mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_replay_window_returns_slice() {
|
||||
TEST(replay_window_returns_slice);
|
||||
TimeTravelDebugEventBuffer buffer(8);
|
||||
std::string error;
|
||||
CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed");
|
||||
CHECK(buffer.append(event("e2", "sess-1", "step", 0x1001, 11), &error), "append e2 failed");
|
||||
CHECK(buffer.append(event("e3", "sess-1", "step", 0x1002, 12), &error), "append e3 failed");
|
||||
const auto window = buffer.replayWindow(1, 2);
|
||||
CHECK(window.size() == 2, "replay window size mismatch");
|
||||
CHECK(window[0].eventId == "e2", "window[0] mismatch");
|
||||
CHECK(window[1].eventId == "e3", "window[1] mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_replay_window_out_of_range_is_empty() {
|
||||
TEST(replay_window_out_of_range_is_empty);
|
||||
TimeTravelDebugEventBuffer buffer(4);
|
||||
std::string error;
|
||||
CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append failed");
|
||||
const auto window = buffer.replayWindow(5, 2);
|
||||
CHECK(window.empty(), "window should be empty");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_zero_capacity_defaults_to_one() {
|
||||
TEST(zero_capacity_defaults_to_one);
|
||||
TimeTravelDebugEventBuffer buffer(0);
|
||||
std::string error;
|
||||
CHECK(buffer.capacity() == 1, "capacity should default to one");
|
||||
CHECK(buffer.append(event("e1", "sess-1", "step", 0x1000, 10), &error), "append e1 failed");
|
||||
CHECK(buffer.append(event("e2", "sess-1", "step", 0x1001, 11), &error), "append e2 failed");
|
||||
CHECK(buffer.size() == 1, "size should remain one");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 571: Time-Travel Debug Event Buffer\n";
|
||||
|
||||
test_append_success(); // 1
|
||||
test_append_rejects_missing_ids(); // 2
|
||||
test_append_rejects_missing_type(); // 3
|
||||
test_append_rejects_invalid_timestamp(); // 4
|
||||
test_append_rejects_invalid_ip(); // 5
|
||||
test_append_rejects_duplicate_event_id(); // 6
|
||||
test_capacity_eviction_tracks_dropped_count();// 7
|
||||
test_latest_returns_most_recent_events(); // 8
|
||||
test_for_session_filters_events(); // 9
|
||||
test_replay_window_returns_slice(); // 10
|
||||
test_replay_window_out_of_range_is_empty(); // 11
|
||||
test_zero_capacity_defaults_to_one(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
35
progress.md
35
progress.md
@@ -10405,3 +10405,38 @@ numeric/identifier evaluation.
|
||||
- `editor/src/WatchExpressionEvaluatorHardening.h` within header-size limit (`210` <= `600`)
|
||||
- `editor/tests/step570_test.cpp` within test-file size guidance (`142` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 571: Time-Travel Debug Event Buffer
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements a bounded debug event history buffer for short-horizon time-travel
|
||||
replay with deterministic eviction and session/window query helpers.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/TimeTravelDebugEventBuffer.h` - replay buffer module:
|
||||
- bounded append lifecycle with overflow eviction tracking
|
||||
- event validation (ids, type, timestamp, instruction pointer)
|
||||
- latest-event and replay-window extraction helpers
|
||||
- per-session event filtering
|
||||
- duplicate event-id rejection behavior
|
||||
- `editor/tests/step571_test.cpp` - 12 tests covering:
|
||||
- append success and validation failures
|
||||
- duplicate-id rejection behavior
|
||||
- bounded eviction and dropped-count behavior
|
||||
- latest/replay window query behavior
|
||||
- per-session filtering behavior
|
||||
- zero-capacity normalization behavior
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` - `step571_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step571_test step570_test` - PASS
|
||||
- `./editor/build-native/step571_test` - PASS (12/12)
|
||||
- `./editor/build-native/step570_test` - PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/TimeTravelDebugEventBuffer.h` within header-size limit (`86` <= `600`)
|
||||
- `editor/tests/step571_test.cpp` within test-file size guidance (`174` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user