epiphany/crates/epiphany-testkit/src/textproj.rs

92 lines
3.7 KiB
Rust

//! End-to-end semantic preservation for the Text Projection document layer.
//!
//! This is the weaker `req:textproj:roundtrip` equation over generated real
//! operations:
//!
//! `semantics(parse(project(B))) == semantics(B)`.
//!
//! It compares canonical reducer bytes, never bundle bytes. A text round trip
//! intentionally regenerates layout, collapses duplicate blobs, and omits
//! non-canonical accelerators; none of those changes operation semantics.
use epiphany_bundle::{DocumentId, FileUuid, MemStore, ProfileDeclaration};
use epiphany_ops::{OperationEnvelope, OperationSet};
use epiphany_textproj::parse::parse_document;
use epiphany_textproj::project::{document_from_bundle, project_bundle};
use epiphany_textproj::serialize::serialize_document;
use epiphany_textproj::TextDocument;
use crate::{generators, Rng};
fn reduced_bytes(envelopes: &[OperationEnvelope]) -> Vec<u8> {
let mut operations = OperationSet::new();
operations.accept_all(envelopes.iter().cloned());
operations.reduce().canonical_bytes()
}
/// Generates a bundle carrying real operations and checks the semantic
/// projection equation by reducing both envelope sets to canonical bytes.
pub fn assert_semantics_preserved(seed: u64) {
let mut rng = Rng::new(seed);
let envelopes = generators::operation_envelopes(&mut rng, 24, 3, 8, 8);
assert!(
!envelopes.is_empty(),
"the generated bundle carries operations"
);
let source = TextDocument {
document_id: DocumentId(seed.to_le_bytes().repeat(2).try_into().expect("16 bytes")),
lineage_id: None,
profiles: vec![ProfileDeclaration::full()],
extensions: Vec::new(),
canonical_base: None,
blobs: Vec::new(),
envelopes,
};
let bundle = serialize_document(&source, MemStore::new(), FileUuid([0x7E; 16]))
.unwrap_or_else(|error| panic!("seed {seed}: serializing generated operations: {error}"));
let bundle_document = document_from_bundle(&bundle)
.unwrap_or_else(|error| panic!("seed {seed}: reading source bundle: {error}"));
assert!(
!bundle_document.envelopes.is_empty(),
"seed {seed}: source bundle lost every operation"
);
let text = project_bundle(&bundle)
.unwrap_or_else(|error| panic!("seed {seed}: projecting source bundle: {error}"));
let parsed = parse_document(&text)
.unwrap_or_else(|error| panic!("seed {seed}: parsing its projection: {error}"));
// `semantics(B)` must be computed WITHOUT going through the projection.
// Comparing against `bundle_document` alone would not: `project_bundle` is
// `project_text_document(document_from_bundle(..))`, so both sides would flow
// through `document_from_bundle` and any bug in it — dropping an envelope,
// reordering, corrupting one — would cancel out. `source.envelopes` is the
// independent reference: this test built it and serialized it in, and
// `reduced_bytes` reduces through an `OperationSet`, which imposes canonical
// order itself, so the input order does not matter.
assert_eq!(
reduced_bytes(&parsed.envelopes),
reduced_bytes(&source.envelopes),
"seed {seed}: semantics(parse(project(B))) != semantics(B)"
);
// And the read-back path agrees with the same independent reference.
assert_eq!(
reduced_bytes(&bundle_document.envelopes),
reduced_bytes(&source.envelopes),
"seed {seed}: reading the bundle back changed its semantics"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_operation_bundles_preserve_reduced_semantics_not_bundle_identity() {
for seed in 0..16 {
assert_semantics_preserved(seed);
}
}
}