#![allow(
clippy::cast_possible_truncation,
reason = "generator indices are tiny by construction"
)]
use gedcomkit::dates::GedcomDate;
use gedcomkit::{Document, Node, decode_gedcom};
const LINES: &[&str] = &[
"0 HEAD",
"1 GEDC",
"2 VERS 7.0",
"0 @I1@ INDI",
"0 _PUBLISH",
"1 NAME Ada /Example/",
"1 NOTE leading space kept",
"2 CONT",
"2 CONC and more",
"1 _EXT @VOID@",
"0 TRLR",
];
const DATE_PIECES: &[&str] = &[
"ABT",
"BET",
"AND",
"FROM",
"TO",
"1882",
"99999",
"JAN",
"13",
"0",
"/",
".",
"-",
"(",
")",
"@#DJULIAN@",
"BCE",
"février",
"<",
">",
"th",
"age 26",
"\u{a0}",
"",
];
struct Rng(u64);
impl Rng {
const fn new(seed: u64) -> Self {
Self(seed | 1)
}
const fn next(&mut self) -> u64 {
let mut value = self.0;
value ^= value >> 12;
value ^= value << 25;
value ^= value >> 27;
self.0 = value;
value.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
const fn below(&mut self, ceiling: u64) -> u64 {
if ceiling == 0 {
0
} else {
self.next() % ceiling
}
}
}
fn arbitrary_tag(rng: &mut Rng) -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
let length = 1 + rng.below(6) as usize;
(0..length)
.map(|_| char::from(ALPHABET[rng.below(ALPHABET.len() as u64) as usize]))
.collect()
}
fn arbitrary_payload(rng: &mut Rng) -> String {
const PIECES: &[&str] = &[
"alpha",
"Testwright",
"1882",
" doubled",
"naïve",
"@VOID@",
"line\nbreak",
"…",
"/slash/",
"trailing ",
"a\nb\nc",
"5 JAN 1882",
];
let count = 1 + rng.below(3);
let mut payload = String::new();
for index in 0..count {
if index > 0 {
payload.push(' ');
}
payload.push_str(PIECES[rng.below(PIECES.len() as u64) as usize]);
}
payload
}
fn arbitrary_document(rng: &mut Rng) -> Document {
let mut document = Document::new_v7();
let records = 1 + rng.below(6) as usize;
for index in 0..records {
let mut record = Node::record(format!("@X{index}@"), arbitrary_tag(rng));
let children = rng.below(4);
for _ in 0..children {
let mut child = Node::with_value(arbitrary_tag(rng), arbitrary_payload(rng));
if rng.below(2) == 0 {
child.push(Node::with_value(arbitrary_tag(rng), arbitrary_payload(rng)));
}
record.push(child);
}
let position = document.records.len() - 1;
document.records.insert(position, record);
}
document
}
#[test]
fn any_document_the_builders_produce_reparses_to_the_same_text() {
let mut rng = Rng::new(0x00D0_C5EE_D000_0001);
for case in 0..2_000 {
let document = arbitrary_document(&mut rng);
let text = document.to_text();
let reread = Document::parse(&text).unwrap_or_else(|error| {
panic!("case {case}: built text did not parse: {error}\n{text}")
});
assert_eq!(reread.to_text(), text, "case {case} was not idempotent");
}
}
#[test]
fn parsing_and_writing_arbitrary_parseable_text_is_idempotent() {
let mut rng = Rng::new(0x00D0_C5EE_D000_0002);
for case in 0..2_000 {
let count = 1 + rng.below(12);
let mut text = String::from("0 HEAD\n");
for _ in 0..count {
text.push_str(LINES[rng.below(LINES.len() as u64) as usize]);
text.push('\n');
}
let Ok(document) = Document::parse(&text) else {
continue; };
let written = document.to_text();
let again = Document::parse(&written)
.unwrap_or_else(|error| panic!("case {case}: output did not re-parse: {error}"));
assert_eq!(again.to_text(), written, "case {case} was not idempotent");
}
}
#[test]
fn set_logical_value_inverts_logical_value_for_arbitrary_text() {
let mut rng = Rng::new(0x00D0_C5EE_D000_0003);
for case in 0..2_000 {
let payload = arbitrary_payload(&mut rng);
let mut node = Node::with_value("NOTE", "old");
node.push(Node::with_value("SOUR", "@S1@"));
node.set_logical_value(&payload);
assert_eq!(node.logical_value(), payload, "case {case}");
assert_eq!(
node.all("SOUR").count(),
1,
"case {case} lost an unrelated substructure"
);
}
}
#[test]
fn the_date_parser_never_panics_and_never_loses_the_original() {
let mut rng = Rng::new(0x00D0_C5EE_D000_0004);
for _ in 0..5_000 {
let count = rng.below(6);
let mut payload = String::new();
for index in 0..count {
if index > 0 {
payload.push(' ');
}
payload.push_str(DATE_PIECES[rng.below(DATE_PIECES.len() as u64) as usize]);
}
let date = GedcomDate::parse(&payload);
assert_eq!(date.original, payload, "the original is never rewritten");
}
}
#[test]
fn the_decoder_never_panics_on_arbitrary_bytes() {
let mut rng = Rng::new(0x00D0_C5EE_D000_0005);
for _ in 0..2_000 {
let length = rng.below(512) as usize;
let bytes: Vec<u8> = (0..length).map(|_| (rng.next() & 0xFF) as u8).collect();
if let Ok((text, _)) = decode_gedcom(&bytes) {
let _ = Document::parse(&text);
}
}
}