use filecheck::{CheckerBuilder, NO_VARIABLES};
use std::io::{BufRead, BufReader, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use test_programs_artifacts::*;
use wasmtime::{Result, bail, format_err};
fn wasmtime_binary() -> std::path::PathBuf {
let mut me = std::env::current_exe().expect("current_exe specified");
me.pop(); me.pop(); if cfg!(target_os = "windows") {
me.push("wasmtime.exe");
} else {
me.push("wasmtime");
}
me
}
fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}
fn lldb_path() -> String {
std::env::var("LLDB").unwrap_or("/opt/wasi-sdk/bin/lldb".to_string())
}
const GDBSTUB_READY_MARKER: &str = "Debugger listening on";
struct WasmtimeWithGdbstub {
child: Child,
stderr_reader: BufReader<std::process::ChildStderr>,
}
impl WasmtimeWithGdbstub {
fn spawn(
subcmd: &str,
gdbstub_port: u16,
extra_args: &[&str],
timeout: Duration,
) -> Result<Self> {
let mut cmd = Command::new(wasmtime_binary());
cmd.arg(subcmd)
.arg(format!("-g{gdbstub_port}"))
.args(extra_args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
eprintln!("spawning: {cmd:?}");
let mut child = cmd.spawn()?;
let stderr = child.stderr.take().unwrap();
let mut reader = BufReader::new(stderr);
let deadline = std::time::Instant::now() + timeout;
let mut line = String::new();
loop {
if std::time::Instant::now() > deadline {
let _ = child.kill();
bail!("timed out waiting for gdbstub readiness");
}
line.clear();
reader.read_line(&mut line)?;
eprintln!("wasmtime stderr: {}", line.trim_end());
if line.contains(GDBSTUB_READY_MARKER) {
return Ok(Self {
child,
stderr_reader: reader,
});
}
if line.is_empty() {
let _ = child.kill();
let status = child.wait()?;
bail!("wasmtime exited ({status}) without readiness marker");
}
}
}
fn wait_for_stderr(&mut self, marker: &str, timeout: Duration) -> Result<String> {
let deadline = std::time::Instant::now() + timeout;
let mut line = String::new();
loop {
if std::time::Instant::now() > deadline {
bail!("timed out waiting for '{marker}' on stderr");
}
line.clear();
self.stderr_reader.read_line(&mut line)?;
eprintln!("wasmtime stderr: {}", line.trim_end());
if line.contains(marker) {
return Ok(line);
}
if line.is_empty() {
bail!("wasmtime stderr closed before finding '{marker}'");
}
}
}
}
fn lldb_with_gdbstub_script(port: u16, script: &str) -> Result<String> {
let _ = env_logger::try_init();
let mut cmd = Command::new(lldb_path());
cmd.arg("--batch");
cmd.arg("-o").arg(format!(
"process connect --plugin wasm connect://127.0.0.1:{port}"
));
for line in script.lines() {
let line = line.trim();
if !line.is_empty() {
cmd.arg("-o").arg(line);
}
}
eprintln!("Running LLDB: {cmd:?}");
let output = cmd.output()?;
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
eprintln!("--- LLDB stdout ---\n{stdout}");
eprintln!("--- LLDB stderr ---\n{stderr}");
Ok(stdout)
}
fn check_output(output: &str, directives: &str) -> Result<()> {
let mut builder = CheckerBuilder::new();
builder
.text(directives)
.map_err(|e| format_err!("unable to build checker: {e:?}"))?;
let checker = builder.finish();
let check = checker
.explain(output, NO_VARIABLES)
.map_err(|e| format_err!("{e:?}"))?;
assert!(check.0, "didn't pass check {}", check.1);
Ok(())
}
#[test]
#[ignore]
fn guest_debug_cli_fib_breakpoint() -> Result<()> {
let port = free_port();
let mut wt = WasmtimeWithGdbstub::spawn(
"run",
port,
&["-Ccache=n", GUEST_DEBUG_FIB],
Duration::from_secs(30),
)?;
let output = lldb_with_gdbstub_script(
port,
r#"
b fib
c
fr v
c
"#,
)?;
wt.child.kill().ok();
wt.child.wait()?;
check_output(
&output,
r#"
check: stop reason
check: fib
check: n =
"#,
)?;
Ok(())
}
#[test]
#[ignore]
fn guest_debug_cli_fib_step() -> Result<()> {
let port = free_port();
let mut wt = WasmtimeWithGdbstub::spawn(
"run",
port,
&["-Ccache=n", GUEST_DEBUG_FIB],
Duration::from_secs(30),
)?;
let output = lldb_with_gdbstub_script(
port,
r#"
b fib
c
n
n
n
fr v
c
"#,
)?;
wt.child.kill().ok();
wt.child.wait()?;
check_output(
&output,
r#"
check: stop reason
check: fib
"#,
)?;
Ok(())
}
fn http_request(addr: SocketAddr, path: &str) -> Result<String> {
let mut tcp = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
tcp.set_read_timeout(Some(Duration::from_secs(5)))?;
write!(tcp, "GET {path} HTTP/1.0\r\nHost: localhost\r\n\r\n")?;
let mut response = String::new();
let _ = std::io::Read::read_to_string(&mut tcp, &mut response);
Ok(response)
}
fn parse_http_addr(line: &str) -> Result<SocketAddr> {
line.find("127.0.0.1")
.and_then(|start| {
let addr = &line[start..];
let end = addr.find('/')?;
addr[..end].parse().ok()
})
.ok_or_else(|| format_err!("failed to parse HTTP address from: {line}"))
}
#[test]
#[ignore]
fn guest_debug_serve_requests() -> Result<()> {
let gdb_port = free_port();
let mut wt = WasmtimeWithGdbstub::spawn(
"serve",
gdb_port,
&[
"-Ccache=n",
"--addr=127.0.0.1:0",
"-Scli",
P2_CLI_SERVE_HELLO_WORLD_COMPONENT,
],
Duration::from_secs(30),
)?;
let lldb_handle = std::thread::spawn(move || lldb_with_gdbstub_script(gdb_port, "c\n"));
let line = wt.wait_for_stderr("Serving HTTP", Duration::from_secs(15))?;
let http_addr = parse_http_addr(&line)?;
eprintln!("HTTP address: {http_addr}");
for i in 1..=3 {
let resp = http_request(http_addr, "/")?;
eprintln!("Response {i}: {}", resp.lines().last().unwrap_or(""));
assert!(
resp.contains("Hello, WASI!"),
"request {i}: expected 'Hello, WASI!' in response, got:\n{resp}"
);
}
wt.child.kill().ok();
wt.child.wait()?;
let lldb_output = lldb_handle.join().unwrap()?;
check_output(
&lldb_output,
r#"
check: stop reason
check: resuming
"#,
)?;
Ok(())
}
#[test]
#[ignore]
fn guest_debug_serve_breakpoint() -> Result<()> {
let gdb_port = free_port();
let mut wt = WasmtimeWithGdbstub::spawn(
"serve",
gdb_port,
&[
"-Ccache=n",
"--addr=127.0.0.1:0",
"-Scli",
P2_CLI_SERVE_HELLO_WORLD_COMPONENT,
],
Duration::from_secs(30),
)?;
let lldb_handle = std::thread::spawn(move || {
lldb_with_gdbstub_script(
gdb_port,
r#"
rbreak Guest.*handle
c
bt
c
bt
c
bt
c
"#,
)
});
let line = wt.wait_for_stderr("Serving HTTP", Duration::from_secs(15))?;
let http_addr = parse_http_addr(&line)?;
eprintln!("HTTP address: {http_addr}");
for i in 1..=3 {
let resp = http_request(http_addr, "/")?;
eprintln!("Response {i}: {}", resp.lines().last().unwrap_or(""));
assert!(
resp.contains("Hello, WASI!"),
"request {i}: expected 'Hello, WASI!' in response, got:\n{resp}"
);
}
wt.child.kill().ok();
wt.child.wait()?;
let lldb_output = lldb_handle.join().unwrap()?;
check_output(
&lldb_output,
r#"
check: Guest
check: handle
check: stop reason
check: Guest
check: handle
check: stop reason
check: Guest
check: handle
"#,
)?;
Ok(())
}