The repair loop grounded fixes in failure output; this adds the InspectCoder
"Program Inspector" layer -- capture the ACTUAL variable values at the fault from a
debugger, so a fix can reason from real state (divisor = 0) instead of what the
source implies.
- joule_core::debugger: RuntimeState{stopped_at, variables, frames} +
parse_runtime_state, which reads both lldb (`(Type) name = value`,
`* frame #N: … at file:line`) and gdb (`name = value`, `#N fn at file:line`)
batch output. repair() now takes an `inspect` closure consulted per attempt; the
captured state folds into repair_intent as first-class evidence and onto the
DebugSession receipt. Deterministic (the parser + the fold); only acquisition is
injected.
- joule_coded::inspect_runtime: the backend -- best-effort drive of lldb (macOS) or
gdb (Linux). Sets a breakpoint at the fault, runs, dumps locals + backtrace, parses
it. Returns None (loop falls back to output grounding) for compile errors, when no
debugger is present, or when the target binary can't be located; handles direct
binary commands and `cargo test` (build --no-run, newest deps binary). Wired into
spawn_debug.
Verified: 11 debugger unit tests (lldb + gdb parse, intent folding, inspected state
threaded onto the receipt); an #[ignore] end-to-end test drives REAL lldb on a
divide-by-zero fixture and asserts it captured divisor = 0 (passes on this machine);
lean + l3 daemon builds green.
62 lines
2.5 KiB
Rust
62 lines
2.5 KiB
Rust
//! The debugger's Program Inspector, end-to-end against a real system debugger.
|
|
//! `#[ignore]`d because it needs lldb/gdb installed and OS debugging permission; run
|
|
//! it explicitly with:
|
|
//! cargo test -p joule_coded --test debug_inspect -- --ignored
|
|
|
|
use joule_coded::inspect_runtime;
|
|
use joule_core::debugger::{Diagnostic, DiagnosticKind};
|
|
|
|
#[test]
|
|
#[ignore = "needs a system debugger (lldb/gdb) + OS debugging permission"]
|
|
fn inspector_captures_live_variables_at_a_fault() {
|
|
let root = std::env::temp_dir().join(format!("jc-inspect-{}", std::process::id()));
|
|
let src = root.join("src");
|
|
std::fs::create_dir_all(&src).unwrap();
|
|
std::fs::write(
|
|
root.join("Cargo.toml"),
|
|
"[package]\nname = \"broken\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\
|
|
[[bin]]\nname = \"broken\"\npath = \"src/main.rs\"\n[profile.dev]\ndebug = true\n",
|
|
)
|
|
.unwrap();
|
|
std::fs::write(
|
|
src.join("main.rs"),
|
|
"fn divide(numerator: i32, divisor: i32) -> i32 {\n\
|
|
\x20 let ratio = numerator / divisor;\n\
|
|
\x20 ratio\n\
|
|
}\n\
|
|
fn main() {\n\
|
|
\x20 let divisor = 0;\n\
|
|
\x20 let numerator = 10;\n\
|
|
\x20 println!(\"{}\", divide(numerator, divisor));\n\
|
|
}\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let built = std::process::Command::new("cargo")
|
|
.arg("build")
|
|
.current_dir(&root)
|
|
.status()
|
|
.map(|status| status.success())
|
|
.unwrap_or(false);
|
|
assert!(built, "cargo build of the broken fixture");
|
|
|
|
// The parsed panic points at the divide on line 2; inspect the live locals there.
|
|
let diagnostic = Diagnostic {
|
|
kind: DiagnosticKind::Panic,
|
|
file: Some("src/main.rs".into()),
|
|
line: Some(2),
|
|
message: "attempt to divide by zero".into(),
|
|
};
|
|
let state = inspect_runtime(&root, "./target/debug/broken", &diagnostic);
|
|
std::fs::remove_dir_all(&root).ok();
|
|
|
|
let state = state.expect("inspector captured runtime state from the debugger");
|
|
let names: Vec<&str> = state.variables.iter().map(|variable| variable.name.as_str()).collect();
|
|
assert!(
|
|
names.contains(&"numerator") && names.contains(&"divisor"),
|
|
"captured the live locals at the fault: {names:?}"
|
|
);
|
|
// The value the source can't tell you: divisor was actually 0.
|
|
let divisor = state.variables.iter().find(|variable| variable.name == "divisor").unwrap();
|
|
assert_eq!(divisor.value, "0", "the real runtime value of divisor");
|
|
}
|