1 //! Integration tests for guest-debug support (gdbstub + LLDB).
2 //!
3 //! These tests launch `wasmtime run` with `-g <port>`, connect LLDB via
4 //! the wasm remote protocol, execute debug scripts (set breakpoints,
5 //! continue, step, inspect variables), and validate output.
6 //!
7 //! Requirements:
8 //! - LLDB with Wasm plugin support (`LLDB` env var or `/opt/wasi-sdk/bin/lldb` by default)
9 //! - `WASI_SDK_PATH` env var set (for C test programs)
10 //! - Built with `--features gdbstub`
11
12 use filecheck::{CheckerBuilder, NO_VARIABLES};
13 use std::io::{BufRead, BufReader, Write};
14 use std::net::{SocketAddr, TcpListener, TcpStream};
15 use std::process::{Child, Command, Stdio};
16 use std::time::Duration;
17 use test_programs_artifacts::*;
18 use wasmtime::{Result, bail, format_err};
19
20 /// Find the wasmtime binary built alongside the test binary.
wasmtime_binary() -> std::path::PathBuf21 fn wasmtime_binary() -> std::path::PathBuf {
22 let mut me = std::env::current_exe().expect("current_exe specified");
23 me.pop(); // chop off file name
24 me.pop(); // chop off `deps`
25 if cfg!(target_os = "windows") {
26 me.push("wasmtime.exe");
27 } else {
28 me.push("wasmtime");
29 }
30 me
31 }
32
33 /// Find an available TCP port by binding to port 0.
free_port() -> u1634 fn free_port() -> u16 {
35 TcpListener::bind("127.0.0.1:0")
36 .unwrap()
37 .local_addr()
38 .unwrap()
39 .port()
40 }
41
42 /// Path to the wasm-aware LLDB.
lldb_path() -> String43 fn lldb_path() -> String {
44 std::env::var("LLDB").unwrap_or("/opt/wasi-sdk/bin/lldb".to_string())
45 }
46
47 /// The readiness marker printed by the gdbstub to stderr.
48 const GDBSTUB_READY_MARKER: &str = "Debugger listening on";
49
50 /// A running wasmtime process with a gdbstub endpoint.
51 struct WasmtimeWithGdbstub {
52 child: Child,
53 /// Keeps the stderr pipe alive to avoid SIGPIPE on the child.
54 /// Also used by serve tests to read the HTTP address.
55 stderr_reader: BufReader<std::process::ChildStderr>,
56 }
57
58 impl WasmtimeWithGdbstub {
59 /// Spawn wasmtime and wait for stderr to contain the gdbstub
60 /// readiness marker.
spawn( subcmd: &str, gdbstub_port: u16, extra_args: &[&str], timeout: Duration, ) -> Result<Self>61 fn spawn(
62 subcmd: &str,
63 gdbstub_port: u16,
64 extra_args: &[&str],
65 timeout: Duration,
66 ) -> Result<Self> {
67 let mut cmd = Command::new(wasmtime_binary());
68 cmd.arg(subcmd)
69 .arg(format!("-g{gdbstub_port}"))
70 .args(extra_args)
71 .stdin(Stdio::null())
72 .stdout(Stdio::null())
73 .stderr(Stdio::piped());
74 eprintln!("spawning: {cmd:?}");
75 let mut child = cmd.spawn()?;
76
77 let stderr = child.stderr.take().unwrap();
78 let mut reader = BufReader::new(stderr);
79 let deadline = std::time::Instant::now() + timeout;
80 let mut line = String::new();
81 loop {
82 if std::time::Instant::now() > deadline {
83 let _ = child.kill();
84 bail!("timed out waiting for gdbstub readiness");
85 }
86 line.clear();
87 reader.read_line(&mut line)?;
88 eprintln!("wasmtime stderr: {}", line.trim_end());
89 if line.contains(GDBSTUB_READY_MARKER) {
90 return Ok(Self {
91 child,
92 stderr_reader: reader,
93 });
94 }
95 if line.is_empty() {
96 let _ = child.kill();
97 let status = child.wait()?;
98 bail!("wasmtime exited ({status}) without readiness marker");
99 }
100 }
101 }
102
103 /// Read stderr lines until one contains `marker`, returning that line.
wait_for_stderr(&mut self, marker: &str, timeout: Duration) -> Result<String>104 fn wait_for_stderr(&mut self, marker: &str, timeout: Duration) -> Result<String> {
105 let deadline = std::time::Instant::now() + timeout;
106 let mut line = String::new();
107 loop {
108 if std::time::Instant::now() > deadline {
109 bail!("timed out waiting for '{marker}' on stderr");
110 }
111 line.clear();
112 self.stderr_reader.read_line(&mut line)?;
113 eprintln!("wasmtime stderr: {}", line.trim_end());
114 if line.contains(marker) {
115 return Ok(line);
116 }
117 if line.is_empty() {
118 bail!("wasmtime stderr closed before finding '{marker}'");
119 }
120 }
121 }
122 }
123
124 /// Run an LLDB debug script against a gdbstub endpoint.
125 ///
126 /// Connects LLDB to `127.0.0.1:<port>` using the Wasm plugin,
127 /// executes the given script commands, and returns LLDB's stdout.
lldb_with_gdbstub_script(port: u16, script: &str) -> Result<String>128 fn lldb_with_gdbstub_script(port: u16, script: &str) -> Result<String> {
129 let _ = env_logger::try_init();
130
131 let mut cmd = Command::new(lldb_path());
132 cmd.arg("--batch");
133 cmd.arg("-o").arg(format!(
134 "process connect --plugin wasm connect://127.0.0.1:{port}"
135 ));
136 for line in script.lines() {
137 let line = line.trim();
138 if !line.is_empty() {
139 cmd.arg("-o").arg(line);
140 }
141 }
142
143 eprintln!("Running LLDB: {cmd:?}");
144 let output = cmd.output()?;
145
146 let stdout = String::from_utf8(output.stdout)?;
147 let stderr = String::from_utf8(output.stderr)?;
148 eprintln!("--- LLDB stdout ---\n{stdout}");
149 eprintln!("--- LLDB stderr ---\n{stderr}");
150
151 Ok(stdout)
152 }
153
154 /// Validate output against FileCheck-style directives.
check_output(output: &str, directives: &str) -> Result<()>155 fn check_output(output: &str, directives: &str) -> Result<()> {
156 let mut builder = CheckerBuilder::new();
157 builder
158 .text(directives)
159 .map_err(|e| format_err!("unable to build checker: {e:?}"))?;
160 let checker = builder.finish();
161 let check = checker
162 .explain(output, NO_VARIABLES)
163 .map_err(|e| format_err!("{e:?}"))?;
164 assert!(check.0, "didn't pass check {}", check.1);
165 Ok(())
166 }
167
168 /// Test that breakpoints can be set at the initial stop (before any
169 /// continue), then hit when the program runs.
170 #[test]
171 #[ignore]
guest_debug_cli_fib_breakpoint() -> Result<()>172 fn guest_debug_cli_fib_breakpoint() -> Result<()> {
173 let port = free_port();
174 let mut wt = WasmtimeWithGdbstub::spawn(
175 "run",
176 port,
177 &["-Ccache=n", GUEST_DEBUG_FIB],
178 Duration::from_secs(30),
179 )?;
180
181 // Set breakpoint at the initial stop, *before* continuing.
182 // This tests that modules are visible immediately.
183 let output = lldb_with_gdbstub_script(
184 port,
185 r#"
186 b fib
187 c
188 fr v
189 c
190 "#,
191 )?;
192 wt.child.kill().ok();
193 wt.child.wait()?;
194
195 check_output(
196 &output,
197 r#"
198 check: stop reason
199 check: fib
200 check: n =
201 "#,
202 )?;
203 Ok(())
204 }
205
206 /// Test single-stepping within fib.
207 #[test]
208 #[ignore]
guest_debug_cli_fib_step() -> Result<()>209 fn guest_debug_cli_fib_step() -> Result<()> {
210 let port = free_port();
211 let mut wt = WasmtimeWithGdbstub::spawn(
212 "run",
213 port,
214 &["-Ccache=n", GUEST_DEBUG_FIB],
215 Duration::from_secs(30),
216 )?;
217
218 let output = lldb_with_gdbstub_script(
219 port,
220 r#"
221 b fib
222 c
223 n
224 n
225 n
226 fr v
227 c
228 "#,
229 )?;
230 wt.child.kill().ok();
231 wt.child.wait()?;
232
233 check_output(
234 &output,
235 r#"
236 check: stop reason
237 check: fib
238 "#,
239 )?;
240 Ok(())
241 }
242
243 /// Helper: send an HTTP/1.0 request and return the full response.
http_request(addr: SocketAddr, path: &str) -> Result<String>244 fn http_request(addr: SocketAddr, path: &str) -> Result<String> {
245 let mut tcp = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
246 tcp.set_read_timeout(Some(Duration::from_secs(5)))?;
247 write!(tcp, "GET {path} HTTP/1.0\r\nHost: localhost\r\n\r\n")?;
248 let mut response = String::new();
249 let _ = std::io::Read::read_to_string(&mut tcp, &mut response);
250 Ok(response)
251 }
252
253 /// Parse an HTTP serve address from a "Serving HTTP on http://addr/" line.
parse_http_addr(line: &str) -> Result<SocketAddr>254 fn parse_http_addr(line: &str) -> Result<SocketAddr> {
255 line.find("127.0.0.1")
256 .and_then(|start| {
257 let addr = &line[start..];
258 let end = addr.find('/')?;
259 addr[..end].parse().ok()
260 })
261 .ok_or_else(|| format_err!("failed to parse HTTP address from: {line}"))
262 }
263
264 /// Start serve under debugger, continue, and send multiple HTTP requests
265 /// to verify instance reuse works correctly under the debugger.
266 #[test]
267 #[ignore]
guest_debug_serve_requests() -> Result<()>268 fn guest_debug_serve_requests() -> Result<()> {
269 let gdb_port = free_port();
270
271 let mut wt = WasmtimeWithGdbstub::spawn(
272 "serve",
273 gdb_port,
274 &[
275 "-Ccache=n",
276 "--addr=127.0.0.1:0",
277 "-Scli",
278 P2_CLI_SERVE_HELLO_WORLD_COMPONENT,
279 ],
280 Duration::from_secs(30),
281 )?;
282
283 // Connect LLDB in background: just continue to start the HTTP server.
284 let lldb_handle = std::thread::spawn(move || lldb_with_gdbstub_script(gdb_port, "c\n"));
285
286 // Wait for the HTTP server to start.
287 let line = wt.wait_for_stderr("Serving HTTP", Duration::from_secs(15))?;
288 let http_addr = parse_http_addr(&line)?;
289 eprintln!("HTTP address: {http_addr}");
290
291 // Send 3 requests to the same instance, verifying instance reuse.
292 for i in 1..=3 {
293 let resp = http_request(http_addr, "/")?;
294 eprintln!("Response {i}: {}", resp.lines().last().unwrap_or(""));
295 assert!(
296 resp.contains("Hello, WASI!"),
297 "request {i}: expected 'Hello, WASI!' in response, got:\n{resp}"
298 );
299 }
300
301 // Kill wasmtime to unblock LLDB (which is waiting for the process).
302 wt.child.kill().ok();
303 wt.child.wait()?;
304
305 // Collect LLDB output (it exits once the process is killed).
306 let lldb_output = lldb_handle.join().unwrap()?;
307
308 // Verify LLDB connected and the process was running.
309 check_output(
310 &lldb_output,
311 r#"
312 check: stop reason
313 check: resuming
314 "#,
315 )?;
316
317 Ok(())
318 }
319
320 /// Start serve under debugger, set a breakpoint on the HTTP handler,
321 /// send requests, verify breakpoints fire and responses are correct.
322 /// Tests instance reuse across multiple requests.
323 #[test]
324 #[ignore]
guest_debug_serve_breakpoint() -> Result<()>325 fn guest_debug_serve_breakpoint() -> Result<()> {
326 let gdb_port = free_port();
327
328 let mut wt = WasmtimeWithGdbstub::spawn(
329 "serve",
330 gdb_port,
331 &[
332 "-Ccache=n",
333 "--addr=127.0.0.1:0",
334 "-Scli",
335 P2_CLI_SERVE_HELLO_WORLD_COMPONENT,
336 ],
337 Duration::from_secs(30),
338 )?;
339
340 // LLDB script: set a breakpoint on the incoming-handler Guest::handle,
341 // continue to start the server, then for each request: print backtrace
342 // at breakpoint and continue. We do this for 3 requests.
343 let lldb_handle = std::thread::spawn(move || {
344 lldb_with_gdbstub_script(
345 gdb_port,
346 r#"
347 rbreak Guest.*handle
348 c
349 bt
350 c
351 bt
352 c
353 bt
354 c
355 "#,
356 )
357 });
358
359 // Wait for the HTTP server to start.
360 let line = wt.wait_for_stderr("Serving HTTP", Duration::from_secs(15))?;
361 let http_addr = parse_http_addr(&line)?;
362 eprintln!("HTTP address: {http_addr}");
363
364 // Send 3 requests. Each one will hit the breakpoint, LLDB prints
365 // the backtrace, then continues to let the response through.
366 for i in 1..=3 {
367 let resp = http_request(http_addr, "/")?;
368 eprintln!("Response {i}: {}", resp.lines().last().unwrap_or(""));
369 assert!(
370 resp.contains("Hello, WASI!"),
371 "request {i}: expected 'Hello, WASI!' in response, got:\n{resp}"
372 );
373 }
374
375 // Kill wasmtime to unblock LLDB.
376 wt.child.kill().ok();
377 wt.child.wait()?;
378
379 let lldb_output = lldb_handle.join().unwrap()?;
380
381 // Verify LLDB stopped at the breakpoint with the correct function
382 // in the backtrace, and that it happened multiple times.
383 check_output(
384 &lldb_output,
385 r#"
386 check: Guest
387 check: handle
388 check: stop reason
389 check: Guest
390 check: handle
391 check: stop reason
392 check: Guest
393 check: handle
394 "#,
395 )?;
396
397 Ok(())
398 }
399