use cadmpeg_core::CodecError;
use crate::codec::{Codec, CodecEntry, DecodeOptions, EncodeInput, Encoder, ExportPlan};
use crate::document::CadIr;
use crate::hash::DOCUMENT_LOCAL_DIGEST_ATTRIBUTE;
use crate::report::{ExportReport, WritePath};
pub fn verbatim_replay_holds<C>(codec: &C, label: &str, fixture: &[u8]) -> ExportReport
where
C: Codec + Encoder,
{
let decoded = CodecEntry::decode(
codec,
&mut std::io::Cursor::new(fixture.to_vec()),
&DecodeOptions::default(),
)
.unwrap_or_else(|error| panic!("{label}: decode failed: {error}"));
let plan = Encoder::plan(
codec,
EncodeInput {
ir: &decoded.ir,
fidelity: Some(&decoded.source_fidelity),
},
)
.unwrap_or_else(|error| panic!("{label}: plan failed: {error}"));
let path = ExportPlan::write_path(&plan);
let mut written = Vec::new();
let report = plan
.write_to(&mut written)
.unwrap_or_else(|error| panic!("{label}: write failed: {error}"));
assert_eq!(
path,
WritePath::VerbatimReplay,
"{label}: this fixture was expected to replay its retained bytes, but the encoder took the {path} path; \
a byte comparison here would not describe the replay it claims to cover"
);
assert!(
written == fixture,
"{label}: verbatim replay produced different bytes: {}",
describe_byte_difference(fixture, &written)
);
report
}
#[derive(Debug)]
pub enum SemanticOutcome {
Written {
ir: Box<CadIr>,
report: Box<ExportReport>,
bytes: Vec<u8>,
},
Refused {
error: CodecError,
},
}
pub fn semantic_roundtrip<C>(
codec: &C,
label: &str,
fixture: &[u8],
check: impl FnOnce(&SemanticOutcome),
) where
C: Codec + Encoder,
{
let mut decoded = CodecEntry::decode(
codec,
&mut std::io::Cursor::new(fixture.to_vec()),
&DecodeOptions::default(),
)
.unwrap_or_else(|error| panic!("{label}: decode failed: {error}"));
let removed = decoded
.ir
.source
.as_mut()
.and_then(|source| source.attributes.remove(DOCUMENT_LOCAL_DIGEST_ATTRIBUTE));
assert!(
removed.is_some(),
"{label}: the decoded document carries no `{DOCUMENT_LOCAL_DIGEST_ATTRIBUTE}`, so removing it cannot \
force the semantic path; this helper would report whatever the encoder happened to do"
);
let written = match Encoder::plan(
codec,
EncodeInput {
ir: &decoded.ir,
fidelity: Some(&decoded.source_fidelity),
},
) {
Ok(plan) => {
let path = ExportPlan::write_path(&plan);
let mut bytes = Vec::new();
let report = plan
.write_to(&mut bytes)
.unwrap_or_else(|error| panic!("{label}: write failed: {error}"));
Ok((path, report, bytes))
}
Err(error) => Err(error),
};
let outcome = match written {
Ok((path, report, bytes)) => {
assert_ne!(
path,
WritePath::VerbatimReplay,
"{label}: the baseline was removed, so the encoder could not show the retained bytes still \
describe this document, yet it replayed them"
);
SemanticOutcome::Written {
ir: Box::new(decoded.ir),
report: Box::new(report),
bytes,
}
}
Err(error) => SemanticOutcome::Refused { error },
};
check(&outcome);
}
#[derive(Debug)]
pub enum MutationOutcome {
Written {
baseline: Box<CadIr>,
edited: Box<CadIr>,
report: Box<ExportReport>,
bytes: Vec<u8>,
},
Refused {
error: CodecError,
},
}
pub fn mutation_roundtrip<C>(
codec: &C,
label: &str,
fixture: &[u8],
expected_path: WritePath,
mutate: impl FnOnce(&mut CadIr) -> bool,
check: impl FnOnce(&MutationOutcome),
) -> bool
where
C: Codec + Encoder,
{
assert_ne!(
expected_path,
WritePath::VerbatimReplay,
"{label}: this helper edits the document, so replaying the retained bytes would discard the edit; \
no caller may name that path as expected"
);
let mut decoded = CodecEntry::decode(
codec,
&mut std::io::Cursor::new(fixture.to_vec()),
&DecodeOptions::default(),
)
.unwrap_or_else(|error| panic!("{label}: decode failed: {error}"));
let baseline = decoded.ir.clone();
if !mutate(&mut decoded.ir) {
return false;
}
assert!(
decoded.ir != baseline,
"{label}: the mutation reported an edit but left the document equal to the decode, so the encoder \
would still be free to replay its retained bytes and this test would describe nothing"
);
assert!(
decoded
.ir
.source
.as_ref()
.is_some_and(|source| source.attributes.contains_key(DOCUMENT_LOCAL_DIGEST_ATTRIBUTE)),
"{label}: the mutation removed `{DOCUMENT_LOCAL_DIGEST_ATTRIBUTE}`; this helper covers the edited \
document with its baseline intact, and without it the codec answers a different question"
);
let written = match Encoder::plan(
codec,
EncodeInput {
ir: &decoded.ir,
fidelity: Some(&decoded.source_fidelity),
},
) {
Ok(plan) => {
let path = ExportPlan::write_path(&plan);
let mut bytes = Vec::new();
let report = plan
.write_to(&mut bytes)
.unwrap_or_else(|error| panic!("{label}: write failed: {error}"));
Ok((path, report, bytes))
}
Err(error) => Err(error),
};
let outcome = match written {
Ok((path, report, bytes)) => {
assert_eq!(
path, expected_path,
"{label}: the document was edited, so the encoder was expected to write by the \
{expected_path} path, but it took the {path} path"
);
MutationOutcome::Written {
baseline: Box::new(baseline),
edited: Box::new(decoded.ir),
report: Box::new(report),
bytes,
}
}
Err(error) => MutationOutcome::Refused { error },
};
check(&outcome);
true
}
fn describe_byte_difference(expected: &[u8], actual: &[u8]) -> String {
match expected
.iter()
.zip(actual)
.position(|(left, right)| left != right)
{
Some(offset) => format!(
"first difference at offset {offset}: fixture 0x{:02x}, written 0x{:02x} (lengths {} and {})",
expected[offset],
actual[offset],
expected.len(),
actual.len()
),
None => format!(
"one side is a prefix of the other (lengths {} and {})",
expected.len(),
actual.len()
),
}
}