#![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;
49700 + ((pid_tail * 11 + offset * 19) % 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 http_post(port: u16, path: &str, body: &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!(
"POST {path} HTTP/1.1\r\n\
Host: 127.0.0.1:{port}\r\n\
Content-Type: application/json\r\n\
Content-Length: {len}\r\n\
Connection: close\r\n\
\r\n\
{body}",
len = body.len()
);
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_inline(source: &str) -> Child {
Command::new(BURN)
.env("BURN_QUIET", "1")
.arg("-e")
.arg(source)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn burn")
}
#[test]
fn burn_serves_hello_from_burn() {
let port = pick_port();
let source = format!(
r#"
const http = require("node:http");
http.createServer((_req, res) => {{
res.end("hello from burn\n");
}}).listen({port}, () => console.log("listening"));
"#
);
let mut child = spawn_burn_inline(&source);
assert!(
wait_for_listener(port, Duration::from_secs(15)),
"burn listener didn't come up on :{port}"
);
let resp = http_get(port, "/");
assert!(resp.starts_with("HTTP/1.1 200"), "resp:\n{resp}");
assert!(resp.contains("hello from burn"), "resp:\n{resp}");
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn burn_server_echoes_method_and_path() {
let port = pick_port();
let source = format!(
r#"
const http = require("http");
http.createServer((req, res) => {{
res.setHeader("x-method", req.method);
res.setHeader("x-url", req.url);
res.writeHead(201);
res.end("echo\n");
}}).listen({port});
"#
);
let mut child = spawn_burn_inline(&source);
assert!(wait_for_listener(port, Duration::from_secs(15)));
let resp = http_get(port, "/test?q=1");
assert!(resp.starts_with("HTTP/1.1 201"), "resp:\n{resp}");
assert!(resp.contains("x-method: GET"), "resp:\n{resp}");
assert!(resp.contains("x-url: /test?q=1"), "resp:\n{resp}");
assert!(resp.contains("echo"), "resp:\n{resp}");
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn incoming_message_emits_buffer_chunks() {
let port = pick_port();
let source = format!(
r#"
const http = require('http');
http.createServer((req, res) => {{
const chunks = [];
req.on('data', (chunk) => {{
chunks.push({{
type: typeof chunk,
isBuffer: Buffer.isBuffer(chunk),
ctor: chunk && chunk.constructor && chunk.constructor.name,
len: chunk && chunk.length,
}});
}});
req.on('end', () => {{
// Concat must succeed — proves chunks are Buffers in
// the body-parser sense.
let total;
try {{
const bufs = [];
// Re-emit to test concat — we already consumed above,
// so feed the asserted shape into a synthetic concat.
total = chunks.length;
}} catch (_) {{
total = -1;
}}
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({{ chunks, total }}));
}});
}}).listen({port}, () => console.log("listening"));
"#
);
let mut child = spawn_burn_inline(&source);
assert!(
wait_for_listener(port, Duration::from_secs(15)),
"listener didn't bind on :{port}"
);
let resp = http_post(port, "/", r#"{"hello":"world"}"#);
assert!(resp.starts_with("HTTP/1.1 200"), "resp:\n{resp}");
assert!(
resp.contains("\"isBuffer\":true"),
"expected isBuffer:true in response, got:\n{resp}"
);
assert!(
resp.contains("\"len\":17"),
"expected len:17 (length of {{\"hello\":\"world\"}}) in response, got:\n{resp}"
);
assert!(
!resp.contains("\"type\":\"string\""),
"chunk should not be string post-fix, got:\n{resp}"
);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn body_parser_pattern_buffer_concat_succeeds() {
let port = pick_port();
let source = format!(
r#"
const http = require('http');
http.createServer((req, res) => {{
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {{
try {{
const buf = Buffer.concat(chunks);
const parsed = JSON.parse(buf.toString('utf8'));
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({{ ok: true, echo: parsed }}));
}} catch (e) {{
res.statusCode = 500;
res.end('concat error: ' + e.message);
}}
}});
}}).listen({port}, () => console.log("listening"));
"#
);
let mut child = spawn_burn_inline(&source);
assert!(wait_for_listener(port, Duration::from_secs(15)));
let resp = http_post(port, "/", r#"{"a":1,"b":[2,3]}"#);
assert!(resp.starts_with("HTTP/1.1 200"), "resp:\n{resp}");
assert!(resp.contains("\"ok\":true"), "resp:\n{resp}");
assert!(resp.contains("\"a\":1"), "resp:\n{resp}");
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn burn_plain_script_exits_cleanly() {
let child = Command::new(BURN)
.env("BURN_QUIET", "1")
.arg("-e")
.arg(r#"console.log("no listen")"#)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("run burn");
assert!(
child.status.success(),
"exit {}, stderr: {}",
child.status,
String::from_utf8_lossy(&child.stderr)
);
assert!(String::from_utf8_lossy(&child.stdout).contains("no listen"));
}