use std::path::PathBuf;
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_cu-profiler");
fn scratch_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("cu-profiler-it-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn run(dir: &PathBuf, args: &[&str]) -> std::process::Output {
Command::new(BIN)
.args(args)
.current_dir(dir)
.output()
.expect("binary runs")
}
#[test]
fn init_then_run_reports_table_and_exits_zero() {
let dir = scratch_dir("run");
let init = run(&dir, &["init"]);
assert!(init.status.success(), "init failed: {init:?}");
let out = run(&dir, &["run"]);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("swap_exact_in"), "stdout: {stdout}");
assert!(stdout.contains("Status"));
}
#[test]
fn baseline_save_then_regression_compare_exits_one() {
let dir = scratch_dir("regress");
assert!(run(&dir, &["init"]).status.success());
assert!(run(&dir, &["baseline", "save"]).status.success());
let log = dir.join(".cu/logs/swap_exact_in.log");
let text = std::fs::read_to_string(&log)
.unwrap()
.replace("96812", "120000");
std::fs::write(&log, text).unwrap();
let out = run(&dir, &["compare"]);
assert_eq!(
out.status.code(),
Some(1),
"expected budget/regression exit code 1"
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("FAIL"));
}
#[test]
fn compare_without_baseline_is_missing_baseline_exit_four() {
let dir = scratch_dir("nobaseline");
assert!(run(&dir, &["init"]).status.success());
let out = run(&dir, &["compare"]);
assert_eq!(
out.status.code(),
Some(4),
"expected missing-baseline exit code 4"
);
}
#[test]
fn missing_config_is_config_error_exit_two() {
let dir = scratch_dir("noconfig");
let out = run(&dir, &["run"]);
assert_eq!(
out.status.code(),
Some(2),
"expected config error exit code 2"
);
}
#[test]
fn json_output_then_inspect_round_trips() {
let dir = scratch_dir("inspect");
assert!(run(&dir, &["init"]).status.success());
let out = run(
&dir,
&["run", "--format", "json", "--output", "report.json"],
);
assert!(out.status.success());
let inspect = run(&dir, &["inspect", "report.json"]);
assert!(inspect.status.success());
let stdout = String::from_utf8_lossy(&inspect.stdout);
assert!(stdout.contains("swap_exact_in"));
}
#[test]
fn demo_fixtures_warn_on_stderr_not_stdout() {
let dir = scratch_dir("demo-warn");
assert!(run(&dir, &["init"]).status.success());
let out = run(&dir, &["run"]);
assert!(out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stderr.contains("DEMO"),
"expected demo warning on stderr: {stderr}"
);
assert!(
!stdout.contains("DEMO"),
"warning leaked into stdout: {stdout}"
);
}
#[test]
fn real_logs_emit_no_demo_warning() {
let dir = scratch_dir("real-nowarn");
assert!(run(&dir, &["init"]).status.success());
std::fs::write(
dir.join(".cu/logs/swap_exact_in.log"),
"Program P invoke [1]\nProgram P consumed 1234 of 200000 compute units\nProgram P success\n",
)
.unwrap();
let out = run(&dir, &["run", "--scenario", "swap_exact_in"]);
assert!(out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("DEMO"),
"unexpected demo warning for real logs: {stderr}"
);
}
#[test]
fn live_mode_warns_but_still_runs() {
let dir = scratch_dir("livemode");
assert!(run(&dir, &["init"]).status.success());
let cfg = dir.join("cu-profiler.toml");
let text = std::fs::read_to_string(&cfg)
.unwrap()
.replace("mode = \"recorded\"", "mode = \"program-test\"");
std::fs::write(&cfg, text).unwrap();
let out = run(&dir, &["run"]);
assert!(out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("program-test") && stderr.contains("not executed by the CLI"),
"expected live-mode note on stderr: {stderr}"
);
assert!(!String::from_utf8_lossy(&out.stdout).contains("not executed"));
}
#[test]
fn import_real_tx_json_then_run_measures_it() {
let dir = scratch_dir("import");
assert!(run(&dir, &["init"]).status.success());
let tx = r#"{"result":{"meta":{"logMessages":[
"Program Vote111 invoke [1]",
"Program Vote111 consumed 4321 of 200000 compute units",
"Program Vote111 success"
]}}}"#;
std::fs::write(dir.join("tx.json"), tx).unwrap();
let imp = run(&dir, &["import", "tx.json", "--name", "real_vote"]);
assert!(imp.status.success(), "import failed: {imp:?}");
assert!(dir.join(".cu/logs/real_vote.log").exists());
let cfg = dir.join("cu-profiler.toml");
let mut text = std::fs::read_to_string(&cfg).unwrap();
text.push_str("\n[scenario.real_vote]\nbudget = 200000\n");
std::fs::write(&cfg, text).unwrap();
let out = run(&dir, &["run", "--scenario", "real_vote"]);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("4,321"),
"expected imported CU in report: {stdout}"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("DEMO"),
"unexpected demo warning: {stderr}"
);
}
#[test]
fn import_rejects_path_traversal_name() {
let dir = scratch_dir("traversal");
assert!(run(&dir, &["init"]).status.success());
std::fs::write(
dir.join("tx.json"),
r#"{"result":{"meta":{"logMessages":["Program P invoke [1]","Program P success"]}}}"#,
)
.unwrap();
let out = run(&dir, &["import", "tx.json", "--name", "../../ESCAPED"]);
assert!(!out.status.success(), "traversal name should be rejected");
assert!(String::from_utf8_lossy(&out.stderr).contains("invalid"));
assert!(!dir.parent().unwrap().join("ESCAPED.log").exists());
}
#[test]
fn run_rejects_path_traversal_scenario_name() {
let dir = scratch_dir("traversal-cfg");
assert!(run(&dir, &["init"]).status.success());
let cfg = dir.join("cu-profiler.toml");
let mut text = std::fs::read_to_string(&cfg).unwrap();
text.push_str("\n[scenario.\"../../../etc/evil\"]\nbudget = 100000\n");
std::fs::write(&cfg, text).unwrap();
let out = run(&dir, &["run", "--scenario", "../../../etc/evil"]);
assert!(
!out.status.success(),
"traversal scenario name should be rejected"
);
assert!(String::from_utf8_lossy(&out.stderr).contains("invalid"));
}
#[cfg(feature = "remote")]
fn serve_once_http(body: &'static str) -> (String, std::thread::JoinHandle<()>) {
use std::io::{Read, Write};
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local server");
let url = format!("http://{}", listener.local_addr().unwrap());
let handle = std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut buf = [0u8; 4096];
let _ = stream.read(&mut buf); let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = stream.write_all(response.as_bytes());
let _ = stream.flush();
}
});
(url, handle)
}
#[cfg(feature = "remote")]
#[test]
fn import_signature_fetches_logs_over_http_then_runs() {
let dir = scratch_dir("import-rpc");
assert!(run(&dir, &["init"]).status.success());
let response = r#"{"jsonrpc":"2.0","id":1,"result":{"meta":{"logMessages":[
"Program Vote111 invoke [1]",
"Program Vote111 consumed 7777 of 200000 compute units",
"Program Vote111 success"
]}}}"#;
let (url, handle) = serve_once_http(response);
let out = run(
&dir,
&[
"import",
"--signature",
"TestSig1111111111111111111111111111",
"--rpc",
&url,
"--name",
"live_tx",
],
);
handle.join().unwrap();
assert!(out.status.success(), "import --signature failed: {out:?}");
assert!(dir.join(".cu/logs/live_tx.log").exists());
let cfg = dir.join("cu-profiler.toml");
let mut text = std::fs::read_to_string(&cfg).unwrap();
text.push_str("\n[scenario.live_tx]\nbudget = 200000\n");
std::fs::write(&cfg, text).unwrap();
let report = run(&dir, &["run", "--scenario", "live_tx"]);
assert!(report.status.success());
let stdout = String::from_utf8_lossy(&report.stdout);
assert!(
stdout.contains("7,777"),
"expected fetched CU in report: {stdout}"
);
}
#[cfg(feature = "remote")]
#[test]
fn import_signature_reports_not_found() {
let dir = scratch_dir("import-rpc-404");
assert!(run(&dir, &["init"]).status.success());
let (url, handle) = serve_once_http(r#"{"jsonrpc":"2.0","id":1,"result":null}"#);
let out = run(
&dir,
&["import", "--signature", "Missing111", "--rpc", &url],
);
handle.join().unwrap();
assert!(!out.status.success(), "expected failure for missing tx");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("not found"), "stderr: {stderr}");
}
#[cfg(feature = "anchor")]
#[test]
fn anchor_idl_labels_program_in_report() {
let dir = scratch_dir("anchor");
assert!(run(&dir, &["init"]).status.success());
std::fs::write(
dir.join("amm.idl.json"),
r#"{"address":"SwapPRogram1111111111111111111111111111","metadata":{"name":"amm"},"instructions":[],"errors":[]}"#,
)
.unwrap();
let cfg = dir.join("cu-profiler.toml");
let mut text = std::fs::read_to_string(&cfg).unwrap();
text.push_str("\n[anchor]\nidl = \"amm.idl.json\"\n");
std::fs::write(&cfg, text).unwrap();
let out = run(
&dir,
&["run", "--scenario", "swap_exact_in", "--format", "json"],
);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("\"label\": \"amm\""),
"IDL label missing:\n{stdout}"
);
}