use oinkie::birthmarks::Birthmark;
use oinkie::compare::Comparison;
use oinkie::{Error, Program, Result};
use std::io::Write;
use std::path::Path;
pub(crate) trait Side {
fn describe(&self) -> String;
fn names(&self) -> Vec<String>;
}
fn json_path(path: Option<&Path>) -> String {
path.map(|p| p.display().to_string()).unwrap_or_default()
}
impl Side for Birthmark {
fn describe(&self) -> String {
format!(
"birthmark,{},{},{},{},{},{},{}",
escape(self.name()),
escape(&self.path().display().to_string()),
self.birthmark_type(),
self.extracted_at().to_rfc3339(),
self.duration().as_nanos(),
self.ir(),
escape(&json_path(self.json_path()))
)
}
fn names(&self) -> Vec<String> {
self.functions()
.iter()
.map(|f| f.name().to_string())
.collect()
}
}
impl Side for Program {
fn describe(&self) -> String {
format!(
"program,{},{},{},{},{}",
escape(self.name()),
escape(&self.path().display().to_string()),
self.symbol_count(),
self.len(),
escape(&json_path(self.json_path()))
)
}
fn names(&self) -> Vec<String> {
self.function_names()
.into_iter()
.map(str::to_string)
.collect()
}
}
pub(crate) fn store<S: Side, P: AsRef<Path>>(comparison: &Comparison<S>, dest: P) -> Result<()> {
let dest = dest.as_ref();
let io_err = |e| Error::Io(dest.to_path_buf(), e);
let mut file = std::fs::File::create(dest).map_err(io_err)?;
let mut out = std::io::BufWriter::new(&mut file);
writeln!(
out,
"result,{},{}",
comparison.duration().as_nanos(),
comparison.similarity()
)
.map_err(io_err)?;
writeln!(out, "left,{}", comparison.columns().describe()).map_err(io_err)?;
writeln!(out, "right,{}", comparison.rows().describe()).map_err(io_err)?;
let column_names = comparison.columns().names();
let row_names = comparison.rows().names();
let header = column_names
.iter()
.map(|s| escape(s))
.collect::<Vec<_>>()
.join(",");
write!(out, "matrix,,{header}").map_err(io_err)?;
let matrix = comparison.matrix();
for (j, item) in row_names.iter().enumerate() {
write!(out, "\n{},{}", j, escape(item)).map_err(io_err)?;
for i in 0..column_names.len() {
let value = matrix[[i, j]];
write!(out, ",{value}").map_err(io_err)?;
}
}
writeln!(out).map_err(io_err)?;
out.flush().map_err(io_err)?;
Ok(())
}
pub(crate) fn escape(s: &str) -> String {
if s.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oinkie::birthmarks::BirthmarkType;
use oinkie::compare::{Aggregator, Algorithm};
use oinkie::extract::Extractor;
use std::path::PathBuf;
const CLANG: &str = "testdata/lifted/pcodes/hello_clang.json";
const GCC: &str = "testdata/lifted/pcodes/hello_gcc.json";
const UDL: &str = "testdata/lifted/pcodes/udl.json";
const LLIL: &str = "testdata/lifted/bnil_llil/hello_clang.json";
fn fields(line: &str) -> Vec<String> {
csv::ReaderBuilder::new()
.has_headers(false)
.from_reader(line.as_bytes())
.records()
.next()
.unwrap()
.unwrap()
.iter()
.map(str::to_string)
.collect()
}
fn birthmark(lifted: &str) -> Birthmark {
let program = Program::load(Path::new(lifted)).unwrap();
Extractor::new(BirthmarkType::OpSeq)
.extract(&program)
.unwrap()
}
#[test]
fn test_a_comparison_is_read_back_whole_by_review() {
let (b1, b2) = (birthmark(UDL), birthmark(CLANG));
let c = oinkie::Oinkie::new()
.unwrap()
.comparator(&Algorithm::Jaccard)
.compare_birthmarks(&b1, &b2, &Aggregator::Hungarian)
.unwrap();
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("00000.csv");
store(&c, &dest).unwrap();
let content = std::fs::read_to_string(&dest).unwrap();
assert!(content.starts_with("result,"), "{content}");
let stored = crate::review::load_comparison(&dest).unwrap();
let (loaded, left, right) = (stored.matrix, stored.left, stored.right);
let written = c.matrix();
assert_eq!(
written.dim(),
(b1.len(), b2.len()),
"one index per function"
);
assert_eq!(
loaded.dim(),
(b2.len(), b1.len()),
"one row per right-hand function"
);
for i in 0..b1.len() {
for j in 0..b2.len() {
assert_eq!(loaded[[j, i]], written[[i, j]], "score [{i}, {j}]");
}
}
assert!(
(0..b1.len()).any(|i| written[[i, 0]] != written[[0, 0]]),
"the fixture no longer tells a transposed matrix apart"
);
assert_eq!(left.path, b1.path());
assert_eq!(right.path, b2.path());
assert_eq!(left.functions, b1.names(), "the column names, in order");
assert_eq!(right.functions, b2.names(), "the row names, in order");
}
#[test]
fn test_a_path_that_needs_quoting_survives_the_round_trip() {
for path in [
"dir,with \"commas\"/app, v1",
"dir/with\nline feed",
"dir/with\rcarriage return",
] {
let mut json = serde_json::to_value(birthmark(CLANG)).unwrap();
json["metadata"]["path"] = serde_json::json!(path);
let b1: Birthmark = serde_json::from_value(json).unwrap();
let b2 = birthmark(GCC);
let c = oinkie::Oinkie::new()
.unwrap()
.comparator(&Algorithm::Jaccard)
.compare_birthmarks(&b1, &b2, &Aggregator::Hungarian)
.unwrap();
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("00000.csv");
store(&c, &dest).unwrap();
let stored =
crate::review::load_comparison(&dest).unwrap_or_else(|e| panic!("{path:?}: {e}"));
assert_eq!(stored.left.path, PathBuf::from(path), "{path:?}");
assert_eq!(
stored.right.path,
b2.path(),
"{path:?}: the next record was disturbed"
);
}
}
#[test]
fn test_a_function_name_that_needs_quoting_survives_the_round_trip() {
let name = "foo<int, \"q\">";
let mut json = serde_json::to_value(birthmark(CLANG)).unwrap();
json["elements"][0]["name"] = serde_json::json!(name);
let b: Birthmark = serde_json::from_value(json).unwrap();
let c = oinkie::Oinkie::new()
.unwrap()
.comparator(&Algorithm::Jaccard)
.compare_birthmarks(&b, &b, &Aggregator::Hungarian)
.unwrap();
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("00000.csv");
store(&c, &dest).unwrap();
let stored = crate::review::load_comparison(&dest).unwrap();
assert_eq!(stored.left.functions[0], name);
assert_eq!(stored.right.functions[0], name);
assert_eq!(stored.matrix.dim(), (b.len(), b.len()));
}
#[test]
fn test_a_program_describes_itself() {
let mut p = Program::load(Path::new(LLIL)).unwrap();
assert_ne!(
p.symbol_count(),
p.len(),
"the fixture no longer tells the counts apart"
);
assert_eq!(
fields(&p.describe()),
[
"program".to_string(),
p.name().to_string(),
p.path().display().to_string(),
p.symbol_count().to_string(),
p.len().to_string(),
String::new(),
]
);
p.set_json_path(PathBuf::from(LLIL));
assert_eq!(fields(&p.describe()).last().unwrap(), LLIL);
assert_eq!(p.names().len(), p.len());
}
#[test]
fn test_a_birthmark_describes_itself() {
let mut b = birthmark(CLANG);
assert_eq!(
fields(&b.describe()),
[
"birthmark".to_string(),
b.name().to_string(),
b.path().display().to_string(),
b.birthmark_type().to_string(),
b.extracted_at().to_rfc3339(),
b.duration().as_nanos().to_string(),
b.ir().to_string(),
String::new(),
]
);
b.set_json_path(PathBuf::from("birthmarks/hello_clang.json"));
assert_eq!(
fields(&b.describe()).last().unwrap(),
"birthmarks/hello_clang.json"
);
assert_eq!(b.names().len(), b.len());
}
#[test]
fn test_a_destination_that_cannot_be_written_is_an_io_error_on_it() {
let b = birthmark(CLANG);
let c = oinkie::Oinkie::new()
.unwrap()
.comparator(&Algorithm::Jaccard)
.compare_birthmarks(&b, &b, &Aggregator::Hungarian)
.unwrap();
let err = store(&c, "no/such/directory/out.csv").unwrap_err();
assert!(
matches!(&err, Error::Io(p, _) if p == Path::new("no/such/directory/out.csv")),
"{err:?}"
);
}
#[test]
fn test_a_value_is_quoted_only_when_it_needs_to_be() {
assert_eq!(escape("plain"), "plain");
assert_eq!(escape("a,b"), "\"a,b\"");
assert_eq!(escape("a\"b"), "\"a\"\"b\"");
assert_eq!(escape("a\nb"), "\"a\nb\"");
assert_eq!(escape("a\rb"), "\"a\rb\"");
}
}