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"
);
}
#[test]
fn test_reaggregate_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("reaggregate.csv");
let mut cmd = Command::cargo_bin("oinkie").unwrap();
cmd.arg("reaggregate")
.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::prelude::AnyProgram::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"
);
}