use std::io::{BufRead, BufReader, Write};
use std::net::{SocketAddr, TcpStream};
use std::sync::mpsc::{self, RecvTimeoutError};
use std::time::Duration;
use serde_json::{Value, json};
use pasta_lua::{DebugConfig, PastaLuaRuntime, RuntimeConfig, TranspileContext};
fn write_frame<W: Write>(out: &mut W, value: &Value) -> std::io::Result<()> {
let body = serde_json::to_vec(value)?;
write!(out, "Content-Length: {}\r\n\r\n", body.len())?;
out.write_all(&body)?;
out.flush()
}
fn read_frame<R: BufRead>(reader: &mut R) -> std::io::Result<Option<Value>> {
let mut content_length: Option<usize> = None;
loop {
let mut line = String::new();
let n = reader.read_line(&mut line)?;
if n == 0 {
return Ok(None); }
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break; }
if let Some((name, val)) = trimmed.split_once(':')
&& name.trim().eq_ignore_ascii_case("Content-Length")
{
content_length = val.trim().parse::<usize>().ok();
}
}
let len = content_length.expect("framed message must carry a Content-Length");
let mut body = vec![0u8; len];
std::io::Read::read_exact(reader, &mut body)?;
let value = serde_json::from_slice(&body)?;
Ok(Some(value))
}
const WATCHDOG: Duration = Duration::from_secs(15);
const PERSIST_SOURCE: &str = "@persist_scenario";
const PERSIST_BP_LINE: u32 = 2;
const PERSIST_CHUNK: &str = "\
local a = 1
local b = a + 1
return b
";
struct DapClient {
reader: BufReader<TcpStream>,
writer: TcpStream,
}
impl DapClient {
fn connect(addr: SocketAddr) -> Self {
let stream = TcpStream::connect(addr).expect("client must connect to the bound port");
stream
.set_read_timeout(Some(WATCHDOG))
.expect("TEST-ONLY read timeout");
let writer = stream.try_clone().expect("clone socket for writing");
Self {
reader: BufReader::new(stream),
writer,
}
}
fn send_request(&mut self, seq: u64, command: &str, arguments: Value) {
let req = json!({
"seq": seq,
"type": "request",
"command": command,
"arguments": arguments,
});
write_frame(&mut self.writer, &req).expect("client write must succeed");
}
fn recv(&mut self) -> Option<Value> {
read_frame(&mut self.reader)
.expect("client read must succeed (TEST-ONLY timeout)")
}
fn recv_until(&mut self, mut pred: impl FnMut(&Value) -> bool) -> Value {
loop {
let msg = self.recv().expect("a frame must be present (peer did not close)");
if pred(&msg) {
return msg;
}
}
}
}
fn is_event(msg: &Value, name: &str) -> bool {
msg["type"] == "event" && msg["event"] == name
}
fn is_response(msg: &Value, command: &str) -> bool {
msg["type"] == "response" && msg["command"] == command
}
#[test]
fn enabled_runtime_persists_breakpoint_across_requests() {
let (addr_tx, addr_rx) = mpsc::channel::<SocketAddr>();
let (go_tx, go_rx) = mpsc::channel::<()>();
let host = std::thread::spawn(move || -> Result<(), String> {
let debug_cfg = DebugConfig {
enabled: true,
listen: Some("127.0.0.1:0".parse().unwrap()),
..Default::default()
};
let config = RuntimeConfig::minimal().with_debug(debug_cfg);
let runtime = PastaLuaRuntime::with_config(TranspileContext::new(), config)
.map_err(|e| format!("runtime build failed: {e}"))?;
let addr = runtime
.debug_local_addr()
.ok_or_else(|| "enabled runtime must expose a bound debug addr".to_string())?;
addr_tx.send(addr).map_err(|_| "addr send failed".to_string())?;
go_rx
.recv_timeout(WATCHDOG)
.map_err(|_| "no go signal before request #1".to_string())?;
runtime
.exec_named(PERSIST_CHUNK, PERSIST_SOURCE)
.map_err(|e| format!("request #1 exec failed: {e}"))?;
go_rx
.recv_timeout(WATCHDOG)
.map_err(|_| "no go signal before request #2".to_string())?;
runtime
.exec_named(PERSIST_CHUNK, PERSIST_SOURCE)
.map_err(|e| format!("request #2 exec failed: {e}"))?;
drop(runtime);
Ok(())
});
let addr = addr_rx
.recv_timeout(WATCHDOG)
.expect("host must publish the bound addr before the watchdog");
let mut client = DapClient::connect(addr);
client.send_request(1, "initialize", json!({ "adapterID": "pasta" }));
let _ = client.recv_until(|m| is_response(m, "initialize"));
let _ = client.recv_until(|m| is_event(m, "initialized"));
client.send_request(
2,
"setBreakpoints",
json!({
"source": { "path": PERSIST_SOURCE },
"breakpoints": [{ "line": PERSIST_BP_LINE }],
}),
);
let bp_resp = client.recv_until(|m| is_response(m, "setBreakpoints"));
assert_eq!(bp_resp["body"]["breakpoints"][0]["verified"], true);
client.send_request(3, "configurationDone", json!({}));
let _ = client.recv_until(|m| is_response(m, "configurationDone"));
go_tx.send(()).expect("go #1");
let stopped1 = client.recv_until(|m| is_event(m, "stopped"));
assert_eq!(
stopped1["body"]["reason"], "breakpoint",
"request #1 must hit the persisted breakpoint"
);
let thread_id = stopped1["body"]["threadId"].as_u64().unwrap_or(1);
client.send_request(10, "continue", json!({ "threadId": thread_id }));
let _ = client.recv_until(|m| is_response(m, "continue"));
go_tx.send(()).expect("go #2");
let stopped2 = client.recv_until(|m| is_event(m, "stopped"));
assert_eq!(
stopped2["body"]["reason"], "breakpoint",
"request #2 must hit the SAME breakpoint without re-setting it (runtime-scope persistence)"
);
client.send_request(20, "continue", json!({ "threadId": thread_id }));
let _ = client.recv_until(|m| is_response(m, "continue"));
let (done_tx, done_rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = done_tx.send(host.join());
});
match done_rx.recv_timeout(WATCHDOG) {
Ok(joined) => {
joined
.expect("host thread must not panic")
.expect("both requests must run to completion with persisted BP");
}
Err(RecvTimeoutError::Timeout) => panic!("host thread did not finish (hang?)"),
Err(RecvTimeoutError::Disconnected) => panic!("join watcher disconnected"),
}
}
#[test]
fn runtime_teardown_emits_terminated_to_connected_client() {
let (addr_tx, addr_rx) = mpsc::channel::<SocketAddr>();
let (drop_tx, drop_rx) = mpsc::channel::<()>();
let host = std::thread::spawn(move || -> Result<(), String> {
let debug_cfg = DebugConfig {
enabled: true,
listen: Some("127.0.0.1:0".parse().unwrap()),
..Default::default()
};
let config = RuntimeConfig::minimal().with_debug(debug_cfg);
let runtime = PastaLuaRuntime::with_config(TranspileContext::new(), config)
.map_err(|e| format!("runtime build failed: {e}"))?;
let addr = runtime
.debug_local_addr()
.ok_or_else(|| "enabled runtime must expose a bound debug addr".to_string())?;
addr_tx.send(addr).map_err(|_| "addr send failed".to_string())?;
drop_rx
.recv_timeout(WATCHDOG)
.map_err(|_| "no drop signal".to_string())?;
let _ = runtime.exec("return 1");
drop(runtime); Ok(())
});
let addr = addr_rx.recv_timeout(WATCHDOG).expect("bound addr");
let mut client = DapClient::connect(addr);
client.send_request(1, "initialize", json!({ "adapterID": "pasta" }));
let _ = client.recv_until(|m| is_response(m, "initialize"));
let _ = client.recv_until(|m| is_event(m, "initialized"));
drop_tx.send(()).expect("drop signal");
let terminated = client.recv_until(|m| is_event(m, "terminated"));
assert!(
is_event(&terminated, "terminated"),
"runtime teardown must emit a DAP terminated event to the connected client"
);
let (done_tx, done_rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = done_tx.send(host.join());
});
match done_rx.recv_timeout(WATCHDOG) {
Ok(joined) => {
joined.expect("host thread must not panic").expect("teardown ok");
}
Err(_) => panic!("host thread did not finish (hang?)"),
}
}
#[test]
fn disabled_runtime_is_zero_cost() {
let runtime =
PastaLuaRuntime::with_config(TranspileContext::new(), RuntimeConfig::minimal()).unwrap();
assert!(
runtime.debug_local_addr().is_none(),
"disabled runtime must NOT open a debug port"
);
let debug_is_nil: bool = runtime
.exec("return debug == nil")
.expect("eval ok")
.as_boolean()
.expect("boolean");
assert!(debug_is_nil, "disabled runtime must NOT expose std_debug");
let jit_on: bool = runtime
.exec("return jit ~= nil and jit.status() == true")
.expect("eval ok")
.as_boolean()
.expect("boolean");
assert!(
jit_on,
"disabled runtime must NOT install the hook (jit stays ON, no jit.off())"
);
}
#[test]
fn default_runtime_config_debug_is_disabled() {
assert!(!RuntimeConfig::new().debug.enabled, "new() debug disabled");
assert!(!RuntimeConfig::minimal().debug.enabled, "minimal() debug disabled");
assert!(!RuntimeConfig::full().debug.enabled, "full() debug disabled");
assert!(!RuntimeConfig::default().debug.enabled, "default() debug disabled");
}
mod zero_cost_sandbox_regression {
use super::*;
const DEFAULT_DEBUG_PORT: u16 = 9276;
fn disabled_runtime() -> PastaLuaRuntime {
PastaLuaRuntime::with_config(TranspileContext::new(), RuntimeConfig::minimal())
.expect("disabled runtime must build")
}
#[test]
fn r5_2_disabled_installs_no_hook_jit_stays_on() {
let runtime = disabled_runtime();
assert!(
!runtime.debug_enabled(),
"R5.2: disabled runtime must NOT hold a DebugHandle (no hook installed)"
);
let jit_on_after_run: bool = runtime
.exec(
"\
local sum = 0
for i = 1, 1000 do
sum = sum + i
end
return jit ~= nil and jit.status() == true",
)
.expect("multi-line eval ok")
.as_boolean()
.expect("boolean result");
assert!(
jit_on_after_run,
"R5.2: JIT must remain ON after a multi-line run (no hook → no engine-wide jit.off())"
);
}
#[test]
fn r5_3_disabled_keeps_sandbox_debug_is_nil() {
let runtime = disabled_runtime();
let debug_is_nil: bool = runtime
.exec("return debug == nil")
.expect("eval ok")
.as_boolean()
.expect("boolean");
assert!(
debug_is_nil,
"R5.3: disabled runtime must NOT expose the `debug` global (sandbox)"
);
let cannot_introspect: bool = runtime
.exec("return not pcall(function() return debug.getinfo(1) end)")
.expect("eval ok")
.as_boolean()
.expect("boolean");
assert!(
cannot_introspect,
"R5.3: scripts must NOT be able to reach stack introspection (debug.getinfo)"
);
}
#[test]
fn r5_5_disabled_opens_no_port() {
let runtime = disabled_runtime();
assert!(
runtime.debug_local_addr().is_none(),
"R5.5: disabled runtime must NOT open/expose a debug port (authoritative)"
);
assert!(
!runtime.debug_enabled(),
"R5.5: disabled runtime holds no debug handle (no transport bound)"
);
let connect = TcpStream::connect_timeout(
&SocketAddr::from(([127, 0, 0, 1], DEFAULT_DEBUG_PORT)),
Duration::from_millis(200),
);
match connect {
Err(_) => { }
Ok(_) => {
eprintln!(
"[zero_cost_sandbox_regression] note: port {DEFAULT_DEBUG_PORT} accepted a \
connection from an unrelated process; relying on debug_local_addr()==None"
);
}
}
}
#[test]
fn enabled_runtime_flips_the_disabled_signals() {
let debug_cfg = DebugConfig {
enabled: true,
listen: Some("127.0.0.1:0".parse().unwrap()),
..Default::default()
};
let config = RuntimeConfig::minimal().with_debug(debug_cfg);
let runtime = PastaLuaRuntime::with_config(TranspileContext::new(), config)
.expect("enabled runtime must build");
assert!(
runtime.debug_enabled(),
"discrimination: enabled runtime DOES hold a DebugHandle"
);
assert!(
runtime.debug_local_addr().is_some(),
"discrimination: enabled runtime DOES expose a bound debug addr (port opened)"
);
drop(runtime);
}
}