#[path = "../src/md_test.rs"]
mod md_test;
use md_test::Md;
#[test]
fn end_to_end_dump0() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/dump_0_fj-kmeans.hprof"
);
match std::fs::metadata(path) {
Ok(m) if m.len() >= 1024 => {}
_ => return,
}
let out = std::process::Command::new(env!("CARGO_BIN_EXE_hprof-analyzer"))
.arg(path)
.output()
.expect("failed to run hprof-analyzer");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let md = String::from_utf8_lossy(&out.stdout);
let doc = Md::parse(&md);
assert_eq!(
doc.heading("System Overview").map(|h| h.level()),
Some(2),
"missing System Overview (H2)"
);
assert_eq!(
doc.heading("Leak Suspects").map(|h| h.level()),
Some(2),
"missing Leak Suspects (H2)"
);
assert_eq!(
doc.heading("Top Consumers").map(|h| h.level()),
Some(2),
"missing Top Consumers (H2)"
);
assert_eq!(
doc.heading("Heap Summary").map(|h| h.level()),
Some(3),
"missing Heap Summary (H3)"
);
assert_eq!(
doc.heading("Class Histogram").map(|h| h.level()),
Some(3),
"missing Class Histogram (H3)"
);
assert_eq!(
doc.heading("Biggest Objects").map(|h| h.level()),
Some(3),
"missing Biggest Objects (H3)"
);
assert_eq!(
doc.heading("Biggest Classes").map(|h| h.level()),
Some(3),
"missing Biggest Classes (H3)"
);
assert_eq!(
doc.heading("Biggest Packages").map(|h| h.level()),
Some(3),
"missing Biggest Packages (H3)"
);
let sys = doc.section("System Overview").unwrap();
assert!(
sys.body_contains("### Heap Summary"),
"Heap Summary should be nested under System Overview"
);
let hist = doc
.section("Class Histogram")
.expect("Class Histogram section");
let table = hist.table(0).expect("Class Histogram renders a table");
assert!(
table.has_column("Class"),
"histogram table should have a Class column, got {:?}",
table.columns()
);
assert!(
table.has_column("Retained Heap"),
"histogram table should have a Retained Heap column"
);
}
fn normalize_nondeterministic(v: &mut serde_json::Value) {
if let Some(obj) = v.as_object_mut() {
if obj.contains_key("generated") {
obj["generated"] = serde_json::Value::Null;
}
if let Some(ov) = obj.get_mut("overview").and_then(|o| o.as_object_mut()) {
if ov.contains_key("file_path") {
ov["file_path"] = serde_json::Value::Null;
}
}
}
}
#[test]
fn json_golden_snapshot() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
let hprof = format!("{dir}/dump_4_philosophers.hprof");
let golden_path = format!("{dir}/dump_4_philosophers_report.json");
match std::fs::metadata(&hprof) {
Ok(m) if m.len() >= 1024 => {}
_ => return,
}
let out = std::process::Command::new(env!("CARGO_BIN_EXE_hprof-analyzer"))
.arg(&hprof)
.arg("--format")
.arg("json")
.output()
.expect("failed to run hprof-analyzer");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let mut got: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("analyzer stdout was not valid JSON");
let golden_text = std::fs::read_to_string(&golden_path)
.unwrap_or_else(|e| panic!("cannot read golden {golden_path}: {e}"));
let mut want: serde_json::Value =
serde_json::from_str(&golden_text).expect("golden fixture was not valid JSON");
normalize_nondeterministic(&mut got);
normalize_nondeterministic(&mut want);
assert_eq!(
got, want,
"JSON report drifted from the golden snapshot at {golden_path}. If this \
change is intended, regenerate the golden with:\n \
cargo run --release -- analyze tests/fixtures/dump_4_philosophers.hprof \
--format json > tests/fixtures/dump_4_philosophers_report.json"
);
}
#[test]
fn json_gzip_roundtrip() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
let hprof = format!("{dir}/dump_4_philosophers.hprof");
match std::fs::metadata(&hprof) {
Ok(m) if m.len() >= 1024 => {}
_ => return,
}
let bin = env!("CARGO_BIN_EXE_hprof-analyzer");
let tmp = std::env::temp_dir().join(format!("hprof_gz_roundtrip_{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let gz_path = tmp.join("report.json.gz");
let status = std::process::Command::new(bin)
.arg(&hprof)
.arg("--format")
.arg("json")
.arg(&gz_path)
.status()
.expect("failed to run analyzer");
assert!(status.success(), "analyze to .json.gz exited non-zero");
let raw = std::fs::read(&gz_path).unwrap();
assert!(
raw.starts_with(&[0x1f, 0x8b]),
"output .json.gz is not gzip-compressed (magic {:x?})",
&raw[..raw.len().min(2)]
);
let out = std::process::Command::new(bin)
.arg(&gz_path)
.arg("--format")
.arg("json")
.output()
.expect("failed to run render");
assert!(
out.status.success(),
"render .json.gz stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let mut got: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("render output was not valid JSON");
let plain = std::process::Command::new(bin)
.arg(&hprof)
.arg("--format")
.arg("json")
.output()
.expect("failed to run analyzer (plain)");
assert!(plain.status.success());
let mut want: serde_json::Value =
serde_json::from_slice(&plain.stdout).expect("plain output was not valid JSON");
normalize_nondeterministic(&mut got);
normalize_nondeterministic(&mut want);
let _ = std::fs::remove_dir_all(&tmp);
assert_eq!(
got, want,
"rendered .json.gz did not match a plain JSON render of the same dump"
);
}