use bao_engine::context::JsContext;
use bao_engine::value::JsValue;
use std::cell::Cell;
fn eval_str(ctx: &mut JsContext, code: &str) -> String {
match ctx.eval(code, "<test>") {
Ok(JsValue::String(s)) => s,
Ok(JsValue::Number(n)) => format!("{}", n),
Ok(JsValue::Bool(b)) => if b { "true" } else { "false" }.to_string(),
Ok(v) => format!("{:?}", v),
Err(e) => format!("ERROR: {:?}", e),
}
}
thread_local! {
static HOOK_BUDGET: Cell<usize> = const { Cell::new(0) };
}
fn bounded_drain_hook(cx: &mut mozjs::context::JSContext) -> bool {
let exhausted = HOOK_BUDGET.with(|b| {
let n = b.get();
if n == 0 {
return true;
}
b.set(n - 1);
false
});
if exhausted {
return false;
}
bun_runtime::timers::drain_and_check(cx)
}
fn wait_until(ctx: &mut JsContext, js_condition: &str, budget: usize) -> bool {
for _ in 0..60 {
HOOK_BUDGET.with(|b| b.set(budget));
if eval_str(ctx, js_condition) == "y" {
return true;
}
}
false
}
#[test]
fn child_process_spawn_event_lifecycle() {
bun_runtime::install_exit_handler();
bun_runtime::bun_api::init_process_start();
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
ctx.set_post_eval_hook(bounded_drain_hook);
let setup = eval_str(
&mut ctx,
r#"
var cp = require('child_process');
var log = [];
globalThis.__done = false;
var child = cp.spawn('/bin/sh', ['-c', 'echo hi; exit 7']);
// EventEmitter surface present on the RETURNED object.
log.push('on=' + (typeof child.on === 'function'));
log.push('once=' + (typeof child.once === 'function'));
log.push('emit=' + (typeof child.emit === 'function'));
log.push('kill=' + (typeof child.kill === 'function'));
log.push('stdin=' + (typeof child.stdin === 'object'));
log.push('pid=' + (child.pid > 0));
var chunks = [];
child.stdout.on('data', function(d) {
chunks.push(String.fromCharCode.apply(null, new Uint8Array(d)));
});
child.on('exit', function(code, signal) {
log.push('exit=' + code + '/' + signal);
});
child.on('close', function(code, signal) {
log.push('close=' + code + '/' + signal);
log.push('stdout=' + chunks.join(''));
log.push('exitCode=' + child.exitCode);
globalThis.__done = true;
});
globalThis.__log = function() { return log.join('|'); };
'setup-ok'
"#,
);
assert_eq!(setup, "setup-ok");
let done = wait_until(&mut ctx, "globalThis.__done === true ? 'y' : 'n'", 50);
let log = eval_str(&mut ctx, "globalThis.__log()");
assert!(done, "child lifecycle must complete; log: {log}");
for part in [
"on=true",
"once=true",
"emit=true",
"kill=true",
"stdin=true",
"pid=true",
"exit=7/null",
"close=7/null",
"stdout=hi\n",
"exitCode=7",
] {
assert!(
log.split('|').any(|entry| entry == part),
"lifecycle log must contain '{part}', got: {log}"
);
}
let pid_str = eval_str(&mut ctx, "String(child.pid)");
let pid: i32 = pid_str.parse().expect("pid parse");
let proc_path = std::path::PathBuf::from(format!("/proc/{pid}"));
let zombie = std::fs::read_to_string(format!("/proc/{pid}/stat"))
.ok()
.and_then(|s| s.split_whitespace().nth(2).map(|st| st == "Z"))
.unwrap_or(false);
assert!(
!proc_path.exists() || !zombie,
"child must be reaped after exit (no Z zombie residue); /proc/{pid} state: {:?}",
std::fs::read_to_string(format!("/proc/{pid}/stat"))
.ok()
.and_then(|s| s.split_whitespace().nth(2).map(str::to_string))
);
}
#[test]
fn child_process_spawn_once_stderr_and_throw_isolation() {
bun_runtime::install_exit_handler();
bun_runtime::bun_api::init_process_start();
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
ctx.set_post_eval_hook(bounded_drain_hook);
let setup = eval_str(
&mut ctx,
r#"
var cp = require('child_process');
var log = [];
globalThis.__done = false;
var child = cp.spawn('/bin/sh', ['-c', 'echo boom 1>&2; exit 0']);
var exitFires = 0;
child.once('exit', function() { exitFires++; });
child.on('exit', function() {
// Throwing listener must not block later listeners (the close
// handler below still runs and completes the test).
throw new Error('listener throw must be isolated');
});
var errChunks = [];
child.stderr.on('data', function(d) {
errChunks.push(String.fromCharCode.apply(null, new Uint8Array(d)));
});
child.on('close', function() {
log.push('exitFires=' + exitFires);
log.push('stderr=' + errChunks.join(''));
globalThis.__done = true;
});
globalThis.__log = function() { return log.join('|'); };
'setup-ok'
"#,
);
assert_eq!(setup, "setup-ok");
let done = wait_until(&mut ctx, "globalThis.__done === true ? 'y' : 'n'", 50);
let log = eval_str(&mut ctx, "globalThis.__log()");
assert!(done, "stderr lifecycle must complete; log: {log}");
assert!(
log.split('|').any(|e| e == "exitFires=1"),
"once('exit') must fire exactly once, got: {log}"
);
assert!(
log.split('|').any(|e| e == "stderr=boom\n"),
"stderr data must be delivered, got: {log}"
);
}