use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_hprof-analyzer");
fn philosophers() -> Option<String> {
let p = format!(
"{}/tests/fixtures/dump_4_philosophers.hprof",
env!("CARGO_MANIFEST_DIR")
);
match std::fs::metadata(&p) {
Ok(m) if m.len() >= 1024 => Some(p),
_ => None,
}
}
#[test]
fn bare_path_hprof_analyzes() {
let Some(hprof) = philosophers() else { return };
let out = Command::new(BIN).arg(&hprof).output().unwrap();
assert!(
out.status.success(),
"bare-path analyze failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let md = String::from_utf8_lossy(&out.stdout);
assert!(md.contains("## System Overview"), "missing System Overview");
}
#[test]
fn bare_path_json_rerenders() {
let Some(hprof) = philosophers() else { return };
let json = Command::new(BIN)
.arg(&hprof)
.args(["--format", "json"])
.output()
.unwrap();
assert!(
json.status.success(),
"setup analyze→json failed: {}",
String::from_utf8_lossy(&json.stderr)
);
let tmp = std::env::temp_dir().join(format!("hprof_cli_{}.json", std::process::id()));
std::fs::write(&tmp, &json.stdout).unwrap();
let out = Command::new(BIN).arg(&tmp).output().unwrap();
let _ = std::fs::remove_file(&tmp);
assert!(
out.status.success(),
"bare-path re-render failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let md = String::from_utf8_lossy(&out.stdout);
assert!(
md.contains("## System Overview"),
"re-render missing sections"
);
}
#[test]
fn analyze_flag_on_json_errors() {
let Some(hprof) = philosophers() else { return };
let json = Command::new(BIN)
.arg(&hprof)
.args(["--format", "json"])
.output()
.unwrap();
assert!(
json.status.success(),
"setup analyze→json failed: {}",
String::from_utf8_lossy(&json.stderr)
);
let tmp = std::env::temp_dir().join(format!("hprof_cli_flag_{}.json", std::process::id()));
std::fs::write(&tmp, &json.stdout).unwrap();
let out = Command::new(BIN)
.arg(&tmp)
.arg("--collections")
.output()
.unwrap();
let _ = std::fs::remove_file(&tmp);
assert!(!out.status.success(), "--collections on JSON should fail");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("--collections has no effect"),
"missing hint, got: {err}"
);
}
#[test]
fn help_has_no_analyze_or_render_subcommands() {
let out = Command::new(BIN).arg("--help").output().unwrap();
assert!(out.status.success());
let help = String::from_utf8_lossy(&out.stdout);
for line in help.lines() {
let t = line.trim_start();
assert!(
!t.starts_with("analyze") && !t.starts_with("render"),
"help still lists a removed subcommand: {line}"
);
}
assert!(
help.contains("compare"),
"compare subcommand missing from help"
);
assert!(
help.contains("completions"),
"completions missing from help"
);
}
fn analyze_to_json(hprof: &str, dest: &std::path::Path) {
let json = Command::new(BIN)
.arg(hprof)
.args(["--format", "json"])
.output()
.unwrap();
assert!(
json.status.success(),
"setup analyze→json failed: {}",
String::from_utf8_lossy(&json.stderr)
);
std::fs::write(dest, &json.stdout).unwrap();
}
#[test]
fn stdin_dash_rerenders_json() {
let Some(hprof) = philosophers() else { return };
let tmp = std::env::temp_dir().join(format!("hprof_cli_stdin_{}.json", std::process::id()));
analyze_to_json(&hprof, &tmp);
let json = std::fs::File::open(&tmp).unwrap();
let out = Command::new(BIN).arg("-").stdin(json).output().unwrap();
let _ = std::fs::remove_file(&tmp);
assert!(
out.status.success(),
"stdin re-render failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let md = String::from_utf8_lossy(&out.stdout);
assert!(
md.contains("## System Overview"),
"stdin re-render missing sections"
);
}
#[test]
fn misnamed_json_dot_hprof_hints() {
let Some(hprof) = philosophers() else { return };
let tmp = std::env::temp_dir().join(format!("hprof_cli_misnamed_{}.hprof", std::process::id()));
analyze_to_json(&hprof, &tmp);
let out = Command::new(BIN).arg(&tmp).output().unwrap();
let _ = std::fs::remove_file(&tmp);
assert!(
!out.status.success(),
"misnamed .hprof JSON should fail to analyze"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("does not start with the HPROF magic"),
"missing misnamed-report hint, got: {err}"
);
}
#[test]
fn bare_path_hprof_gz_analyzes() {
use flate2::{Compression, write::GzEncoder};
use std::io::Write;
let Some(hprof) = philosophers() else { return };
let raw = std::fs::read(&hprof).unwrap();
let tmp = std::env::temp_dir().join(format!("hprof_cli_{}.hprof.gz", std::process::id()));
let mut enc = GzEncoder::new(Vec::new(), Compression::fast());
enc.write_all(&raw).unwrap();
std::fs::write(&tmp, enc.finish().unwrap()).unwrap();
let out = Command::new(BIN).arg(&tmp).output().unwrap();
let _ = std::fs::remove_file(&tmp);
assert!(
out.status.success(),
"bare-path .hprof.gz analyze failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let md = String::from_utf8_lossy(&out.stdout);
assert!(
md.contains("## System Overview"),
"gz analyze missing System Overview"
);
}
#[test]
fn rerender_html_input_hints_html() {
let Some(hprof) = philosophers() else { return };
let tmp = std::env::temp_dir().join(format!("hprof_cli_{}.html", std::process::id()));
let setup = Command::new(BIN)
.arg(&hprof)
.arg(&tmp)
.args(["--format", "html"])
.output()
.unwrap();
assert!(
setup.status.success(),
"setup analyze→html failed: {}",
String::from_utf8_lossy(&setup.stderr)
);
let out = Command::new(BIN).arg(&tmp).output().unwrap();
let _ = std::fs::remove_file(&tmp);
assert!(!out.status.success(), "HTML re-render input should fail");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("rendered HTML report"),
"missing HTML re-render hint, got: {err}"
);
}
#[test]
fn rerender_gz_markdown_input_hints_markdown() {
let Some(hprof) = philosophers() else { return };
let tmp = std::env::temp_dir().join(format!("hprof_cli_{}.md.gz", std::process::id()));
let setup = Command::new(BIN).arg(&hprof).arg(&tmp).output().unwrap();
assert!(
setup.status.success(),
"setup analyze→md.gz failed: {}",
String::from_utf8_lossy(&setup.stderr)
);
let out = Command::new(BIN).arg(&tmp).output().unwrap();
let _ = std::fs::remove_file(&tmp);
assert!(!out.status.success(), "gz-md re-render input should fail");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("rendered Markdown report"),
"missing Markdown re-render hint, got: {err}"
);
}
#[test]
fn truncated_dump_hints_corrupt() {
let Some(hprof) = philosophers() else { return };
let raw = std::fs::read(&hprof).unwrap();
assert!(raw.len() > 5000, "fixture unexpectedly small");
let tmp = std::env::temp_dir().join(format!("hprof_trunc_{}.hprof", std::process::id()));
std::fs::write(&tmp, &raw[..5000]).unwrap();
let out = Command::new(BIN).arg(&tmp).output().unwrap();
let _ = std::fs::remove_file(&tmp);
assert!(!out.status.success(), "truncated dump should fail");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("truncated or corrupt"),
"missing truncated-dump hint, got: {err}"
);
}
#[test]
fn dup_strings_markdown_has_no_control_bytes() {
let Some(hprof) = philosophers() else { return };
let out = Command::new(BIN)
.arg(&hprof)
.arg("--dup-strings")
.output()
.unwrap();
assert!(
out.status.success(),
"dup-strings analyze failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let bad = out
.stdout
.iter()
.filter(|&&b| (b < 0x20 && b != b'\t' && b != b'\n' && b != b'\r') || b == 0x7f)
.count();
assert_eq!(bad, 0, "dup-strings Markdown leaked {bad} control byte(s)");
}