Step 554: add debug session lifecycle model
This commit is contained in:
@@ -3865,4 +3865,13 @@ target_link_libraries(step553_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step554_test tests/step554_test.cpp)
|
||||
target_include_directories(step554_test PRIVATE src)
|
||||
target_link_libraries(step554_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)
|
||||
|
||||
106
editor/src/DebugSessionModel.h
Normal file
106
editor/src/DebugSessionModel.h
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
// Step 554: Debug Session Model
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct DebugTargetContext {
|
||||
std::string targetId;
|
||||
std::string bufferId;
|
||||
std::string executablePath;
|
||||
bool attached = false;
|
||||
};
|
||||
|
||||
struct DebugSessionState {
|
||||
std::string sessionId;
|
||||
bool active = false;
|
||||
bool paused = false;
|
||||
std::map<std::string, DebugTargetContext> targets;
|
||||
std::string currentTargetId;
|
||||
};
|
||||
|
||||
class DebugSessionModel {
|
||||
public:
|
||||
DebugSessionModel() {
|
||||
state_.sessionId = "debug-session-1";
|
||||
}
|
||||
|
||||
const DebugSessionState& state() const { return state_; }
|
||||
|
||||
bool start(const std::string& targetId,
|
||||
const std::string& bufferId,
|
||||
const std::string& executablePath) {
|
||||
if (state_.active) return false;
|
||||
state_.active = true;
|
||||
state_.paused = false;
|
||||
state_.targets.clear();
|
||||
state_.currentTargetId.clear();
|
||||
return addTargetInternal(targetId, bufferId, executablePath, false, true);
|
||||
}
|
||||
|
||||
bool attach(const std::string& targetId,
|
||||
const std::string& bufferId,
|
||||
const std::string& executablePath) {
|
||||
if (!state_.active) state_.active = true;
|
||||
return addTargetInternal(targetId, bufferId, executablePath, true,
|
||||
state_.currentTargetId.empty());
|
||||
}
|
||||
|
||||
bool pause() {
|
||||
if (!state_.active || state_.paused) return false;
|
||||
state_.paused = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resume() {
|
||||
if (!state_.active || !state_.paused) return false;
|
||||
state_.paused = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool stop() {
|
||||
if (!state_.active) return false;
|
||||
state_.active = false;
|
||||
state_.paused = false;
|
||||
state_.targets.clear();
|
||||
state_.currentTargetId.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool switchTarget(const std::string& targetId) {
|
||||
if (!state_.active) return false;
|
||||
if (state_.targets.count(targetId) == 0) return false;
|
||||
state_.currentTargetId = targetId;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hasTargetForBuffer(const std::string& bufferId) const {
|
||||
for (const auto& kv : state_.targets) {
|
||||
if (kv.second.bufferId == bufferId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
DebugSessionState state_;
|
||||
|
||||
bool addTargetInternal(const std::string& targetId,
|
||||
const std::string& bufferId,
|
||||
const std::string& executablePath,
|
||||
bool attached,
|
||||
bool makeCurrent) {
|
||||
if (targetId.empty() || bufferId.empty() || executablePath.empty()) return false;
|
||||
if (state_.targets.count(targetId) != 0) return false;
|
||||
|
||||
DebugTargetContext context;
|
||||
context.targetId = targetId;
|
||||
context.bufferId = bufferId;
|
||||
context.executablePath = executablePath;
|
||||
context.attached = attached;
|
||||
state_.targets[targetId] = context;
|
||||
|
||||
if (makeCurrent) state_.currentTargetId = targetId;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
140
editor/tests/step554_test.cpp
Normal file
140
editor/tests/step554_test.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
// Step 554: Debug Session Model (12 tests)
|
||||
|
||||
#include "DebugSessionModel.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_start_creates_active_session() {
|
||||
TEST(start_creates_active_session);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(m.state().active, "session should be active");
|
||||
CHECK(!m.state().paused, "session should not be paused");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_start_fails_when_already_active() {
|
||||
TEST(start_fails_when_already_active);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "first start should succeed");
|
||||
CHECK(!m.start("t2", "buf2", "/bin/app2"), "second start should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_attach_starts_session_when_inactive() {
|
||||
TEST(attach_starts_session_when_inactive);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.attach("t1", "buf1", "/bin/app"), "attach should start inactive session");
|
||||
CHECK(m.state().active, "session should become active");
|
||||
CHECK(m.state().targets.at("t1").attached, "target should be attached");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_attach_adds_secondary_target() {
|
||||
TEST(attach_adds_secondary_target);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(m.attach("t2", "buf2", "/bin/app2"), "attach should add second target");
|
||||
CHECK(m.state().targets.size() == 2, "two targets expected");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_pause_transitions_to_paused() {
|
||||
TEST(pause_transitions_to_paused);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(m.pause(), "pause should succeed");
|
||||
CHECK(m.state().paused, "session should be paused");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_resume_transitions_from_paused() {
|
||||
TEST(resume_transitions_from_paused);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(m.pause(), "pause should succeed");
|
||||
CHECK(m.resume(), "resume should succeed");
|
||||
CHECK(!m.state().paused, "session should not be paused");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_pause_fails_when_inactive() {
|
||||
TEST(pause_fails_when_inactive);
|
||||
DebugSessionModel m;
|
||||
CHECK(!m.pause(), "pause should fail when inactive");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_resume_fails_when_not_paused() {
|
||||
TEST(resume_fails_when_not_paused);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(!m.resume(), "resume should fail when not paused");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_stop_clears_session_and_targets() {
|
||||
TEST(stop_clears_session_and_targets);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(m.attach("t2", "buf2", "/bin/app2"), "attach should succeed");
|
||||
CHECK(m.stop(), "stop should succeed");
|
||||
CHECK(!m.state().active, "session should be inactive");
|
||||
CHECK(m.state().targets.empty(), "targets should be cleared");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_switch_target_changes_current_target() {
|
||||
TEST(switch_target_changes_current_target);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(m.attach("t2", "buf2", "/bin/app2"), "attach should succeed");
|
||||
CHECK(m.switchTarget("t2"), "switch should succeed");
|
||||
CHECK(m.state().currentTargetId == "t2", "current target mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_switch_target_fails_for_unknown_target() {
|
||||
TEST(switch_target_fails_for_unknown_target);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(!m.switchTarget("missing"), "switch should fail for unknown target");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_has_target_for_buffer_maps_per_buffer_context() {
|
||||
TEST(has_target_for_buffer_maps_per_buffer_context);
|
||||
DebugSessionModel m;
|
||||
CHECK(m.start("t1", "buf1", "/bin/app"), "start should succeed");
|
||||
CHECK(m.attach("t2", "buf2", "/bin/app2"), "attach should succeed");
|
||||
CHECK(m.hasTargetForBuffer("buf1"), "buffer1 should map to target");
|
||||
CHECK(m.hasTargetForBuffer("buf2"), "buffer2 should map to target");
|
||||
CHECK(!m.hasTargetForBuffer("buf3"), "unknown buffer should not map");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 554: Debug Session Model\n";
|
||||
|
||||
test_start_creates_active_session(); // 1
|
||||
test_start_fails_when_already_active(); // 2
|
||||
test_attach_starts_session_when_inactive(); // 3
|
||||
test_attach_adds_secondary_target(); // 4
|
||||
test_pause_transitions_to_paused(); // 5
|
||||
test_resume_transitions_from_paused(); // 6
|
||||
test_pause_fails_when_inactive(); // 7
|
||||
test_resume_fails_when_not_paused(); // 8
|
||||
test_stop_clears_session_and_targets(); // 9
|
||||
test_switch_target_changes_current_target(); // 10
|
||||
test_switch_target_fails_for_unknown_target(); // 11
|
||||
test_has_target_for_buffer_maps_per_buffer_context(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
33
progress.md
33
progress.md
@@ -9769,3 +9769,36 @@ duplication and re-validate architecture constraints without behavior changes.
|
||||
- `./editor/build-native/step551_test` - PASS (12/12)
|
||||
- `./editor/build-native/step552_test` - PASS (12/12)
|
||||
- `./editor/build-native/step553_test` - PASS (8/8)
|
||||
|
||||
### Step 554: Debug Session Model
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements the core debug session lifecycle model with start/attach/pause/
|
||||
resume/stop transitions and multi-target per-buffer context support.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/DebugSessionModel.h` - debug session control-plane model:
|
||||
- session lifecycle transitions (`start/attach/pause/resume/stop`)
|
||||
- multi-target session state with per-target buffer/executable context
|
||||
- current-target switching
|
||||
- per-buffer target lookup for editor mapping
|
||||
- `editor/tests/step554_test.cpp` - 12 tests covering:
|
||||
- lifecycle transition behavior and invalid transition guards
|
||||
- attach semantics for inactive/active sessions
|
||||
- multi-target session behavior
|
||||
- target switching behavior
|
||||
- per-buffer target mapping behavior
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` - `step554_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step554_test step553_test` - PASS
|
||||
- `./editor/build-native/step554_test` - PASS (12/12)
|
||||
- `./editor/build-native/step553_test` - PASS (8/8) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/DebugSessionModel.h` within header-size limit (`106` <= `600`)
|
||||
- `editor/tests/step554_test.cpp` within test-file size guidance (`140` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user