use std::fmt;
use std::path::PathBuf;
use std::process::{self, Command};
use std::sync::atomic::{AtomicUsize, Ordering};
use super::jobs::{JobChoice, JOBS_ENV};
pub const CHILD_ENV: &str = "OXIJOLT_DIGEST_CHILD";
pub const OUTPUT_ENV: &str = "OXIJOLT_DIGEST_OUTPUT";
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TickDigest {
pub shape: Vec<u8>,
pub state: Vec<u8>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Digest {
pub ticks: Vec<TickDigest>,
}
impl Digest {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self) -> &mut TickDigest {
self.ticks.push(TickDigest::default());
self.ticks.last_mut().unwrap()
}
pub fn encode(&self) -> Vec<u8> {
let mut bytes = Vec::new();
let put_len = |bytes: &mut Vec<u8>, len: usize| {
let len = u32::try_from(len).expect("digest section longer than u32::MAX");
bytes.extend_from_slice(&len.to_le_bytes());
};
put_len(&mut bytes, self.ticks.len());
for tick in &self.ticks {
for section in [&tick.shape, &tick.state] {
put_len(&mut bytes, section.len());
bytes.extend_from_slice(section);
}
}
bytes
}
pub fn decode(bytes: &[u8]) -> Result<Digest, String> {
let mut reader = Reader { bytes, offset: 0 };
let count = reader.len()?;
let mut digest = Digest::new();
for _ in 0..count {
let shape = reader.section()?;
let state = reader.section()?;
digest.ticks.push(TickDigest { shape, state });
}
if reader.offset != bytes.len() {
return Err(format!(
"{} trailing bytes at offset {}",
bytes.len() - reader.offset,
reader.offset
));
}
Ok(digest)
}
}
struct Reader<'a> {
bytes: &'a [u8],
offset: usize,
}
impl Reader<'_> {
fn take(&mut self, count: usize, what: &str) -> Result<&[u8], String> {
let end = self
.offset
.checked_add(count)
.filter(|&end| end <= self.bytes.len());
let Some(end) = end else {
return Err(format!("truncated {what} at offset {}", self.offset));
};
let taken = &self.bytes[self.offset..end];
self.offset = end;
Ok(taken)
}
fn len(&mut self) -> Result<usize, String> {
let bytes = self.take(4, "length")?;
Ok(u32::from_le_bytes(bytes.try_into().unwrap()) as usize)
}
fn section(&mut self) -> Result<Vec<u8>, String> {
let len = self.len()?;
Ok(self.take(len, "section")?.to_vec())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Section {
Shape,
State,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Divergence {
TickCount { a: usize, b: usize },
Tick {
tick: usize,
section: Section,
byte: usize,
},
}
impl fmt::Display for Divergence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TickCount { a, b } => write!(f, "tick counts {a} and {b}"),
Self::Tick {
tick,
section,
byte,
} => {
let section = match section {
Section::Shape => "shape",
Section::State => "state",
};
write!(f, "tick {tick}, {section} byte {byte}")
}
}
}
}
fn first_different_byte(a: &[u8], b: &[u8]) -> Option<usize> {
a.iter()
.zip(b)
.position(|(x, y)| x != y)
.or_else(|| (a.len() != b.len()).then(|| a.len().min(b.len())))
}
pub fn first_divergence(a: &Digest, b: &Digest) -> Option<Divergence> {
for (tick, (x, y)) in a.ticks.iter().zip(&b.ticks).enumerate() {
for (section, x, y) in [
(Section::Shape, &x.shape, &y.shape),
(Section::State, &x.state, &y.state),
] {
if let Some(byte) = first_different_byte(x, y) {
return Some(Divergence::Tick {
tick,
section,
byte,
});
}
}
}
(a.ticks.len() != b.ticks.len()).then_some(Divergence::TickCount {
a: a.ticks.len(),
b: b.ticks.len(),
})
}
pub fn assert_same(what: &str, a: &Digest, b: &Digest) {
if let Some(divergence) = first_divergence(a, b) {
panic!("{what}: digests diverge at {divergence}");
}
}
pub fn child_request() -> Option<(String, u32, String)> {
match std::env::var(CHILD_ENV) {
Ok(request) => Some(parse_child_request(&request)),
Err(std::env::VarError::NotPresent) => None,
Err(std::env::VarError::NotUnicode(request)) => {
panic!("{CHILD_ENV}={request:?} is not Unicode")
}
}
}
pub fn parse_child_request(request: &str) -> (String, u32, String) {
let mut fields = request.splitn(3, ',');
let mut field = |name| {
fields
.next()
.unwrap_or_else(|| panic!("{CHILD_ENV}={request:?} has no {name} field"))
.to_owned()
};
let scenario = field("scenario");
let threads = field("threads");
let variant = field("variant");
let threads = threads
.parse()
.unwrap_or_else(|_| panic!("{CHILD_ENV}={request:?}: bad thread count"));
(scenario, threads, variant)
}
pub fn finish_child(digest: &Digest) {
let output = std::env::var(OUTPUT_ENV).unwrap_or_else(|_| panic!("{OUTPUT_ENV} is not set"));
std::fs::write(output, digest.encode()).unwrap();
}
struct TempFile(PathBuf);
impl Drop for TempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
const OUTPUT_TAIL: usize = 4000;
pub fn digest_in_child(child_test: &str, scenario: &str, threads: u32, variant: &str) -> Digest {
digest_in_child_with_jobs(child_test, scenario, threads, variant, JobChoice::Native)
}
pub fn digest_in_child_with_jobs(
child_test: &str,
scenario: &str,
threads: u32,
variant: &str,
jobs: JobChoice,
) -> Digest {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let file = TempFile(std::env::temp_dir().join(format!(
"oxijolt-digest-{}-{}.bin",
process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
)));
let request = format!("{scenario},{threads},{variant}");
let mut command = Command::new(std::env::current_exe().unwrap());
command
.args([child_test, "--exact", "--ignored", "--test-threads=1"])
.env(CHILD_ENV, &request)
.env(OUTPUT_ENV, &file.0);
match jobs.as_env() {
Some(value) => command.env(JOBS_ENV, value),
None => command.env_remove(JOBS_ENV),
};
let request = format!("{request} on {jobs:?} jobs");
let output = command.output().unwrap();
if !output.status.success() {
let tail = |bytes: &[u8]| {
String::from_utf8_lossy(&bytes[bytes.len().saturating_sub(OUTPUT_TAIL)..]).into_owned()
};
panic!(
"child {request} failed: {}\nstdout:\n{}\nstderr:\n{}",
output.status,
tail(&output.stdout),
tail(&output.stderr)
);
}
let bytes = std::fs::read(&file.0)
.unwrap_or_else(|error| panic!("child {request} wrote no digest: {error}"));
Digest::decode(&bytes).unwrap_or_else(|error| panic!("child {request}: {error}"))
}
pub fn assert_caller_job_systems_agree(child_test: &str, scenario: &str, variant: &str) {
let run =
|threads, jobs| digest_in_child_with_jobs(child_test, scenario, threads, variant, jobs);
let native = run(1, JobChoice::Native);
let rayon = run(4, JobChoice::Rayon);
let inline = run(1, JobChoice::Inline);
assert!(!native.ticks.is_empty());
assert_same(
&format!("{scenario} {variant}, 1 worker vs Rayon 4 threads"),
&native,
&rayon,
);
assert_same(
&format!("{scenario} {variant}, 1 worker vs inline"),
&native,
&inline,
);
}