#![cfg(feature = "bin")]
use std::process::{Command, Stdio};
const BURN: &str = env!("CARGO_BIN_EXE_burn");
fn run_eval(code: &str) -> (String, String, bool) {
let out = Command::new(BURN)
.env("BURN_QUIET", "1")
.env("BURN_SHARDS", "1")
.arg("-e")
.arg(code)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("spawn burn");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
out.status.success(),
)
}
#[test]
fn exit_event_fires_on_natural_completion() {
let (out, err, ok) = run_eval(
"process.on('exit', () => process.stdout.write('EXIT-HANDLER\\n')); console.log('BODY');",
);
assert!(ok, "burn should exit 0; stderr={err}");
assert!(out.contains("BODY"), "body output missing: {out:?}");
assert!(
out.contains("EXIT-HANDLER"),
"process.on('exit') handler did not run on natural completion: {out:?}"
);
}
#[test]
fn before_exit_event_fires_on_natural_completion() {
let (out, err, ok) = run_eval(
"process.on('beforeExit', () => process.stdout.write('BEFORE-EXIT\\n')); console.log('BODY');",
);
assert!(ok, "stderr={err}");
assert!(
out.contains("BEFORE-EXIT"),
"process.on('beforeExit') did not fire: {out:?}"
);
}
#[test]
fn lifecycle_order_is_body_then_before_exit_then_exit() {
let (out, err, ok) = run_eval(
"process.on('beforeExit', () => console.log('MARK-BEFORE')); \
process.on('exit', () => console.log('MARK-EXIT')); \
console.log('MARK-BODY');",
);
assert!(ok, "stderr={err}");
let body = out.find("MARK-BODY");
let before = out.find("MARK-BEFORE");
let exit = out.find("MARK-EXIT");
assert!(
body.is_some() && before.is_some() && exit.is_some(),
"all three lifecycle markers should be present: {out:?}"
);
assert!(body < before, "body must precede beforeExit: {out:?}");
assert!(before < exit, "beforeExit must precede exit: {out:?}");
}
#[test]
fn exit_event_fires_exactly_once_on_natural_completion() {
let (out, err, ok) = run_eval(
"process.on('exit', () => process.stdout.write('EXIT-ONCE\\n')); console.log('BODY');",
);
assert!(ok, "stderr={err}");
assert_eq!(
out.matches("EXIT-ONCE").count(),
1,
"'exit' must fire exactly once: {out:?}"
);
}
#[test]
fn exit_handler_receives_zero_code_on_clean_drain() {
let (out, err, ok) =
run_eval("process.on('exit', (code) => process.stdout.write('CODE=' + code + '\\n'));");
assert!(ok, "stderr={err}");
assert!(
out.contains("CODE=0"),
"exit handler should receive code 0 on clean drain: {out:?}"
);
}
#[test]
fn before_exit_can_rearm_the_event_loop() {
let (out, err, ok) = run_eval(
"let armed = false; \
process.on('beforeExit', () => { if (!armed) { armed = true; setTimeout(() => console.log('REARMED'), 5); } }); \
process.on('exit', () => process.stdout.write('FINAL-EXIT\\n')); \
console.log('BODY');",
);
assert!(ok, "stderr={err}");
assert!(
out.contains("REARMED"),
"beforeExit re-arm work did not run: {out:?}"
);
assert!(
out.contains("FINAL-EXIT"),
"exit did not fire after the re-arm drained: {out:?}"
);
assert!(
out.find("REARMED") < out.find("FINAL-EXIT"),
"exit must fire after the re-armed work completes: {out:?}"
);
}
#[test]
fn explicit_process_exit_fires_exit_exactly_once() {
let (out, err, ok) = run_eval(
"process.on('exit', () => process.stdout.write('ONLY-ONCE\\n')); console.log('BODY'); process.exit(0);",
);
assert!(ok, "stderr={err}");
assert_eq!(
out.matches("ONLY-ONCE").count(),
1,
"'exit' must fire exactly once even with an explicit process.exit(): {out:?}"
);
}
#[test]
fn exit_fires_after_pending_async_work_completes() {
let (out, err, ok) = run_eval(
"require('fs').promises.readFile('/etc/hostname', 'utf8') \
.then(() => console.log('ASYNC-DONE')); \
process.on('exit', () => process.stdout.write('EXIT-AFTER\\n'));",
);
assert!(ok, "stderr={err}");
assert!(
out.contains("ASYNC-DONE"),
"pending async work did not complete before exit: {out:?}"
);
assert!(out.contains("EXIT-AFTER"), "exit did not fire: {out:?}");
assert!(
out.find("ASYNC-DONE") < out.find("EXIT-AFTER"),
"exit must fire only after async work resolved: {out:?}"
);
}