#![expect(
clippy::arithmetic_side_effects,
clippy::assertions_on_result_states,
clippy::indexing_slicing,
clippy::let_underscore_must_use,
clippy::manual_let_else,
clippy::uninlined_format_args,
clippy::unwrap_used,
reason = "pre-existing parser property test debt moved into hl7v2; cleanup is split from topology collapse"
)]
use crate::model::*;
use crate::{get, get_presence, parse, parse_batch};
use proptest::prelude::*;
fn segment_id_strategy() -> impl Strategy<Value = String> {
"[A-Z0-9]{3}"
}
fn _field_value_strategy() -> impl Strategy<Value = String> {
"[A-Za-z0-9 .,_-]{0,50}"
}
fn simple_field_value() -> impl Strategy<Value = String> {
"[A-Za-z0-9]{1,20}"
}
fn msh_segment_strategy() -> impl Strategy<Value = String> {
(
simple_field_value(), simple_field_value(), simple_field_value(), simple_field_value(), "[0-9]{14}", simple_field_value(), simple_field_value(), "[PAT]", "2\\.[0-9]\\.[0-9]", )
.prop_map(
|(app, fac, recv_app, recv_fac, dt, trigger, ctrl, proc, ver)| {
format!(
"MSH|^~\\&|{}|{}|{}|{}|{}||ADT^{}|{}|{}|{}",
app, fac, recv_app, recv_fac, dt, trigger, ctrl, proc, ver
)
},
)
}
fn pid_segment_strategy() -> impl Strategy<Value = String> {
(
simple_field_value(), simple_field_value(), simple_field_value(), )
.prop_map(|(id, last, first)| format!("PID|1||{}^^^HOSP^MR||{}^{}", id, last, first))
}
fn valid_message_strategy() -> impl Strategy<Value = String> {
(
msh_segment_strategy(),
prop::option::of(pid_segment_strategy()),
)
.prop_map(|(msh, pid)| match pid {
Some(pid) => format!("{}\r{}\r", msh, pid),
None => format!("{}\r", msh),
})
}
fn _random_bytes_strategy() -> impl Strategy<Value = Vec<u8>> {
prop::collection::vec(any::<u8>(), 0..1000)
}
proptest! {
#[test]
fn test_roundtrip_simple_message(
app in simple_field_value(),
fac in simple_field_value(),
recv_app in simple_field_value(),
recv_fac in simple_field_value(),
dt in "[0-9]{14}",
trigger in simple_field_value(),
ctrl in simple_field_value(),
proc in "[PAT]",
ver in "2\\.[0-9]\\.[0-9]"
) {
let hl7 = format!(
"MSH|^~\\&|{}|{}|{}|{}|{}||ADT^{}|{}|{}|{}\r",
app, fac, recv_app, recv_fac, dt, trigger, ctrl, proc, ver
);
let message = match parse(hl7.as_bytes()) {
Ok(m) => m,
Err(_) => return Ok(()), };
prop_assert!(!message.segments.is_empty());
prop_assert_eq!(&message.segments[0].id, b"MSH");
}
#[test]
fn test_roundtrip_with_pid(
app in simple_field_value(),
fac in simple_field_value(),
patient_id in simple_field_value(),
last_name in simple_field_value(),
first_name in simple_field_value()
) {
let hl7 = format!(
"MSH|^~\\&|{}|{}|RecvApp|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\rPID|1||{}^^^HOSP^MR||{}^{}\r",
app, fac, patient_id, last_name, first_name
);
let message = parse(hl7.as_bytes())?;
prop_assert_eq!(message.segments.len(), 2);
prop_assert_eq!(&message.segments[0].id, b"MSH");
prop_assert_eq!(&message.segments[1].id, b"PID");
prop_assert_eq!(get(&message, "PID.3.1"), Some(patient_id.as_str()));
prop_assert_eq!(get(&message, "PID.5.1"), Some(last_name.as_str()));
prop_assert_eq!(get(&message, "PID.5.2"), Some(first_name.as_str()));
}
}
proptest! {
#[test]
fn test_random_bytes_never_panics(bytes in prop::collection::vec(any::<u8>(), 0..1000)) {
let _ = parse(&bytes);
}
#[test]
fn test_random_string_never_panics(s in ".*") {
let _ = parse(s.as_bytes());
}
#[test]
fn test_random_bytes_batch_never_panics(bytes in prop::collection::vec(any::<u8>(), 0..1000)) {
let _ = parse_batch(&bytes);
}
}
proptest! {
#[test]
fn test_valid_message_always_parses(hl7 in valid_message_strategy()) {
let result = parse(hl7.as_bytes());
prop_assert!(result.is_ok(), "Valid message should parse: {:?}", result);
let message = result.unwrap();
prop_assert!(!message.segments.is_empty());
prop_assert_eq!(&message.segments[0].id, b"MSH");
}
#[test]
fn test_message_with_repeating_fields(
app in simple_field_value(),
name1 in simple_field_value(),
name2 in simple_field_value(),
name3 in simple_field_value()
) {
let hl7 = format!(
"MSH|^~\\&|{}|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\rPID|1||123||{}~{}~{}\r",
app, name1, name2, name3
);
let message = parse(hl7.as_bytes())?;
prop_assert_eq!(get(&message, "PID.5[1].1"), Some(name1.as_str()));
prop_assert_eq!(get(&message, "PID.5[2].1"), Some(name2.as_str()));
prop_assert_eq!(get(&message, "PID.5[3].1"), Some(name3.as_str()));
}
#[test]
fn test_message_with_components(
id in simple_field_value(),
namespace in simple_field_value(),
type_code in simple_field_value()
) {
let hl7 = format!(
"MSH|^~\\&|App|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\rPID|1||{}^^^{}^{}\r",
id, namespace, type_code
);
let message = parse(hl7.as_bytes())?;
prop_assert_eq!(get(&message, "PID.3.1"), Some(id.as_str()));
prop_assert_eq!(get(&message, "PID.3.4"), Some(namespace.as_str()));
prop_assert_eq!(get(&message, "PID.3.5"), Some(type_code.as_str()));
}
#[test]
fn test_message_with_subcomponents(
value1 in simple_field_value(),
value2 in simple_field_value(),
value3 in simple_field_value()
) {
let hl7 = format!(
"MSH|^~\\&|App|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\rPID|1||123||{}&{}&{}\r",
value1, value2, value3
);
let message = parse(hl7.as_bytes())?;
prop_assert_eq!(get(&message, "PID.5.1"), Some(value1.as_str()));
}
}
proptest! {
#[test]
fn test_empty_field_handling(
app in simple_field_value(),
fac in simple_field_value()
) {
let hl7 = format!(
"MSH|^~\\&|{}|{}|||||ADT^A01|MSG123|P|2.5\rPID|1|||||||\r",
app, fac
);
let message = parse(hl7.as_bytes())?;
match get_presence(&message, "PID.3.1") {
Presence::Empty | Presence::Missing => {}
Presence::Value(_) => prop_assert!(false, "Expected empty or missing"),
Presence::Null => {}
}
}
#[test]
fn test_long_field_value(value in "[A-Za-z0-9]{1,1000}") {
let hl7 = format!(
"MSH|^~\\&|App|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\rPID|1||{}||Test\r",
value
);
let message = parse(hl7.as_bytes())?;
prop_assert_eq!(get(&message, "PID.3.1"), Some(value.as_str()));
}
#[test]
fn test_many_segments(num_segments in 1usize..50) {
let mut hl7 = String::from("MSH|^~\\&|App|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\r");
for i in 0..num_segments {
hl7.push_str(&format!("OBX|{}|ST|Test||Value\r", i));
}
let message = parse(hl7.as_bytes())?;
prop_assert_eq!(message.segments.len(), 1 + num_segments);
}
#[test]
fn test_many_repetitions(num_reps in 1usize..20) {
let mut field_value = String::new();
for i in 0..num_reps {
if i > 0 {
field_value.push('~');
}
field_value.push_str(&format!("Name{}", i));
}
let hl7 = format!(
"MSH|^~\\&|App|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\rPID|1||123||{}\r",
field_value
);
let message = parse(hl7.as_bytes())?;
prop_assert_eq!(get(&message, "PID.5.1"), Some("Name0"));
let last_name = format!("Name{}", num_reps - 1);
prop_assert_eq!(get(&message, &format!("PID.5[{}].1", num_reps)), Some(last_name.as_str()));
}
}
#[test]
fn test_delimiter_uniqueness_required() {
let hl7 = "MSH|||||App|Fac\r";
let result = parse(hl7.as_bytes());
assert!(result.is_err());
}
#[test]
fn test_standard_delimiters_work() {
let hl7 = "MSH|^~\\&|App|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\r";
let message = parse(hl7.as_bytes()).unwrap();
assert_eq!(message.delims.field, '|');
assert_eq!(message.delims.comp, '^');
assert_eq!(message.delims.rep, '~');
assert_eq!(message.delims.esc, '\\');
assert_eq!(message.delims.sub, '&');
}
proptest! {
#[test]
fn test_valid_segment_ids(seg_id in segment_id_strategy()) {
let hl7 = format!(
"MSH|^~\\&|App|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\r{}|1\r",
seg_id
);
let result = parse(hl7.as_bytes());
prop_assert!(result.is_ok());
}
}
proptest! {
#[test]
fn test_field_access_consistency(
app in simple_field_value(),
fac in simple_field_value(),
recv_app in simple_field_value(),
recv_fac in simple_field_value()
) {
let hl7 = format!(
"MSH|^~\\&|{}|{}|{}|{}|20250128120000||ADT^A01|MSG123|P|2.5\r",
app, fac, recv_app, recv_fac
);
let message = parse(hl7.as_bytes())?;
prop_assert_eq!(get(&message, "MSH.3"), Some(app.as_str()));
prop_assert_eq!(get(&message, "MSH.4"), Some(fac.as_str()));
prop_assert_eq!(get(&message, "MSH.5"), Some(recv_app.as_str()));
prop_assert_eq!(get(&message, "MSH.6"), Some(recv_fac.as_str()));
}
#[test]
fn test_missing_field_returns_none(field_num in 100usize..1000) {
let hl7 = "MSH|^~\\&|App|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\rPID|1||123||Test\r";
let message = parse(hl7.as_bytes())?;
let path = format!("PID.{}.1", field_num);
prop_assert_eq!(get(&message, &path), None);
}
}
proptest! {
#[test]
fn test_single_message_as_batch(
app in simple_field_value(),
patient_id in simple_field_value()
) {
let hl7 = format!(
"MSH|^~\\&|{}|Fac|Recv|RecvFac|20250128120000||ADT^A01|MSG123|P|2.5\rPID|1||{}||Test\r",
app, patient_id
);
let batch = parse_batch(hl7.as_bytes())?;
prop_assert!(batch.header.is_none());
prop_assert!(batch.trailer.is_none());
prop_assert_eq!(batch.messages.len(), 1);
}
}