use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_info_command() {
let mut cmd = Command::cargo_bin("oinkie").unwrap();
cmd.arg("info")
.assert()
.success()
.stdout(predicate::str::contains("Oinkie Info"))
.stdout(predicate::str::contains("Birthmarks"))
.stdout(predicate::str::contains("Compare Algorithms"));
}
#[test]
#[serial(ghidra)]
fn test_lift_command() {
let temp_dir = tempdir().unwrap();
let dest = temp_dir.path().join("lifted");
let mut cmd = Command::cargo_bin("oinkie").unwrap();
let result = cmd
.arg("lift")
.arg("-d")
.arg(&dest)
.arg("-r")
.arg("ghidra-pcode")
.arg("testdata/bin/hello_clang")
.assert();
result.success();
let out_file = dest.join("hello_clang.json");
assert!(out_file.exists(), "hello_clang.json was not generated");
}
#[test]
fn test_the_lifter_home_is_read_from_the_environment() {
let temp_dir = tempdir().unwrap();
let dest = temp_dir.path().join("lifted");
Command::cargo_bin("oinkie")
.unwrap()
.env("GHIDRA_HOME", "/oinkie-no-such-ghidra")
.arg("lift")
.arg("-d")
.arg(&dest)
.arg("testdata/bin/hello_clang")
.assert()
.failure()
.stderr(
predicate::str::contains("oinkie-no-such-ghidra")
.and(predicate::str::contains("analyzeHeadless")),
);
}
#[test]
fn test_a_usage_error_is_not_prefixed_twice() {
Command::cargo_bin("oinkie")
.unwrap()
.arg("run")
.arg("--bogus-flag")
.assert()
.failure()
.stderr(predicate::str::starts_with("error: "))
.stderr(predicate::str::contains("Error: error:").not());
let temp_dir = tempdir().unwrap();
Command::cargo_bin("oinkie")
.unwrap()
.arg("extract")
.arg("-d")
.arg(temp_dir.path().join("birthmarks"))
.arg("no-such-file.json")
.assert()
.failure()
.stderr(predicate::str::contains("Error: IO error for"));
}
#[test]
fn test_extract_command() {
let temp_dir = tempdir().unwrap();
let dest = temp_dir.path().join("birthmarks");
let mut cmd = Command::cargo_bin("oinkie").unwrap();
cmd.arg("extract")
.arg("-d")
.arg(&dest)
.arg("-b")
.arg("op-seq")
.arg("testdata/lifted/pcodes/hello_clang.json")
.arg("testdata/lifted/pcodes/hello_gcc.json")
.assert()
.success();
let entries: Vec<_> = fs::read_dir(&dest)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
assert_eq!(
entries.len(),
2,
"Expected 2 birthmark files to be generated"
);
}
#[test]
fn test_extract_names_a_kgram_the_way_the_library_does() {
let temp_dir = tempdir().unwrap();
let dest = temp_dir.path().join("birthmarks");
Command::cargo_bin("oinkie")
.unwrap()
.arg("extract")
.arg("-d")
.arg(&dest)
.arg("-b")
.arg("op-3gram-set")
.arg("testdata/lifted/pcodes/hello_clang.json")
.assert()
.success();
assert_eq!(fs::read_dir(&dest).unwrap().count(), 1);
Command::cargo_bin("oinkie")
.unwrap()
.arg("extract")
.arg("-d")
.arg(temp_dir.path().join("unused"))
.arg("-b")
.arg("op-tri-gram-set")
.arg("testdata/lifted/pcodes/hello_clang.json")
.assert()
.failure()
.stderr(predicate::str::contains("unknown birthmark type"));
}
#[test]
fn test_a_kgram_frequency_birthmark_can_be_written_and_read_back() {
let temp_dir = tempdir().unwrap();
let birthmarks = temp_dir.path().join("birthmarks");
let through_a_file = temp_dir.path().join("through-a-file");
let in_memory = temp_dir.path().join("in-memory");
let inputs = [
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/hello_gcc.json",
];
Command::cargo_bin("oinkie")
.unwrap()
.args(["extract", "-b", "op-2gram-freq", "-d"])
.arg(&birthmarks)
.args(inputs)
.assert()
.success();
let written: Vec<_> = fs::read_dir(&birthmarks)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
assert_eq!(written.len(), 2, "the birthmarks were not written");
assert!(
fs::read_to_string(&written[0])
.unwrap()
.contains("KgramFreq"),
"the file does not hold what was asked for"
);
Command::cargo_bin("oinkie")
.unwrap()
.args(["compare", "-a", "cosine", "-d"])
.arg(&through_a_file)
.args(&written)
.assert()
.success();
Command::cargo_bin("oinkie")
.unwrap()
.args(["run", "-a", "op-2gram-freq-cosine", "-d"])
.arg(&in_memory)
.args(inputs)
.assert()
.success();
let scores = |dir: &std::path::Path| {
let csv = fs::read_to_string(dir.join("results.csv")).unwrap();
let mut rows: Vec<String> = csv
.lines()
.filter(|l| !l.starts_with("total duration,"))
.map(|l| l.rsplit_once(',').unwrap().0.to_string())
.collect();
rows.sort();
assert!(!rows.is_empty(), "no scores in {}", dir.display());
rows
};
assert_eq!(scores(&through_a_file), scores(&in_memory));
}
#[test]
fn test_compare_command() {
let temp_dir = tempdir().unwrap();
let birthmarks_dir = temp_dir.path().join("birthmarks");
let similarities_dir = temp_dir.path().join("similarities");
Command::cargo_bin("oinkie")
.unwrap()
.arg("extract")
.arg("-d")
.arg(&birthmarks_dir)
.arg("-b")
.arg("op-seq")
.arg("testdata/lifted/pcodes/hello_clang.json")
.arg("testdata/lifted/pcodes/hello_gcc.json")
.assert()
.success();
let entries: Vec<_> = fs::read_dir(&birthmarks_dir)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
let mut cmd = Command::cargo_bin("oinkie").unwrap();
cmd.arg("compare")
.arg("-d")
.arg(&similarities_dir)
.arg("-a")
.arg("jaccard")
.arg("-A")
.arg("hungarian")
.arg("-s")
.arg("all")
.args(&entries)
.assert()
.success();
let sim_entries: Vec<_> = fs::read_dir(&similarities_dir)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
assert!(
!sim_entries.is_empty(),
"Expected similarity files to be generated"
);
}
#[test]
fn test_run_command() {
let temp_dir = tempdir().unwrap();
let similarities_dir = temp_dir.path().join("similarities");
let mut cmd = Command::cargo_bin("oinkie").unwrap();
cmd.arg("run")
.arg("-a")
.arg("op-set-jaccard")
.arg("-s")
.arg("all")
.arg("-d")
.arg(&similarities_dir)
.arg("testdata/lifted/pcodes/hello_clang.json")
.arg("testdata/lifted/pcodes/hello_gcc.json")
.assert()
.success();
let sim_entries: Vec<_> = fs::read_dir(&similarities_dir)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
assert!(
!sim_entries.is_empty(),
"Expected similarity files to be generated"
);
}
fn first_score(summary: &std::path::Path) -> f64 {
let content =
fs::read_to_string(summary).unwrap_or_else(|e| panic!("{}: {e}", summary.display()));
let first = content.lines().next().expect("an empty summary");
first.split(',').nth(1).unwrap().parse().unwrap()
}
#[test]
fn test_run_scores_what_extract_and_compare_score() {
let inputs = [
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/udl.json",
];
for (birthmark_type, algorithm) in [
("op-seq", "levenshtein"),
("fc-seq", "levenshtein"),
("op-3gram-seq", "levenshtein"),
("fc-set", "jaccard"),
("op-2gram-set", "jaccard"),
("fc-freq", "cosine"),
("op-2gram-freq", "weighted-jaccard"),
("op-freq", "jensen-shannon"),
("fc-freq", "tanimoto"),
] {
let analysis = format!("{birthmark_type}-{algorithm}");
let dir = tempdir().unwrap();
Command::cargo_bin("oinkie")
.unwrap()
.args(["run", "-a", &analysis, "-s", "all", "-d"])
.arg(dir.path().join("run"))
.args(inputs)
.assert()
.success();
let birthmarks = dir.path().join("birthmarks");
Command::cargo_bin("oinkie")
.unwrap()
.args(["extract", "-b", birthmark_type, "-d"])
.arg(&birthmarks)
.args(inputs)
.assert()
.success();
let mut extracted = fs::read_dir(&birthmarks)
.unwrap()
.map(|e| e.unwrap().path())
.collect::<Vec<_>>();
extracted.sort_by_key(|p| {
!p.file_name()
.unwrap()
.to_string_lossy()
.starts_with("hello_clang")
});
Command::cargo_bin("oinkie")
.unwrap()
.args(["compare", "-a", algorithm, "-s", "all", "-d"])
.arg(dir.path().join("compare"))
.args(&extracted)
.assert()
.success();
let run = first_score(&dir.path().join("run").join("results.csv"));
let compare = first_score(&dir.path().join("compare").join("results.csv"));
assert_eq!(
run, compare,
"{analysis}: run and extract + compare disagree"
);
}
}
#[test]
fn test_a_zero_gram_analysis_is_refused_not_a_panic() {
let dir = tempdir().unwrap();
Command::cargo_bin("oinkie")
.unwrap()
.args(["run", "-a", "op-0gram-set-jaccard", "-s", "all", "-d"])
.arg(dir.path())
.args([
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/udl.json",
])
.assert()
.failure()
.code(1)
.stderr(predicates::str::contains("op-0gram-set"))
.stderr(predicates::str::contains("panicked").not());
}
fn side_birthmark(pair_csv: &std::path::Path, side: &str) -> std::path::PathBuf {
let content = fs::read_to_string(pair_csv).unwrap();
let line = content
.lines()
.find(|l| l.starts_with(&format!("{side},birthmark,")))
.unwrap_or_else(|| panic!("no {side} birthmark in {}", pair_csv.display()));
std::path::PathBuf::from(line.rsplit(',').next().unwrap())
}
#[test]
fn test_run_writes_the_birthmarks_it_compares() {
let dir = tempdir().unwrap();
let dest = dir.path().join("out");
let inputs = [
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/hello_gcc.json",
"testdata/lifted/pcodes/udl.json",
];
Command::cargo_bin("oinkie")
.unwrap()
.args(["run", "-a", "fc-set-jaccard", "-s", "all", "-d"])
.arg(&dest)
.args(inputs)
.assert()
.success();
let written = fs::read_dir(dest.join("birthmarks"))
.expect("no birthmarks directory")
.map(|e| e.unwrap().path())
.collect::<Vec<_>>();
assert_eq!(
written.len(),
inputs.len(),
"one birthmark per input: {written:?}"
);
for pair in ["00000.csv", "00001.csv", "00002.csv"] {
for side in ["left", "right"] {
let named = side_birthmark(&dest.join(pair), side);
assert!(
written.contains(&named),
"{pair} {side}: {} was not written",
named.display()
);
let b: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&named).unwrap()).unwrap();
assert_eq!(
b["metadata"]["birthmark_type"],
"FcSet",
"{}",
named.display()
);
}
}
}
#[test]
fn test_run_skip_does_not_reuse_a_birthmark_of_another_type() {
let dir = tempdir().unwrap();
let dest = dir.path().join("out");
let inputs = [
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/udl.json",
];
let run = |analysis: &str, scores: &str| {
Command::cargo_bin("oinkie")
.unwrap()
.args(["run", "-S", "-a", analysis, "-s", "all", "-d"])
.arg(dest.join(scores))
.args(inputs)
.assert()
.success();
};
run("fc-set-jaccard", "");
run("op-set-jaccard", "");
let named = side_birthmark(&dest.join("00000.csv"), "left");
let b: serde_json::Value = serde_json::from_str(&fs::read_to_string(&named).unwrap()).unwrap();
assert_eq!(
b["metadata"]["birthmark_type"], "OpSet",
"the fc-set birthmark was reused"
);
let fresh = dir.path().join("fresh");
Command::cargo_bin("oinkie")
.unwrap()
.args(["run", "-a", "op-set-jaccard", "-s", "all", "-d"])
.arg(&fresh)
.args(inputs)
.assert()
.success();
let similarity = |pair: &std::path::Path| -> String {
let content = fs::read_to_string(pair).unwrap();
let line = content.lines().find(|l| l.starts_with("result,")).unwrap();
line.split(',').nth(2).unwrap().to_string()
};
assert_eq!(
similarity(&dest.join("00000.csv")),
similarity(&fresh.join("00000.csv")),
"the fc-set score was kept"
);
}
#[test]
fn test_run_given_an_input_twice_writes_its_birthmark_once() {
let dir = tempdir().unwrap();
let dest = dir.path().join("out");
Command::cargo_bin("oinkie")
.unwrap()
.args(["run", "-s", "all", "-d"])
.arg(&dest)
.args([
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/udl.json",
])
.assert()
.success();
let written = fs::read_dir(dest.join("birthmarks"))
.unwrap()
.map(|e| e.unwrap().path())
.collect::<Vec<_>>();
assert_eq!(written.len(), 2, "{written:?}");
for w in written {
serde_json::from_str::<serde_json::Value>(&fs::read_to_string(&w).unwrap())
.unwrap_or_else(|e| panic!("{}: {e}", w.display()));
}
}
fn fc_set_scores(dir: &std::path::Path) -> std::path::PathBuf {
let dest = dir.join("scores");
Command::cargo_bin("oinkie")
.unwrap()
.args(["run", "-a", "fc-set-jaccard", "-s", "all", "-d"])
.arg(&dest)
.args([
"testdata/lifted/pcodes/udl.json",
"testdata/lifted/pcodes/hello_clang.json",
])
.assert()
.success();
dest
}
fn review(scores: &std::path::Path, min: Option<&str>) -> (f64, Option<String>) {
let out = scores.parent().unwrap().join("review.csv");
let mut cmd = Command::cargo_bin("oinkie").unwrap();
cmd.arg("review").arg("-d").arg(&out).arg(scores);
if let Some(m) = min {
cmd.args(["--min-elements", m]);
}
cmd.assert().success();
let content = fs::read_to_string(&out).unwrap();
let last = content.lines().last().unwrap();
let recorded = last.starts_with("min elements,").then(|| last.to_string());
(first_score(&out), recorded)
}
#[test]
fn test_review_drops_the_functions_with_too_few_elements() {
let dir = tempdir().unwrap();
let scores = fc_set_scores(dir.path());
let (plain, recorded) = review(&scores, None);
assert_eq!(recorded, None, "no threshold, no line");
assert!((plain - 1.0 / 9.0).abs() < 1e-9, "{plain}");
let (half, recorded) = review(&scores, Some("0.5x"));
assert!((half - 1.0 / 3.0).abs() < 1e-9, "{half}");
assert_eq!(recorded.as_deref(), Some("min elements,0.5x,0.5"));
let (two, recorded) = review(&scores, Some("2"));
assert_eq!(two, 0.0);
assert_eq!(recorded.as_deref(), Some("min elements,2,2"));
let (four, _) = review(&scores, Some("4"));
assert_eq!(four, 1.0);
let (zero, recorded) = review(&scores, Some("0"));
assert_eq!(zero, plain);
assert_eq!(recorded.as_deref(), Some("min elements,0,0"));
}
#[test]
fn test_review_scores_under_containment_and_matched() {
let dir = tempdir().unwrap();
let scores = fc_set_scores(dir.path());
let score = |aggregator: &str| {
let out = dir
.path()
.join(format!("{}.csv", aggregator.replace(':', "_")));
Command::cargo_bin("oinkie")
.unwrap()
.args(["review", "-A", aggregator, "-d"])
.arg(&out)
.arg(&scores)
.assert()
.success();
first_score(&out)
};
assert!((score("hungarian") - 1.0 / 9.0).abs() < 1e-9);
assert!((score("containment") - 1.0 / 3.0).abs() < 1e-9);
assert!((score("matched:0.3") - 1.0 / 3.0).abs() < 1e-9);
assert_eq!(score("matched:0.5"), 0.0);
Command::cargo_bin("oinkie")
.unwrap()
.args(["review", "-A", "matched:2", "-d"])
.arg(dir.path().join("refused.csv"))
.arg(&scores)
.assert()
.failure();
}
#[test]
fn test_review_weighted_scores_what_run_weighted_scores() {
let dir = tempdir().unwrap();
let scores = dir.path().join("scores");
Command::cargo_bin("oinkie")
.unwrap()
.args([
"run",
"-a",
"op-seq-lcs",
"-A",
"weighted",
"-s",
"all",
"-d",
])
.arg(&scores)
.args([
"testdata/lifted/pcodes/udl.json",
"testdata/lifted/pcodes/hello_gcc.json",
])
.assert()
.success();
let run = first_score(&scores.join("results.csv"));
let review = |aggregator: &str| {
let out = dir.path().join(format!("{aggregator}.csv"));
Command::cargo_bin("oinkie")
.unwrap()
.args(["review", "-A", aggregator, "-d"])
.arg(&out)
.arg(&scores)
.assert()
.success();
first_score(&out)
};
assert_eq!(review("weighted"), run);
assert_ne!(
review("weighted"),
review("hungarian"),
"the sizes made no difference"
);
}
#[test]
fn test_review_min_elements_ratio_is_of_the_mean_over_the_directory() {
let dir = tempdir().unwrap();
let scores = dir.path().join("scores");
Command::cargo_bin("oinkie")
.unwrap()
.args([
"run",
"-a",
"op-seq-levenshtein",
"-s",
"first-vs-others",
"-d",
])
.arg(&scores)
.args([
"testdata/lifted/pcodes/udl.json",
"testdata/lifted/pcodes/hello_clang.json",
"testdata/lifted/pcodes/hello_gcc.json",
])
.assert()
.success();
let lengths = fs::read_dir(scores.join("birthmarks"))
.unwrap()
.flat_map(|e| {
let b: serde_json::Value =
serde_json::from_str(&fs::read_to_string(e.unwrap().path()).unwrap()).unwrap();
b["elements"]
.as_array()
.unwrap()
.iter()
.map(|f| f["data"]["Seq"].as_array().unwrap().len())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let mean = lengths.iter().sum::<usize>() as f64 / lengths.len() as f64;
assert_ne!(
mean, 1.0,
"the fixture no longer tells a ratio from a count"
);
let (_, recorded) = review(&scores, Some("1x"));
assert_eq!(recorded, Some(format!("min elements,1x,{mean}")));
}
#[test]
fn test_review_refuses_a_bare_fraction_for_min_elements() {
let dir = tempdir().unwrap();
let scores = fc_set_scores(dir.path());
Command::cargo_bin("oinkie")
.unwrap()
.args(["review", "--min-elements", "0.3"])
.arg(&scores)
.assert()
.failure()
.stderr(predicates::str::contains("write 0.3x"));
}
#[test]
fn test_review_min_elements_needs_the_birthmarks_named() {
let dir = tempdir().unwrap();
let scores = fc_set_scores(dir.path());
let pair = scores.join("00000.csv");
let content = fs::read_to_string(&pair).unwrap();
let without = content
.lines()
.map(|l| {
if l.starts_with("left,") {
format!("{},", &l[..l.rfind(',').unwrap()])
} else {
l.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
fs::write(&pair, without).unwrap();
Command::cargo_bin("oinkie")
.unwrap()
.args(["review", "-d"])
.arg(dir.path().join("r.csv"))
.args(["--min-elements", "2"])
.arg(&scores)
.assert()
.failure()
.stderr(predicates::str::contains("does not name the birthmarks"));
Command::cargo_bin("oinkie")
.unwrap()
.args(["review", "-d"])
.arg(dir.path().join("r.csv"))
.arg(&scores)
.assert()
.success();
}
#[test]
fn test_review_min_elements_refuses_a_birthmark_that_has_changed() {
let dir = tempdir().unwrap();
let scores = fc_set_scores(dir.path());
let pair = scores.join("00000.csv");
let (left, right) = (
side_birthmark(&pair, "left"),
side_birthmark(&pair, "right"),
);
fs::copy(&right, &left).unwrap();
Command::cargo_bin("oinkie")
.unwrap()
.args(["review", "-d"])
.arg(dir.path().join("r.csv"))
.args(["--min-elements", "2"])
.arg(&scores)
.assert()
.failure()
.stderr(predicates::str::contains("has changed"));
}
#[test]
fn test_review_reads_back_a_comparison_with_an_empty_birthmark() {
let dir = tempdir().unwrap();
let birthmarks = dir.path().join("birthmarks");
Command::cargo_bin("oinkie")
.unwrap()
.args(["extract", "-b", "fc-set", "-d"])
.arg(&birthmarks)
.args([
"testdata/lifted/pcodes/udl.json",
"testdata/lifted/pcodes/hello_clang.json",
])
.assert()
.success();
let some = fs::read_dir(&birthmarks)
.unwrap()
.next()
.unwrap()
.unwrap()
.path();
let mut json: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&some).unwrap()).unwrap();
json["elements"] = serde_json::json!([]);
fs::write(birthmarks.join("empty.json"), json.to_string()).unwrap();
let mut inputs = fs::read_dir(&birthmarks)
.unwrap()
.map(|e| e.unwrap().path())
.collect::<Vec<_>>();
inputs.sort();
let scores = dir.path().join("scores");
Command::cargo_bin("oinkie")
.unwrap()
.args(["compare", "-s", "all-and-self", "-d"])
.arg(&scores)
.args(&inputs)
.assert()
.success();
let out = dir.path().join("review.csv");
Command::cargo_bin("oinkie")
.unwrap()
.args(["review", "-d"])
.arg(&out)
.arg(&scores)
.assert()
.success();
let score_column = |p: &std::path::Path| {
fs::read_to_string(p)
.unwrap()
.lines()
.take_while(|l| !l.starts_with("total duration,"))
.map(|l| l.split(',').nth(1).unwrap().to_string())
.collect::<Vec<_>>()
};
let compared = score_column(&scores.join("results.csv"));
assert!(
compared.contains(&"0".to_string()) && compared.contains(&"1".to_string()),
"the fixture no longer has an empty side and two empty sides: {compared:?}"
);
assert_eq!(score_column(&out), compared);
}
#[test]
fn test_help_written_to_a_pipe_has_no_colour_codes() {
for args in [
vec!["-h"],
vec!["--help"],
vec!["review", "-h"],
vec!["compare", "--help"],
vec!["--version"],
] {
let out = Command::cargo_bin("oinkie")
.unwrap()
.args(&args)
.output()
.unwrap();
assert!(out.status.success(), "{args:?}");
assert!(!out.stdout.is_empty(), "{args:?}: nothing printed");
assert!(
!out.stdout.contains(&0x1b),
"{args:?}: an escape code in {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
}
#[test]
fn test_the_old_name_reaggregate_is_refused_naming_review() {
for args in [
vec!["reaggregate"],
vec!["reaggregate", "-A", "topn:3", "-d", "x.csv", "out/"],
] {
Command::cargo_bin("oinkie")
.unwrap()
.args(&args)
.assert()
.failure()
.stderr(predicates::str::contains("use review"));
}
}
#[test]
fn test_review_command() {
let temp_dir = tempdir().unwrap();
let birthmarks_dir = temp_dir.path().join("birthmarks");
let similarities_dir = temp_dir.path().join("similarities");
Command::cargo_bin("oinkie")
.unwrap()
.arg("extract")
.arg("-d")
.arg(&birthmarks_dir)
.arg("-b")
.arg("op-seq")
.arg("testdata/lifted/pcodes/hello_clang.json")
.arg("testdata/lifted/pcodes/hello_gcc.json")
.assert()
.success();
let entries: Vec<_> = fs::read_dir(&birthmarks_dir)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
Command::cargo_bin("oinkie")
.unwrap()
.arg("compare")
.arg("-d")
.arg(&similarities_dir)
.arg("-a")
.arg("levenshtein")
.arg("-s")
.arg("all")
.args(&entries)
.assert()
.success();
let dest_file = temp_dir.path().join("review.csv");
let mut cmd = Command::cargo_bin("oinkie").unwrap();
cmd.arg("review")
.arg("-A")
.arg("hungarian")
.arg("-d")
.arg(&dest_file)
.arg(&similarities_dir)
.assert()
.success();
assert!(dest_file.exists());
}
#[test]
#[serial(ghidra)]
fn test_lift_command_intermediate_dir_is_created_and_resolved_once() {
let temp_dir = tempfile::Builder::new()
.prefix("oinkie_test")
.tempdir()
.unwrap();
let input = fs::canonicalize("testdata/bin/hello_clang").unwrap();
let dest = temp_dir.path().join("lifted");
Command::cargo_bin("oinkie")
.unwrap()
.current_dir(temp_dir.path())
.arg("lift")
.arg("-i")
.arg("irs/nested")
.arg("-d")
.arg(&dest)
.arg(&input)
.assert()
.success();
let intermediate = temp_dir.path().join("irs/nested");
assert!(intermediate.is_dir(), "the -i directory was not created");
assert!(
!intermediate.join("irs/nested").exists(),
"the -i path was resolved twice"
);
assert!(dest.join("hello_clang.json").exists());
}
#[test]
#[serial(ghidra)]
fn test_lift_escapes_a_quote_in_a_function_name() {
let temp_dir = tempdir().unwrap();
let dest = temp_dir.path().join("lifted");
Command::cargo_bin("oinkie")
.unwrap()
.arg("lift")
.arg("-d")
.arg(&dest)
.arg("testdata/bin/udl")
.assert()
.success();
let out_file = dest.join("udl.json");
oinkie::Program::load(&out_file).expect("oinkie cannot read the file it just wrote");
let body = fs::read_to_string(&out_file).unwrap();
let json: serde_json::Value = serde_json::from_str(&body).unwrap();
let names = json["functions"]
.as_array()
.expect("no functions array")
.iter()
.map(|f| f["name"].as_str().expect("a function with no name"))
.collect::<Vec<_>>();
assert!(
names.iter().any(|n| n.contains('"')),
"the quoted name did not survive escaping: {names:?}"
);
}
#[test]
#[serial(ghidra)]
fn test_a_lift_whose_output_cannot_be_read_is_not_a_successful_lift() {
let temp_dir = tempdir().unwrap();
let dest = temp_dir.path().join("lifted");
Command::cargo_bin("oinkie")
.unwrap()
.arg("lift")
.arg("--script")
.arg("testdata/scripts/BrokenLifter.java")
.arg("-d")
.arg(&dest)
.arg("testdata/bin/hello_clang")
.assert()
.failure()
.stderr(predicate::str::contains("testdata/bin/hello_clang:"))
.stderr(predicate::str::contains("hello_clang.json"))
.stderr(predicate::str::contains("reported success"));
}
fn birthmarks_in(dir: &std::path::Path) {
Command::cargo_bin("oinkie")
.unwrap()
.arg("extract")
.arg("-d")
.arg(dir)
.arg("-b")
.arg("op-seq")
.arg("testdata/lifted/pcodes/hello_clang.json")
.arg("testdata/lifted/pcodes/hello_gcc.json")
.assert()
.success();
}
#[test]
fn test_stats_writes_a_row_per_file_as_csv() {
let temp_dir = tempdir().unwrap();
let dir = temp_dir.path().join("birthmarks");
birthmarks_in(&dir);
let out = Command::cargo_bin("oinkie")
.unwrap()
.arg("stats")
.arg("-f")
.arg("csv")
.arg("--per-file")
.arg(&dir)
.assert()
.success()
.get_output()
.stdout
.clone();
let text = String::from_utf8(out).unwrap();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(
lines[0],
"path,file_name,ir,birthmark_type,functions,empty,\
elements_min,elements_q1,elements_median,elements_q3,elements_max,\
elements_mean,elements_stddev"
);
assert_eq!(lines.len(), 3, "expected a header and two rows: {text}");
for name in ["hello_clang", "hello_gcc"] {
assert!(
lines[1..].iter().any(|l| l.contains(&format!(",{name},"))),
"no row for {name}: {text}"
);
}
}
#[test]
fn test_stats_writes_a_per_file_section_as_markdown() {
let temp_dir = tempdir().unwrap();
let dir = temp_dir.path().join("birthmarks");
birthmarks_in(&dir);
let out = Command::cargo_bin("oinkie")
.unwrap()
.arg("stats")
.arg("-f")
.arg("markdown")
.arg("--per-file")
.arg(&dir)
.assert()
.success()
.get_output()
.stdout
.clone();
let text = String::from_utf8(out).unwrap();
let per_file = text
.split_once("## Per file")
.unwrap_or_else(|| panic!("no per-file section: {text}"))
.1;
for name in ["hello_clang", "hello_gcc"] {
assert!(
per_file.contains(name),
"{name} is not in the section: {text}"
);
}
}
#[test]
fn test_stats_ranks_the_most_frequent_elements_as_csv() {
let temp_dir = tempdir().unwrap();
let dir = temp_dir.path().join("birthmarks");
birthmarks_in(&dir);
let out = Command::cargo_bin("oinkie")
.unwrap()
.arg("stats")
.arg("-f")
.arg("csv")
.arg("--top")
.arg("3")
.arg(&dir)
.assert()
.success()
.get_output()
.stdout
.clone();
let text = String::from_utf8(out).unwrap();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines[0], "ir,birthmark_type,rank,element,count");
assert_eq!(lines.len(), 4, "expected a header and three ranks: {text}");
let counts: Vec<u64> = lines[1..]
.iter()
.map(|l| l.rsplit(',').next().unwrap().parse().unwrap())
.collect();
assert!(
counts.windows(2).all(|w| w[0] >= w[1]),
"the ranks are not in descending order of count: {counts:?}"
);
}
#[test]
fn test_stats_refuses_two_csv_tables_and_says_where_to_put_them() {
let temp_dir = tempdir().unwrap();
let dir = temp_dir.path().join("birthmarks");
birthmarks_in(&dir);
Command::cargo_bin("oinkie")
.unwrap()
.arg("stats")
.arg("-f")
.arg("csv")
.arg("--per-file")
.arg("--top")
.arg("3")
.arg(&dir)
.assert()
.failure()
.stderr(
predicate::str::contains("--per-file")
.and(predicate::str::contains("-f json"))
.and(predicate::str::contains("-f markdown")),
);
}
#[test]
fn test_stats_writes_to_the_named_file_and_not_to_stdout() {
let temp_dir = tempdir().unwrap();
let dir = temp_dir.path().join("birthmarks");
birthmarks_in(&dir);
let dest = temp_dir.path().join("stats.md");
Command::cargo_bin("oinkie")
.unwrap()
.arg("stats")
.arg("-o")
.arg(&dest)
.arg(&dir)
.assert()
.success()
.stdout(predicate::str::is_empty());
let written = fs::read_to_string(&dest).expect("the report was not written");
assert!(
written.contains("# Birthmark statistics"),
"the file does not hold the report: {written}"
);
}
#[test]
fn test_stats_reports_the_groups_as_json() {
let temp_dir = tempdir().unwrap();
let dir = temp_dir.path().join("birthmarks");
birthmarks_in(&dir);
let out = Command::cargo_bin("oinkie")
.unwrap()
.arg("stats")
.arg("-f")
.arg("json")
.arg(&dir)
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&out).expect("the report is not JSON");
let groups = json["groups"].as_array().expect("no groups array");
assert_eq!(
groups.len(),
1,
"two files of one type are one group: {json}"
);
assert_eq!(groups[0]["ir"], "ghidra-pcode");
assert_eq!(groups[0]["birthmark_type"], "op-seq");
assert_eq!(groups[0]["files"], 2);
assert_eq!(groups[0]["elements"]["min"], 4);
assert!(
groups[0]["occurrences"].is_null(),
"a sequence has no occurrence total: {json}"
);
assert!(json["skipped"].as_array().unwrap().is_empty());
}
#[test]
fn test_stats_totals_the_counts_of_a_freq_birthmark() {
let temp_dir = tempdir().unwrap();
let dir = temp_dir.path().join("birthmarks");
Command::cargo_bin("oinkie")
.unwrap()
.arg("extract")
.arg("-d")
.arg(&dir)
.arg("-b")
.arg("op-freq")
.arg("testdata/lifted/pcodes/hello_clang.json")
.assert()
.success();
let out = Command::cargo_bin("oinkie")
.unwrap()
.arg("stats")
.arg("-f")
.arg("json")
.arg(&dir)
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&out).unwrap();
let group = &json["groups"][0];
assert_eq!(group["birthmark_type"], "op-freq");
let vocabulary = group["vocabulary"].as_u64().expect("no vocabulary");
let occurrences = group["occurrences"]
.as_u64()
.unwrap_or_else(|| panic!("a freq birthmark has an occurrence total: {json}"));
assert!(
occurrences >= vocabulary,
"fewer occurrences than distinct elements: {occurrences} < {vocabulary}"
);
}
#[test]
fn test_stats_descends_only_when_asked() {
let temp_dir = tempdir().unwrap();
let flat = temp_dir.path().join("birthmarks");
birthmarks_in(&flat);
let nested = temp_dir.path().join("nested");
let deeper = nested.join("deeper");
fs::create_dir_all(&deeper).unwrap();
for entry in fs::read_dir(&flat).unwrap() {
let path = entry.unwrap().path();
let name = path.file_name().unwrap();
let to = if name.to_string_lossy().starts_with("hello_gcc") {
deeper.join(name)
} else {
nested.join(name)
};
fs::copy(&path, &to).unwrap();
}
let files_seen = |recursive: bool| {
let mut cmd = Command::cargo_bin("oinkie").unwrap();
cmd.arg("stats").arg("-f").arg("json");
if recursive {
cmd.arg("-r");
}
let out = cmd
.arg(&nested)
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&out).unwrap();
json["groups"][0]["files"].as_u64().unwrap()
};
assert_eq!(files_seen(false), 1, "the subdirectory was read without -r");
assert_eq!(files_seen(true), 2, "-r did not reach the subdirectory");
}
#[test]
fn test_stats_counts_what_it_could_not_read() {
let temp_dir = tempdir().unwrap();
let dir = temp_dir.path().join("birthmarks");
birthmarks_in(&dir);
fs::write(dir.join("junk.json"), r#"{"not":"a birthmark"}"#).unwrap();
let out = Command::cargo_bin("oinkie")
.unwrap()
.arg("stats")
.arg("-f")
.arg("json")
.arg(&dir)
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&out).unwrap();
let skipped = json["skipped"].as_array().expect("no skipped array");
assert_eq!(
skipped.len(),
1,
"the unreadable file was not counted: {json}"
);
assert!(
skipped[0]["path"].as_str().unwrap().ends_with("junk.json"),
"the skip does not name the file: {json}"
);
assert_eq!(
json["groups"][0]["files"], 2,
"the readable files were affected: {json}"
);
}
#[test]
#[serial(ghidra)]
fn test_a_lift_that_found_no_functions_warns_and_succeeds() {
let temp_dir = tempdir().unwrap();
let dest = temp_dir.path().join("lifted");
Command::cargo_bin("oinkie")
.unwrap()
.arg("lift")
.arg("--script")
.arg("testdata/scripts/EmptyLifter.java")
.arg("-d")
.arg(&dest)
.arg("testdata/bin/hello_clang")
.assert()
.success()
.stderr(predicate::str::contains("testdata/bin/hello_clang:"))
.stderr(predicate::str::contains("no functions"))
.stderr(predicate::str::contains("really has no functions"))
.stderr(predicate::str::contains("the tool found none"));
assert!(
dest.join("hello_clang.json").exists(),
"the lift warned and then kept nothing"
);
}