use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
const BAO_BIN: &str = "target/debug/bao";
fn bao_path() -> Option<PathBuf> {
if let Ok(override_path) = std::env::var("BAO_BIN") {
let candidate = PathBuf::from(override_path);
if candidate.is_file() {
return Some(candidate);
}
}
if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
let candidate = PathBuf::from(target_dir).join("debug").join("bao");
if candidate.is_file() {
return Some(candidate);
}
}
let mut here = std::env::current_dir().ok()?;
for _ in 0..5 {
let candidate = here.join(BAO_BIN);
if candidate.is_file() {
return Some(candidate);
}
if !here.pop() {
break;
}
}
let direct = PathBuf::from(BAO_BIN);
if direct.is_file() {
Some(direct)
} else {
None
}
}
fn run_bao(args: &[&str], stdin: Option<&str>) -> std::io::Result<std::process::Output> {
let bao = bao_path().expect("bao binary not found — run `cargo build` first");
let mut cmd = Command::new(bao);
cmd.args(args);
if stdin.is_some() {
cmd.stdin(std::process::Stdio::piped());
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn()?;
if let Some(input) = stdin {
use std::io::Write;
let mut child_stdin = child.stdin.take().expect("stdin pipe");
child_stdin.write_all(input.as_bytes())?;
drop(child_stdin); }
child.wait_with_output()
}
#[test]
fn bao_cli_e2e_full_lifecycle() {
let bao = match bao_path() {
Some(p) => p,
None => {
eprintln!(
"SKIP: bao binary not found at ./{} — run `cargo build` first",
BAO_BIN
);
return;
}
};
eprintln!("using bao binary: {}", bao.display());
let mut passed = 0u32;
let mut failed = 0u32;
match run_bao(&["--help"], None) {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
if combined.to_lowercase().contains("usage")
|| combined.to_lowercase().contains("bao")
|| combined.contains("run")
|| combined.contains("browser")
{
eprintln!("PASS §1::cli_help_responds");
passed += 1;
} else {
eprintln!("FAIL §1::cli_help_responds (combined output empty or unexpected)");
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §1::cli_help_responds (spawn failed: {})", e);
failed += 1;
}
}
match run_bao(&["run", "--eval", "console.log('bao-e2e-marker')"], None) {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("bao-e2e-marker") {
eprintln!("PASS §2::eval_console_log");
passed += 1;
} else {
eprintln!(
"FAIL §2::eval_console_log (stdout='{}', exit={:?})",
stdout.trim(),
output.status.code()
);
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §2::eval_console_log (spawn failed: {})", e);
failed += 1;
}
}
match run_bao(&["run", "--eval", "console.log(typeof Bun)"], None) {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.trim().contains("object") {
eprintln!("PASS §3::bun_api_available");
passed += 1;
} else {
eprintln!(
"FAIL §3::bun_api_available (typeof Bun = '{}', exit={:?})",
stdout.trim(),
output.status.code()
);
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §3::bun_api_available (spawn failed: {})", e);
failed += 1;
}
}
match run_bao(&["run", "--eval", "console.log(typeof process)"], None) {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.trim().contains("object") {
eprintln!("PASS §4::node_api_available");
passed += 1;
} else {
eprintln!(
"FAIL §4::node_api_available (typeof process = '{}', exit={:?})",
stdout.trim(),
output.status.code()
);
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §4::node_api_available (spawn failed: {})", e);
failed += 1;
}
}
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("bao_e2e_test_script.js");
std::fs::write(
&script_path,
r#"
// 文件脚本 — 用 Node API (Buffer) + Bun API (Bun.version)
const buf = Buffer.from('hello-from-file');
console.log('file-script-runs');
console.log(buf.toString());
console.log(typeof Bun === 'object' ? 'bun-ok' : 'bun-missing');
"#,
)
.expect("write temp script");
let script_str = script_path.to_string_lossy().to_string();
match run_bao(&["run", script_str.as_str()], None) {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("file-script-runs")
&& stdout.contains("hello-from-file")
&& stdout.contains("bun-ok")
{
eprintln!("PASS §5::run_file_script");
passed += 1;
} else {
eprintln!(
"FAIL §5::run_file_script (stdout='{}', exit={:?})",
stdout.trim(),
output.status.code()
);
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §5::run_file_script (spawn failed: {})", e);
failed += 1;
}
}
match run_bao(&["run", "--eval", "process.exit(0)"], None) {
Ok(output) => {
let code = output.status.code();
if code == Some(0) {
eprintln!("PASS §6a::exit_code_zero");
passed += 1;
} else {
eprintln!("FAIL §6a::exit_code_zero (got {:?})", code);
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §6a::exit_code_zero (spawn failed: {})", e);
failed += 1;
}
}
match run_bao(&["run", "--eval", "process.exit(42)"], None) {
Ok(output) => {
let code = output.status.code();
if code == Some(42) {
eprintln!("PASS §6b::exit_code_42");
passed += 1;
} else {
eprintln!("FAIL §6b::exit_code_42 (got {:?})", code);
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §6b::exit_code_42 (spawn failed: {})", e);
failed += 1;
}
}
match run_bao(
&[
"run",
"--eval",
"console.log('line1'); console.log('line2');",
],
None,
) {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("line1") && stdout.contains("line2") {
eprintln!("PASS §7::multi_line_stdout");
passed += 1;
} else {
eprintln!("FAIL §7::multi_line_stdout (stdout='{}')", stdout.trim());
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §7::multi_line_stdout (spawn failed: {})", e);
failed += 1;
}
}
match run_bao(
&[
"run",
"--eval",
"console.log('stdout-marker'); console.error('stderr-marker');",
],
None,
) {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if stdout.contains("stdout-marker")
&& stderr.contains("stderr-marker")
&& !stdout.contains("stderr-marker")
&& !stderr.contains("stdout-marker")
{
eprintln!("PASS §8::console_stream_routing");
passed += 1;
} else {
eprintln!(
"FAIL §8::console_stream_routing (stdout='{}', stderr='{}', exit={:?})",
stdout.trim(),
stderr.trim(),
output.status.code()
);
failed += 1;
}
}
Err(e) => {
eprintln!("FAIL §8::console_stream_routing (spawn failed: {})", e);
failed += 1;
}
}
let _ = std::fs::remove_file(&script_path);
eprintln!(
"=== bao CLI E2E ===\n--- {} passed, {} failed ---",
passed, failed
);
assert!(
passed >= 5,
"too few CLI E2E sub-assertions passed: {}/9",
passed
);
assert_eq!(
failed, 0,
"{} CLI E2E sub-assertions failed — see stderr above",
failed
);
}
#[test]
fn bao_cli_browser_subcommand_starts() {
if std::env::var("BAO_TEST_NETWORK").as_deref() != Ok("1") {
eprintln!("[skip] 环境不可用: BAO_TEST_NETWORK=1 not set (bao browser subcommand E2E)");
return;
}
let bao = bao_path().expect("bao binary not found");
let port = pick_free_port();
let mut cmd = Command::new(&bao);
cmd.args(["browser", "--headless", "--cdp-port", &port.to_string()])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn().expect("spawn bao browser");
std::thread::sleep(Duration::from_secs(5));
let connected = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok();
let _ = child.kill();
let _ = child.wait();
assert!(connected, "bao browser --cdp-port {} must listen", port);
}
fn pick_free_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.and_then(|l| l.local_addr())
.map(|a| a.port())
.unwrap_or(9922)
}