#![allow(
clippy::unwrap_used,
reason = "panicking is the failure mode a test wants"
)]
use hl7probe::parser::{parse_message, split_messages, Segment};
struct Rng(u64);
impl Rng {
const fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
usize::try_from(self.next() % n as u64).unwrap_or(0)
}
const fn byte(&mut self) -> u8 {
(self.next() & 0xff) as u8
}
}
fn examples() -> Vec<Vec<u8>> {
let mut out = Vec::new();
for name in ["adt_a01", "batch", "invalid", "oru_r01"] {
out.push(std::fs::read(format!("examples/{name}.hl7")).unwrap());
}
out
}
fn walk(text: &str) -> usize {
let (raws, _notes) = split_messages(text);
let mut seen = 0;
for raw in &raws {
let Ok(message) = parse_message(raw) else {
continue;
};
let _ = (
message.version(),
message.type_label(),
message.control_id(),
);
let _ = message.message_type();
for segment in &message.segments {
seen += 1;
let _ = (segment.is_custom(), segment.last_populated());
for seq in 1..=segment.last_populated().max(1) {
let _ = (segment.has(seq), segment.text(seq), segment.comp(seq, 1));
let Some(field) = segment.field(seq) else {
continue;
};
let _ = (field.is_empty(), field.is_null(), field.rep_count());
for rep in field.reps() {
let _ = (rep.is_empty(), rep.filled_comps(), rep.text());
for comp in rep.comps() {
let _ = comp.is_empty();
for sub in comp.subs() {
let _ = sub.len();
}
}
}
}
}
}
seen
}
fn rebuilds_its_own_line(segment: &Segment<'_>, field_separator: char) -> bool {
let count = (1..=segment.raw.len())
.take_while(|seq| segment.field(*seq).is_some())
.count();
let fields: Vec<&str> = (1..=count)
.map(|seq| segment.field(seq).unwrap().text())
.collect();
let joiner = field_separator.to_string();
let rebuilt = if segment.name == "MSH" {
let Some(first) = fields.first() else {
return true;
};
format!(
"{}{first}{}",
&segment.raw[..segment.name.len()],
fields[1..].join(&joiner)
)
} else {
let Some(at) = segment.raw.find(field_separator) else {
return fields.is_empty();
};
let head = &segment.raw[..at + field_separator.len_utf8()];
format!("{head}{}", fields.join(&joiner))
};
rebuilt == segment.raw
}
#[test]
fn no_damaged_input_can_make_the_parser_panic() {
let seeds = examples();
let mut rng = Rng(0x5eed_1234_abcd_ef01);
let mut cases = 0;
for seed in &seeds {
for cut in 0..seed.len() {
let text = String::from_utf8_lossy(&seed[..cut]).into_owned();
walk(&text);
cases += 1;
}
for _ in 0..400 {
let mut bytes = seed.clone();
for _ in 0..=rng.below(8) {
let at = rng.below(bytes.len());
bytes[at] = rng.byte();
}
let text = String::from_utf8_lossy(&bytes).into_owned();
walk(&text);
cases += 1;
}
}
for text in [
"",
"\r",
"\r\n\r\n",
"MSH",
"MSH|",
"MSH|^~\\&",
"MSH|^~\\&|\r",
"MSHzzzz\r",
"MSH|^~\\&|\rPID|\r",
"MSH|^~\\&|\rPID|~~~|^^^|&&&\r",
"MSH|^~\\&|\rPID|\\X\\|\\Xzz\\|\\E\\|\\\\\r",
"MSH|^~\\&|\rPID|\u{e9}\u{4e2d}\u{1f600}\r",
"MSH|^^^^|\rPID|a^b\r",
"\u{b}MSH|^~\\&|\r\u{1c}\r",
"FHS|x\rBHS|y\rMSH|^~\\&|\rBTS|1\rFTS|1\r",
] {
walk(text);
cases += 1;
}
assert!(
cases > 2_000,
"the corpus should be substantial, got {cases}"
);
}
#[test]
fn a_decoded_segment_still_adds_up_to_the_line_it_came_from() {
let seeds = examples();
let mut rng = Rng(0x0bad_c0ff_ee12_3456);
let mut checked = 0;
let mut verify = |text: &str| {
let (raws, _notes) = split_messages(text);
for raw in &raws {
let Ok(message) = parse_message(raw) else {
continue;
};
for segment in &message.segments {
assert!(
rebuilds_its_own_line(segment, message.sep.field),
"{} on line {} does not rebuild:\n raw: {:?}",
segment.name,
segment.line,
segment.raw
);
checked += 1;
}
}
};
for seed in &seeds {
verify(&String::from_utf8_lossy(seed));
for _ in 0..300 {
let mut bytes = seed.clone();
for _ in 0..=rng.below(6) {
let at = rng.below(bytes.len());
bytes[at] = rng.byte();
}
verify(&String::from_utf8_lossy(&bytes));
}
}
assert!(checked > 1_000, "expected a real sample, got {checked}");
}
#[test]
fn reading_the_same_text_twice_gives_the_same_answer() {
for seed in examples() {
let text = String::from_utf8_lossy(&seed).into_owned();
let (first, first_notes) = split_messages(&text);
let (second, second_notes) = split_messages(&text);
assert_eq!(first.len(), second.len());
assert_eq!(first_notes, second_notes);
for (a, b) in first.iter().zip(&second) {
let (a, b) = (parse_message(a), parse_message(b));
match (a, b) {
(Ok(a), Ok(b)) => {
assert_eq!(a.segments.len(), b.segments.len());
assert_eq!(a.type_label(), b.type_label());
assert_eq!(a.notes, b.notes);
}
(Err(a), Err(b)) => assert_eq!(a.to_string(), b.to_string()),
_ => panic!("the same text parsed differently on a second reading"),
}
}
}
}