#![cfg(feature = "bin")]
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpStream};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicU16, Ordering};
use std::time::{Duration, Instant};
const BURN: &str = env!("CARGO_BIN_EXE_burn");
static PORT_CTR: AtomicU16 = AtomicU16::new(0);
fn pick_port() -> u16 {
let offset = PORT_CTR.fetch_add(1, Ordering::Relaxed);
let pid_tail = (std::process::id() & 0xFF) as u16;
51100 + ((pid_tail * 13 + offset * 23) % 5000)
}
fn wait_for_listener(port: u16, timeout: Duration) -> bool {
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let start = Instant::now();
while start.elapsed() < timeout {
if TcpStream::connect_timeout(&addr, Duration::from_millis(100)).is_ok() {
return true;
}
std::thread::sleep(Duration::from_millis(50));
}
false
}
fn http_get(port: u16, path: &str) -> String {
let mut stream = TcpStream::connect(("127.0.0.1", port)).expect("connect");
stream.set_read_timeout(Some(Duration::from_secs(3))).ok();
let req = format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n");
stream.write_all(req.as_bytes()).expect("write");
let mut resp = String::new();
stream.read_to_string(&mut resp).expect("read");
resp
}
fn spawn_burn(source: &str) -> Child {
Command::new(BURN)
.env("BURN_QUIET", "1")
.arg("-A")
.arg("-e")
.arg(source)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn burn")
}
#[test]
fn precompiled_daemon_serves_canonical_request() {
let port = pick_port();
let src = format!(
r#"
const http = require('http');
http.createServer((req, res) => {{
res.setHeader('content-type', 'text/plain');
res.end('precompiled-ok-' + req.url);
}}).listen({port});
console.log('listening');
"#
);
let mut child = spawn_burn(&src);
assert!(
wait_for_listener(port, Duration::from_secs(15)),
"burn listener didn't bind on :{port}"
);
let resp = http_get(port, "/probe");
assert!(resp.starts_with("HTTP/1.1 200"), "resp:\n{resp}");
assert!(
resp.contains("precompiled-ok-/probe"),
"missing body marker:\n{resp}"
);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn precompiled_daemon_preserves_argv_env() {
let port = pick_port();
let src = format!(
r#"
const http = require('http');
const argvLen = (process.argv || []).length;
const hasHome = !!(process.env && process.env.HOME);
http.createServer((_req, res) => {{
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({{
argvLen: argvLen,
hasHome: hasHome,
argv0: process.argv[0],
cwd: process.cwd(),
}}));
}}).listen({port});
"#
);
let mut child = spawn_burn(&src);
assert!(wait_for_listener(port, Duration::from_secs(15)));
let resp = http_get(port, "/");
assert!(resp.starts_with("HTTP/1.1 200"), "resp:\n{resp}");
assert!(
resp.contains("\"argvLen\":") && !resp.contains("\"argvLen\":0"),
"argv not propagated through precompile:\n{resp}"
);
assert!(
resp.contains("\"hasHome\":true") || resp.contains("\"hasHome\":false"),
"env shape unexpected:\n{resp}"
);
assert!(
resp.contains("\"cwd\":\"/"),
"cwd missing or empty:\n{resp}"
);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn precompiled_daemon_async_handler_round_trips() {
let port = pick_port();
let src = format!(
r#"
const http = require('http');
// Touch top-level await so the async wrap is exercised.
await Promise.resolve(0);
let i = 0;
http.createServer(async (_req, res) => {{
await Promise.resolve();
res.end('counter=' + (i++));
}}).listen({port});
"#
);
let mut child = spawn_burn(&src);
assert!(wait_for_listener(port, Duration::from_secs(15)));
for expected in 0..3 {
let resp = http_get(port, "/");
assert!(resp.starts_with("HTTP/1.1 200"), "iter {expected}: {resp}");
let needle = format!("counter={expected}");
assert!(
resp.contains(&needle),
"iter {expected} missing {needle}:\n{resp}"
);
}
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn precompile_propagates_user_syntax_errors_with_nonzero_exit() {
let out = Command::new(BURN)
.env("BURN_QUIET", "1")
.arg("-A")
.arg("-e")
.arg("function broken( { /* unclosed paren */")
.output()
.expect("spawn burn");
assert!(!out.status.success(), "syntax-broken script should fail");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!stderr.trim().is_empty(), "stderr should not be empty");
let lower = stderr.to_lowercase();
assert!(
lower.contains("daemon-init")
|| lower.contains("invoke")
|| lower.contains("syntaxerror")
|| lower.contains("syntax")
|| lower.contains("unexpected"),
"expected init/invoke/syntax marker in stderr, got:\n{stderr}"
);
}
#[test]
fn precompiled_daemon_console_log_reaches_stdout() {
let port = pick_port();
let src = format!(
r#"
console.log('startup-marker');
const http = require('http');
http.createServer((_req, res) => res.end('ok')).listen({port});
"#
);
let mut child = Command::new(BURN)
.env("BURN_QUIET", "1")
.arg("-A")
.arg("-e")
.arg(&src)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn burn");
assert!(wait_for_listener(port, Duration::from_secs(15)));
let stdout_handle = child.stdout.take().expect("stdout pipe");
let _ = child.kill();
let _ = child.wait();
let mut buf = String::new();
let mut stdout_handle = stdout_handle;
let _ = stdout_handle.read_to_string(&mut buf);
assert!(
buf.contains("startup-marker"),
"console.log lost in precompiled init path: {buf:?}"
);
}