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/support/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"));
}