difftest structure refactor

* one `#[test]` per difftest
* in any crate
* just evaluate all sides within the test
* allows sharing structs between tests
* allows diffing structs instead of bytes
This commit is contained in:
firestar99
2026-04-29 14:13:14 +02:00
parent e834363370
commit b0cd114461
7 changed files with 211 additions and 1 deletions

8
Cargo.lock generated
View File

@@ -926,6 +926,14 @@ dependencies = [
[[package]]
name = "difftests"
version = "0.0.0"
dependencies = [
"anyhow",
"dissimilar",
]
[[package]]
name = "difftests-old-bin"
version = "0.0.0"
dependencies = [
"anyhow",
"difftest-runner",

View File

@@ -33,6 +33,7 @@ members = [
"tests/difftests/bin",
"tests/difftests/runner",
"tests/difftests/types",
"tests/difftests/difftests",
]
exclude = [

View File

@@ -1,5 +1,5 @@
[package]
name = "difftests"
name = "difftests-old-bin"
version = "0.0.0"
authors.workspace = true
edition.workspace = true

View File

@@ -0,0 +1,15 @@
[package]
name = "difftests"
version = "0.0.0"
publish = false
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
anyhow = "1.0"
dissimilar = "1.0.11"
[lints]
workspace = true

View File

@@ -0,0 +1,68 @@
use std::fmt::{Formatter, Write};
type Result = std::fmt::Result;
pub trait Diff {
fn fmt(f: &mut Formatter<'_>, pad: Padding, a: Self, b: Self) -> Result;
}
#[derive(Copy, Clone, Default)]
pub struct Padding {
pad: u32,
}
impl Padding {
pub fn new(pad: u32) -> Self {
Self { pad }
}
pub fn pad(&self, fmt: &mut Formatter<'_>) -> Result {
for _ in 0..self.pad {
fmt.write_char(' ')?;
}
Ok(())
}
pub fn add(&self, pad: u32) -> Padding {
Padding {
pad: self.pad + pad,
}
}
}
pub const PADDING_TAB: u32 = 4;
pub struct StructDiff<'a, 'b: 'a> {
fmt: &'b mut Formatter<'a>,
pad: Padding,
result: Result,
has_fields: bool,
}
impl<'a, 'b: 'a> StructDiff<'a, 'b> {
pub fn new(fmt: &'b mut Formatter<'a>, pad: Padding, name: &str) -> Self {
Self {
result: write!(fmt, "{name} {{"),
fmt,
pad,
has_fields: false,
}
}
pub fn field<T: Diff>(&mut self, name: &str, value_a: &T, value_b: &T) -> &mut self {
self.result = self.result.and_then(|_| {
self.pad.add(PADDING_TAB).pad(&mut self.fmt)?;
write!(self.fmt, "{name}: ")?;
T::fmt(&mut self.fmt, self.pad, value_a, value_b)?;
write!(self.fmt, ",\n")
});
self
}
pub fn finish(&mut self) -> Result {
self.result.and_then(|_| {
self.pad.pad(&mut self.fmt)?;
write!(self.fmt, "}}\n")
})
}
}

View File

@@ -0,0 +1,68 @@
use std::borrow::Cow;
use std::fmt::Debug;
mod side;
mod fmt;
pub use side::*;
pub use fmt::*;
/// A [`Side`] of a difftest
pub struct Side<'a, T> {
pub name: &'a str,
pub value: T,
}
impl<'a, T> Side<'a, T> {
pub fn new(name: &'a str, t: T) -> Self {
Self { name, value: t }
}
}
/// Run a difftest between these different [`Side`]s, panics on mismatch
pub fn difftest<T: Debug>(sides: &[Side<'_, T>]) {
difftest_anyhow(sides).unwrap()
}
/// Run a difftest between these different [`Side`]s, returns an [`anyhow::Result`] instead of panic-ing
pub fn difftest_anyhow<T: Debug>(sides: &[Side<'_, T>]) -> anyhow::Result<()> {
if sides.len() < 2 {
anyhow::bail!("No trails compared, expected at least two trails");
}
let results = sides
.into_iter()
.map(|t| Side::new(t.name, format!("{:#?}", t.value)))
.collect::<Vec<_>>();
let reference = &results.first().unwrap().value;
if results.iter().skip(1).all(|t| &t.value == reference) {
// all trails are equal to each other
Ok(())
} else {
// there was a mismatch between trails
difftest_report(&results)
}
}
/// Report a difference in any amount of trails
#[cold]
fn difftest_report(sides: &[Side<'_, String>]) -> anyhow::Result<()> {
let mut hashed = HashMap::with_capacity(sides.len());
for (i, side) in sides.iter().enumerate() {
hashed
.entry(side.value.as_str())
.or_insert(Vec::new())
.push(i);
}
let mut groups = hashed.into_iter().collect::<Vec<_>>();
assert!(groups.len() >= 2);
// sort with the most common group first
// if there are multiple groups that are as common as the rest, sort them by *something* stable
groups.sort_by(|a, b| a.1.len().cmp(&b.1.len()).then(a.0.cmp(b.0)));
// the most common group serves as the reference
let reference_ids = groups[0].1.as_slice();
let reference = &sides[reference_ids[0]];
anyhow::bail!(r#"Difftest failed!"#)
}

View File

@@ -0,0 +1,50 @@
use std::fmt::Formatter;
#[derive(Copy, Clone, Debug)]
pub struct SideConfig {
pub float_epsilon: f32,
}
pub trait SideValue {
fn check_equality(&self, other: &Self, config: &SideConfig) -> Result<(), >;
fn report_error(&self, other: &Self, config: &SideConfig) -> anyhow::Result<String>;
}
impl SideValue for &str {
fn check_equality(&self, other: &Self, config: &SideConfig) -> anyhow::Result<bool> {
self == other
}
fn report_error(&self, other: &Self, config: &SideConfig) -> anyhow::Result<String> {
let diff = dissimilar::diff(self, other);
diff.iter()
.map(|chunk| match chunk {
dissimilar::Chunk::Equal(text) => Cow::Borrowed(text),
dissimilar::Chunk::Delete(text) => {
Cow::Owned(format!("\x1b[4m\x1b[31m{}\x1b[0m", text))
}
dissimilar::Chunk::Insert(text) => {
Cow::Owned(format!("\x1b[4m\x1b[32m{}\x1b[0m", text))
}
})
.collect()
}
}
impl SideValue for u32 {
fn check_equality(&self, other: &Self, _: &SideConfig) -> Result<(), String> {
if self == other {
Ok(())
}
}
}
pub struct DiffFormatter<'a> {
pub f: Formatter<'a>,
has_diff: bool,
}
impl DiffFormatter {
pub fn
}