//! The stateful DAP client, end-to-end against a real `lldb-dap`. `#[ignore]`d //! because it needs the adapter installed + OS debugging permission; run with: //! cargo test -p joule_coded --test dap_step -- --ignored use joule_coded::dap::{find_adapter, DapSession}; #[test] #[ignore = "needs lldb-dap + OS debugging permission"] fn dap_session_walks_execution_and_watches_a_value_change() { let Some(adapter) = find_adapter() else { eprintln!("no lldb-dap adapter; skipping"); return; }; let root = std::env::temp_dir().join(format!("jc-dap-{}", std::process::id())); let src = root.join("src"); std::fs::create_dir_all(&src).unwrap(); std::fs::write( root.join("Cargo.toml"), "[package]\nname = \"loopy\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\ [[bin]]\nname = \"loopy\"\npath = \"src/main.rs\"\n[profile.dev]\ndebug = true\n", ) .unwrap(); // `total` evolves 0 → 1 → 3 across the breakpoint on line 4. std::fs::write( src.join("main.rs"), "fn main() {\n\ \x20 let mut total = 0i32;\n\ \x20 for i in 1..=3 {\n\ \x20 total += i;\n\ \x20 }\n\ \x20 println!(\"{}\", total);\n\ }\n", ) .unwrap(); // Blocking is the point: this test cannot proceed until the fixture is // built, and there is no event loop here for it to stall. #[allow(clippy::disallowed_methods)] 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 loop fixture"); let binary = root.join("target/debug/loopy"); let source = root.join("src/main.rs"); // Run-to-stop is adapter-dependent (some macOS lldb-dap builds won't launch the // debuggee under the adapter). The session times out cleanly rather than hanging; // treat that as "adapter can't drive here" and skip, not a client bug. let (mut session, first) = match DapSession::launch(&adapter, &binary, &root, &source, &[4]) { Ok(result) => result, Err(error) => { eprintln!("lldb-dap did not stop at the breakpoint ({error}); skipping the walk assertions"); std::fs::remove_dir_all(&root).ok(); return; } }; let total_at = |stop: &joule_coded::dap::Stop| -> Option { stop.variables.iter().find(|variable| variable.name == "total").and_then(|variable| variable.value.parse().ok()) }; // First hit: the loop body hasn't added yet, total == 0. assert_eq!(first.reason, "breakpoint"); let first_total = total_at(&first).expect("captured `total` at the first stop"); assert_eq!(first_total, 0, "total is 0 on the first iteration"); // Continue to the next iteration: total must have advanced — we WATCHED it change // by walking execution, not by reading the source. let second = session.continue_run().expect("continue to the next breakpoint"); let second_total = total_at(&second).expect("captured `total` at the second stop"); assert!(second_total > first_total, "total advanced across iterations: {first_total} -> {second_total}"); session.terminate(); std::fs::remove_dir_all(&root).ok(); }