oxedyne/fe2o3/fe2o3_sbj/tests/conformance.rs
19.3 KiB, 45 runs
created by r1870400018:22234, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The conformance suite of `SPEC.md` §7. |
| 2 | //! |
| 3 | //! The fixtures are the specification's teeth. They are what stops the binary quietly becoming the |
| 4 | //! definition of the format: every acceptance fixture is rebuilt here from its JDAT text and the |
| 5 | //! committed key, and the bytes that come out must be the bytes that were committed, so a change to |
| 6 | //! the encoder that nobody meant shows up as a different file rather than as nothing at all. |
| 7 | //! |
| 8 | //! Every rejection fixture declares what must go wrong with it, and where: the rule broken, the step |
| 9 | //! of §2 that must catch it, what the error must say, and the node or the byte it must name. A |
| 10 | //! reader that refused everything would pass a suite that only checked for an `Err`, so this checks |
| 11 | //! the reason. It also checks the ordering that §2 exists for: a fixture refused at step 6 or 7 must |
| 12 | //! pass steps 1 to 5 first, and a fixture refused before that must never reach a decoder at all. |
| 13 | //! |
| 14 | //! A fixture directory the suite does not know how to run is a failure rather than a skip, so a |
| 15 | //! fixture cannot be added and silently ignored, and the fixtures §7 requires by name are required |
| 16 | //! here by name, so one cannot be deleted and silently missed. |
| 17 | |
| 18 | mod common; |
| 19 | |
| 20 | use common::{ |
| 21 | Keys, |
| 22 | Meta, |
| 23 | Reject, |
| 24 | DOC_JDAT, |
| 25 | DOC_SBJ, |
| 26 | KEY_FILE, |
| 27 | META_JDAT, |
| 28 | README_FILE, |
| 29 | REJECT_JDAT, |
| 30 | }; |
| 31 | |
| 32 | use oxedyne_fe2o3_sbj::{ |
| 33 | card::Card, |
| 34 | doc::{ |
| 35 | self, |
| 36 | Payload, |
| 37 | }, |
| 38 | envelope, |
| 39 | index, |
| 40 | post::Post, |
| 41 | share::Share, |
| 42 | validate, |
| 43 | HEADER_LEN, |
| 44 | SCHEMA_CARD, |
| 45 | SCHEMA_POST, |
| 46 | SCHEMA_SHARE, |
| 47 | }; |
| 48 | |
| 49 | use oxedyne_fe2o3_core::prelude::*; |
| 50 | use oxedyne_fe2o3_crypto::sign::SignatureScheme; |
| 51 | use oxedyne_fe2o3_jdat::prelude::*; |
| 52 | |
| 53 | use std::{ |
| 54 | collections::BTreeSet, |
| 55 | fs, |
| 56 | path::Path, |
| 57 | }; |
| 58 | |
| 59 | /// The fixtures `SPEC.md` §7 requires by name. |
| 60 | /// |
| 61 | /// A fixture that is missing is a failure, so that the suite cannot be quietly hollowed out by |
| 62 | /// deleting the ones that fail. |
| 63 | const REQUIRED: [&'static str; 30] = [ |
| 64 | // The list at the end of §7, in its order. |
| 65 | "empty", // An empty document. |
| 66 | "one_para", // One paragraph. |
| 67 | "every_kind", // Every node kind once. |
| 68 | "depth_at_limit", // Nesting at the depth limit, |
| 69 | "depth_over_limit", // and one past it. |
| 70 | "size_at_limit", // A tree at the size limit, |
| 71 | "size_over_limit", // and one past it. |
| 72 | "canon_rule1_undeclared_field", // Each canonicalisation rule of §3, |
| 73 | "canon_rule2_ordmap", // violated |
| 74 | "canon_rule3_uppercase_key", // exactly |
| 75 | "canon_rule3_duplicate_key", // once. |
| 76 | "canon_rule4_empty_children", |
| 77 | "canon_rule4_opt_none", |
| 78 | "canon_rule5_control_char", |
| 79 | "canon_rule6_int_width", |
| 80 | "canon_rule7_vek_children", |
| 81 | "truncated_tree", // A truncated tree. |
| 82 | "tree_longer_than_tree_len", // A tree one byte longer than tree_len. |
| 83 | "corrupt_tree_byte", // A corrupted hash, from the tree's side, |
| 84 | "bad_hash", // and from the envelope's. |
| 85 | "bad_sig", // A corrupted signature. |
| 86 | "wrong_key", // A signature by the wrong key. |
| 87 | "unknown_kind", // An unknown node kind. |
| 88 | "reserved_edit_in_doc", // A document carrying an edit node, |
| 89 | "reserved_surface_in_doc", // one carrying a surface node, |
| 90 | "reserved_surface_with_fallback_still_refused", // and one whose surface carries a fallback. |
| 91 | "para_in_para", // A forbidden child. |
| 92 | "heading_level_0", // A heading with level 0, |
| 93 | "heading_level_7", // and one with level 7. |
| 94 | "bdat_not_sbj", // Valid BDAT, and not valid SBJ. |
| 95 | ]; |
| 96 | |
| 97 | #[test] |
| 98 | fn test_conformance_suite() -> Outcome<()> { |
| 99 | // A node costs three daticle levels, so the deepest legal document nests daticles 770 deep, and |
| 100 | // a recursive decoder spends a frame on each. The limit is the format's, and it does not move to |
| 101 | // suit a test, so the test moves instead. |
| 102 | let thread = match std::thread::Builder::new() |
| 103 | .name("sbj_conformance".to_string()) |
| 104 | .stack_size(common::STACK_BYTES) |
| 105 | .spawn(suite) |
| 106 | { |
| 107 | Ok(thread) => thread, |
| 108 | Err(e) => return Err(err!(e, |
| 109 | "Could not spawn the thread the deepest fixture is read on."; |
| 110 | Test, Init)), |
| 111 | }; |
| 112 | match thread.join() { |
| 113 | Ok(outcome) => outcome, |
| 114 | Err(_) => Err(err!( |
| 115 | "The thread running the conformance suite did not return."; |
| 116 | Test, Panic)), |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | /// Walks the fixture directory and runs everything in it. |
| 121 | fn suite() -> Outcome<()> { |
| 122 | |
| 123 | let root = common::fixtures_dir(); |
| 124 | if !root.is_dir() { |
| 125 | return Err(err!( |
| 126 | "There is no fixture directory at {}. The fixtures are written by \ |
| 127 | `cargo run -p sbj --example gen_fixtures`.", root.display(); |
| 128 | Test, Missing)); |
| 129 | } |
| 130 | let keys = res!(Keys::load(&root)); |
| 131 | |
| 132 | let mut names: BTreeSet<String> = BTreeSet::new(); |
| 133 | for entry in res!(fs::read_dir(&root), IO, File) { |
| 134 | let entry = res!(entry, IO, File); |
| 135 | let path = entry.path(); |
| 136 | let name = match path.file_name().and_then(|s| s.to_str()) { |
| 137 | Some(name) => name.to_string(), |
| 138 | None => return Err(err!( |
| 139 | "The fixture directory holds {}, whose name is not UTF-8.", path.display(); |
| 140 | Test, Invalid)), |
| 141 | }; |
| 142 | if path.is_file() { |
| 143 | // The only files beside the fixtures are the key they are signed with and the note |
| 144 | // saying what they are. Anything else is a fixture nobody runs. |
| 145 | if name != KEY_FILE && name != README_FILE { |
| 146 | return Err(err!( |
| 147 | "The fixture directory holds the file '{}', which is neither the key '{}' nor \ |
| 148 | the note '{}'. A file the suite does not know what to do with is a failure, \ |
| 149 | not a skip.", name, KEY_FILE, README_FILE; |
| 150 | Test, Invalid, Unexpected)); |
| 151 | } |
| 152 | continue; |
| 153 | } |
| 154 | if !path.is_dir() { |
| 155 | return Err(err!( |
| 156 | "The fixture directory holds '{}', which is neither a file nor a directory.", name; |
| 157 | Test, Invalid, Unexpected)); |
| 158 | } |
| 159 | res!(fixture(&path, &name, &keys)); |
| 160 | names.insert(name); |
| 161 | } |
| 162 | |
| 163 | if names.is_empty() { |
| 164 | return Err(err!( |
| 165 | "The fixture directory {} holds no fixtures.", root.display(); |
| 166 | Test, Missing)); |
| 167 | } |
| 168 | for req in REQUIRED { |
| 169 | if !names.contains(req) { |
| 170 | return Err(err!( |
| 171 | "The fixture '{}', which SPEC.md §7 requires, is not in {}.", req, root.display(); |
| 172 | Test, Missing)); |
| 173 | } |
| 174 | } |
| 175 | Ok(()) |
| 176 | } |
| 177 | |
| 178 | /// Runs one fixture, refusing to skip one it does not understand. |
| 179 | fn fixture( |
| 180 | dir: &Path, |
| 181 | name: &str, |
| 182 | keys: &Keys, |
| 183 | ) |
| 184 | -> Outcome<()> |
| 185 | { |
| 186 | // Every file of a fixture is one of four, so that a fixture cannot carry something the suite |
| 187 | // silently ignores. |
| 188 | for entry in res!(fs::read_dir(dir), IO, File) { |
| 189 | let entry = res!(entry, IO, File); |
| 190 | let file = match entry.file_name().to_str() { |
| 191 | Some(file) => file.to_string(), |
| 192 | None => return Err(err!( |
| 193 | "The fixture '{}' holds a file whose name is not UTF-8.", name; |
| 194 | Test, Invalid)), |
| 195 | }; |
| 196 | match file.as_str() { |
| 197 | DOC_JDAT | DOC_SBJ | META_JDAT | REJECT_JDAT => (), |
| 198 | _ => return Err(err!( |
| 199 | "The fixture '{}' holds the file '{}'. A fixture carries '{}', and then either \ |
| 200 | '{}' or '{}'.", name, file, DOC_SBJ, META_JDAT, REJECT_JDAT; |
| 201 | Test, Invalid, Unexpected)), |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | let has_meta = dir.join(META_JDAT).is_file(); |
| 206 | let has_reject = dir.join(REJECT_JDAT).is_file(); |
| 207 | if !dir.join(DOC_SBJ).is_file() { |
| 208 | return Err(err!( |
| 209 | "The fixture '{}' carries no '{}'. Every fixture is an artefact, whether it is one a \ |
| 210 | reader must accept or one it must refuse.", name, DOC_SBJ; |
| 211 | Test, Missing)); |
| 212 | } |
| 213 | match (has_meta, has_reject) { |
| 214 | (true, false) => accept(dir, name, keys), |
| 215 | (false, true) => reject(dir, name), |
| 216 | (true, true) => Err(err!( |
| 217 | "The fixture '{}' carries both '{}' and '{}'. A document is either accepted or \ |
| 218 | refused.", name, META_JDAT, REJECT_JDAT; |
| 219 | Test, Invalid, Conflict)), |
| 220 | (false, false) => Err(err!( |
| 221 | "The suite does not know how to run the fixture '{}': it carries neither '{}' nor \ |
| 222 | '{}'. A fixture the suite cannot run is a failure, not a skip, so that a fixture \ |
| 223 | cannot be added and quietly ignored.", name, META_JDAT, REJECT_JDAT; |
| 224 | Test, Invalid, Unknown)), |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | /// Runs an acceptance fixture: the artefact reads, and it is what `meta.jdat` says it is. |
| 229 | /// |
| 230 | /// The artefact is then rebuilt from `doc.jdat` and the committed key, and must come out byte for |
| 231 | /// byte the file that was committed. That is what makes `doc.jdat` the source of truth rather than a |
| 232 | /// comment: a change to the encoder that nobody meant shows up here as a different file. |
| 233 | fn accept( |
| 234 | dir: &Path, |
| 235 | name: &str, |
| 236 | keys: &Keys, |
| 237 | ) |
| 238 | -> Outcome<()> |
| 239 | { |
| 240 | let buf = res!(common::read_bytes(&dir.join(DOC_SBJ))); |
| 241 | let meta = res!(Meta::from_dat(&res!(common::from_jdat_plain( |
| 242 | &res!(common::read_text(&dir.join(META_JDAT))) |
| 243 | )))); |
| 244 | |
| 245 | // Steps 1 to 5 touch no content, and a caller may run them and stop. |
| 246 | let env = match doc::verify_only(&buf) { |
| 247 | Ok(env) => env, |
| 248 | Err(e) => return Err(err!(e, |
| 249 | "The fixture '{}' does not verify.", name; |
| 250 | Test, Invalid)), |
| 251 | }; |
| 252 | let art = match doc::read_artefact(&buf) { |
| 253 | Ok(art) => art, |
| 254 | Err(e) => return Err(err!(e, |
| 255 | "The fixture '{}' does not read.", name; |
| 256 | Test, Invalid)), |
| 257 | }; |
| 258 | res!(req(name, "the envelope of verify_only is the envelope of read", &env, art.env())); |
| 259 | |
| 260 | // The envelope says what `meta.jdat` says it says. |
| 261 | res!(req(name, "schema", &art.env().schema, &meta.schema)); |
| 262 | res!(req(name, "time", &art.env().time, &meta.time)); |
| 263 | res!(req(name, "hash", &art.env().hash, &meta.hash)); |
| 264 | res!(req(name, "tree_len", &art.env().tree_len, &meta.tree_len)); |
| 265 | res!(req(name, "author", &art.env().author, &keys.author.pk)); |
| 266 | |
| 267 | // The hash in the envelope is the hash of the region, and the region is the length declared. |
| 268 | let (_, region) = res!(doc::verify(&buf)); |
| 269 | res!(req(name, "tree region length", &try_into!(u64, region.len()), &meta.tree_len)); |
| 270 | let hash = res!(doc::hash_tree(art.env().hash_scheme, region)); |
| 271 | res!(req(name, "the hash of the payload region", &hash, &meta.hash)); |
| 272 | |
| 273 | // `doc.jdat` is the payload, and the payload is `doc.sbj`. What the payload IS decides how it |
| 274 | // is read back and how it is rebuilt: a node tree carries `usr` nodes and is written by |
| 275 | // `doc::write`, while a post and a card are flat maps with no node labels in them at all. |
| 276 | let signer = res!(keys.author.signer()); |
| 277 | let rebuilt = match art.payload() { |
| 278 | Payload::Tree { tree, .. } => { |
| 279 | // The tree is the shape `meta.jdat` says it is. |
| 280 | let stats = res!(validate::validate(tree, &art.env().schema)); |
| 281 | res!(req(name, "node count", &Some(try_into!(u64, stats.nodes)), &meta.nodes)); |
| 282 | res!(req(name, "depth", &Some(try_into!(u64, stats.depth)), &meta.depth)); |
| 283 | let from_text = res!(read_tree(&dir.join(DOC_JDAT), name)); |
| 284 | res!(req(name, "the tree of doc.jdat against the tree of doc.sbj", &from_text, tree)); |
| 285 | res!(rewrite(&from_text, &meta, &signer)) |
| 286 | }, |
| 287 | Payload::Post(post) => { |
| 288 | res!(req(name, "node count", &None, &meta.nodes)); |
| 289 | let from_text = res!(read_payload(&dir.join(DOC_JDAT), name)); |
| 290 | let back = res!(Post::from_dat(&from_text)); |
| 291 | res!(req(name, "the post of doc.jdat against the post of doc.sbj", &back, post)); |
| 292 | res!(doc::write_artefact(&Payload::Post(back), &signer, meta.time)) |
| 293 | }, |
| 294 | Payload::Card(card) => { |
| 295 | res!(req(name, "node count", &None, &meta.nodes)); |
| 296 | let from_text = res!(read_payload(&dir.join(DOC_JDAT), name)); |
| 297 | let back = res!(Card::from_dat(&from_text)); |
| 298 | res!(req(name, "the card of doc.jdat against the card of doc.sbj", &back, card)); |
| 299 | res!(doc::write_artefact(&Payload::Card(back), &signer, meta.time)) |
| 300 | }, |
| 301 | Payload::Share(share) => { |
| 302 | res!(req(name, "node count", &None, &meta.nodes)); |
| 303 | let from_text = res!(read_payload(&dir.join(DOC_JDAT), name)); |
| 304 | let back = res!(Share::from_dat(&from_text)); |
| 305 | res!(req(name, "the share of doc.jdat against the share of doc.sbj", &back, share)); |
| 306 | res!(doc::write_artefact(&Payload::Share(back), &signer, meta.time)) |
| 307 | }, |
| 308 | }; |
| 309 | if rebuilt != buf { |
| 310 | return Err(err!( |
| 311 | "The fixture '{}' does not rebuild: writing the document of '{}' with the committed \ |
| 312 | key gives {} bytes, and the committed '{}' is {} bytes{}. A document written twice is \ |
| 313 | the same document, so either the encoder has changed or the fixture is stale; \ |
| 314 | regenerate the fixtures if the change was meant.", |
| 315 | name, DOC_JDAT, rebuilt.len(), DOC_SBJ, buf.len(), |
| 316 | match common::first_diff(&rebuilt, &buf) { |
| 317 | Some(at) => fmt!(", first differing at byte {}", at), |
| 318 | None => String::new(), |
| 319 | }; |
| 320 | Test, Invalid, Mismatch)); |
| 321 | } |
| 322 | |
| 323 | // The index of §1.4 is derived data lying outside the hash, and is never trusted: whatever it |
| 324 | // says is checked against the tree it claims to describe. |
| 325 | let rest = res!(doc::index_region(&buf)); |
| 326 | if meta.index { |
| 327 | if rest.is_empty() { |
| 328 | return Err(err!( |
| 329 | "The fixture '{}' declares an index, and carries none.", name; |
| 330 | Test, Missing)); |
| 331 | } |
| 332 | let idx = res!(index::parse(rest)); |
| 333 | res!(index::check(region, &idx)); |
| 334 | res!(req(name, "the entries of the index", &Some(try_into!(u64, idx.len())), &meta.nodes)); |
| 335 | } else if !rest.is_empty() { |
| 336 | return Err(err!( |
| 337 | "The fixture '{}' declares no index, and carries {} trailing bytes.", |
| 338 | name, rest.len(); |
| 339 | Test, Invalid, Unexpected)); |
| 340 | } |
| 341 | Ok(()) |
| 342 | } |
| 343 | |
| 344 | /// Writes a document as its fixture declares it was written. |
| 345 | fn rewrite( |
| 346 | tree: &Dat, |
| 347 | meta: &Meta, |
| 348 | signer: &SignatureScheme, |
| 349 | ) |
| 350 | -> Outcome<Vec<u8>> |
| 351 | { |
| 352 | if meta.index { |
| 353 | doc::write_with_index(tree, &meta.schema, signer, meta.time) |
| 354 | } else { |
| 355 | doc::write(tree, &meta.schema, signer, meta.time) |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | /// Runs a rejection fixture: the artefact is refused, at the step declared, for the reason declared. |
| 360 | /// |
| 361 | /// "It was rejected" is not the claim, since a reader that refused every document would satisfy it. |
| 362 | /// The claim is that the rule named in `reject.jdat` is the rule that caught it, that the failure |
| 363 | /// names the node or the byte it declares, and that it happened at the step of §2 it declares, which |
| 364 | /// is what holds the implementation to verifying before it parses. |
| 365 | fn reject( |
| 366 | dir: &Path, |
| 367 | name: &str, |
| 368 | ) |
| 369 | -> Outcome<()> |
| 370 | { |
| 371 | let buf = res!(common::read_bytes(&dir.join(DOC_SBJ))); |
| 372 | let dec = res!(Reject::from_dat(&res!(common::from_jdat_plain( |
| 373 | &res!(common::read_text(&dir.join(REJECT_JDAT))) |
| 374 | )))); |
| 375 | |
| 376 | // A fixture whose content is wrong must verify first: steps 1 to 5 touch no content, and a |
| 377 | // document that fails them is never decoded. A fixture whose container is wrong must fail them. |
| 378 | match (dec.stage.verifies(), doc::verify_only(&buf)) { |
| 379 | (true, Err(e)) => return Err(err!(e, |
| 380 | "The fixture '{}' declares that it is refused at the '{}' step of SPEC.md §2, which \ |
| 381 | comes after verification, but it does not verify. Either the fixture is wrong about \ |
| 382 | what is wrong with it, or the reader is refusing it for a reason that is not the \ |
| 383 | fixture's.", name, dec.stage.label(); |
| 384 | Test, Invalid)), |
| 385 | (false, Ok(_)) => return Err(err!( |
| 386 | "The fixture '{}' declares that it is refused at the '{}' step of SPEC.md §2, which is \ |
| 387 | one of the steps that touch no content, and it verified. A document that fails \ |
| 388 | verification must never be decoded, so a reader that verified this one has already \ |
| 389 | gone further than the format allows.", name, dec.stage.label(); |
| 390 | Test, Invalid)), |
| 391 | _ => (), |
| 392 | } |
| 393 | |
| 394 | let msg = match doc::read_artefact(&buf) { |
| 395 | Ok(_) => return Err(err!( |
| 396 | "The fixture '{}' was read. It breaks {} A document that fails at any step renders as \ |
| 397 | an error card and is never partially displayed.", name, dec.rule; |
| 398 | Test, Invalid)), |
| 399 | Err(e) => fmt!("{}", e), |
| 400 | }; |
| 401 | |
| 402 | // SPEC.md §6: every rejection names the failing thing and the rule broken. "Invalid document" is |
| 403 | // not an error message. |
| 404 | if !msg.contains(&dec.says) { |
| 405 | return Err(err!( |
| 406 | "The fixture '{}' was refused, and not for the reason it declares. It breaks {} The \ |
| 407 | rejection must say '{}', and says: {}", name, dec.rule, dec.says, msg; |
| 408 | Test, Invalid, Mismatch)); |
| 409 | } |
| 410 | if let Some(id) = dec.node { |
| 411 | let names_it = fmt!("Node {}", id); |
| 412 | if !msg.contains(&names_it) { |
| 413 | return Err(err!( |
| 414 | "The fixture '{}' was refused for the right reason, and did not name the node it \ |
| 415 | is: SPEC.md §6 requires the rejection to name '{}', and it says: {}", |
| 416 | name, names_it, msg; |
| 417 | Test, Invalid, Mismatch)); |
| 418 | } |
| 419 | } |
| 420 | if let Some(off) = dec.offset { |
| 421 | let names_it = fmt!("byte {}", off); |
| 422 | if !msg.contains(&names_it) { |
| 423 | return Err(err!( |
| 424 | "The fixture '{}' was refused for the right reason, and did not name the byte it \ |
| 425 | is at: SPEC.md §6 requires the rejection to name '{}', and it says: {}", |
| 426 | name, names_it, msg; |
| 427 | Test, Invalid, Mismatch)); |
| 428 | } |
| 429 | } |
| 430 | |
| 431 | // Where the fault is in the tree rather than in the bytes, the fixture carries the tree, and the |
| 432 | // tree it carries is the one in the artefact. |
| 433 | let jdat = dir.join(DOC_JDAT); |
| 434 | if jdat.is_file() { |
| 435 | // A node tree is written with the `sbj_` node labels and a record without them, so which |
| 436 | // codec reads it back is decided by what the envelope says the payload is -- read here |
| 437 | // WITHOUT verifying, since a fixture that fails at the header has no readable envelope and |
| 438 | // carries no `doc.jdat` either. |
| 439 | let payload = match envelope_schema(&buf) { |
| 440 | Some(schema) if schema == SCHEMA_POST || schema == SCHEMA_CARD |
| 441 | || schema == SCHEMA_SHARE => |
| 442 | res!(read_payload(&jdat, name)), |
| 443 | _ => res!(read_tree(&jdat, name)), |
| 444 | }; |
| 445 | let bytes = res!(common::encode_unchecked(&payload)); |
| 446 | let start = res!(common::tree_start(&buf)); |
| 447 | if bytes.as_slice() != &buf[start..] { |
| 448 | return Err(err!( |
| 449 | "The fixture '{}' carries a '{}' that is not the tree region of its '{}': the tree \ |
| 450 | encodes to {} bytes and the region is {} bytes{}.", |
| 451 | name, DOC_JDAT, DOC_SBJ, bytes.len(), buf.len() - start, |
| 452 | match common::first_diff(&bytes, &buf[start..]) { |
| 453 | Some(at) => fmt!(", first differing at byte {}", at), |
| 454 | None => String::new(), |
| 455 | }; |
| 456 | Test, Invalid, Mismatch)); |
| 457 | } |
| 458 | } |
| 459 | Ok(()) |
| 460 | } |
| 461 | |
| 462 | /// Reads the flat payload a fixture's `doc.jdat` carries, for a schema that is not a node tree. |
| 463 | /// |
| 464 | /// Plain JDAT, with none of the `sbj_` node labels: a post and a card carry no `usr` daticles, so |
| 465 | /// the codec that knows the node vocabulary has nothing to do here and reading with it would only |
| 466 | /// make a fixture depend on a table it does not use. |
| 467 | fn read_payload( |
| 468 | path: &Path, |
| 469 | name: &str, |
| 470 | ) |
| 471 | -> Outcome<Dat> |
| 472 | { |
| 473 | let s = res!(common::read_text(path)); |
| 474 | match common::from_jdat_plain(&s) { |
| 475 | Ok(d) => Ok(d), |
| 476 | Err(e) => Err(err!(e, |
| 477 | "The '{}' of the fixture '{}' is not readable JDAT.", DOC_JDAT, name; |
| 478 | Test, Invalid)), |
| 479 | } |
| 480 | } |
| 481 | |
| 482 | /// The schema a file's envelope declares, or `None` if the file has no readable envelope. |
| 483 | /// |
| 484 | /// Deliberately UNVERIFIED, and used for nothing but choosing which text codec reads a fixture's |
| 485 | /// `doc.jdat`. A rejection fixture is a file with something wrong with it, so the envelope may be |
| 486 | /// the wrong thing about it; nothing here believes what it says beyond picking a reader. |
| 487 | fn envelope_schema(buf: &[u8]) -> Option<String> { |
| 488 | let hdr = match envelope::read_header(buf) { |
| 489 | Ok(h) => h, |
| 490 | Err(_) => return None, |
| 491 | }; |
| 492 | let end = HEADER_LEN + hdr.env_len as usize; |
| 493 | if buf.len() < end { |
| 494 | return None; |
| 495 | } |
| 496 | match envelope::Envelope::decode(&buf[HEADER_LEN..end]) { |
| 497 | Ok(env) => Some(env.schema), |
| 498 | Err(_) => None, |
| 499 | } |
| 500 | } |
| 501 | |
| 502 | /// Reads the tree a fixture's `doc.jdat` carries, naming the fixture if it will not read. |
| 503 | fn read_tree( |
| 504 | path: &Path, |
| 505 | name: &str, |
| 506 | ) |
| 507 | -> Outcome<Dat> |
| 508 | { |
| 509 | let s = res!(common::read_text(path)); |
| 510 | match common::from_jdat(&s) { |
| 511 | Ok(tree) => Ok(tree), |
| 512 | Err(e) => Err(err!(e, |
| 513 | "The '{}' of the fixture '{}' is not readable JDAT.", DOC_JDAT, name; |
| 514 | Test, Invalid)), |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | /// Requires two things to be equal, naming the fixture and what was compared. |
| 519 | /// |
| 520 | /// A fixture that fails says which fixture, what was compared, what was declared and what was found, |
| 521 | /// since a suite that reports "assertion failed" of a fixture nobody named is as much use as the |
| 522 | /// error message §6 forbids. |
| 523 | fn req<T: PartialEq + std::fmt::Debug>( |
| 524 | name: &str, |
| 525 | what: &str, |
| 526 | got: &T, |
| 527 | want: &T, |
| 528 | ) |
| 529 | -> Outcome<()> |
| 530 | { |
| 531 | if got == want { |
| 532 | Ok(()) |
| 533 | } else { |
| 534 | Err(err!( |
| 535 | "The fixture '{}' declares {} to be {:?}, and it is {:?}.", name, what, want, got; |
| 536 | Test, Invalid, Mismatch)) |
| 537 | } |
| 538 | } |