use std::process::{Command, Stdio};
use er7::{Message, RenderOptions, Separators, Terminator};
const ORU: &str = include_str!("../samples/oru_r01.er7");
const ADT: &str = include_str!("../samples/adt_a08.er7");
const BATCH: &str = include_str!("../samples/batch.er7");
fn parse(text: &str) -> Message {
er7::parse(text).expect("sample parses")
}
#[test]
fn every_sample_round_trips_byte_for_byte() {
for sample in [ORU, ADT] {
let options = RenderOptions {
terminator: Terminator::Cr,
trailing_terminator: true,
};
assert_eq!(parse(sample).to_er7_with(options), sample);
}
}
#[test]
fn reads_values_by_path() {
let message = parse(ORU);
assert_eq!(
message.query("PID-5.1").unwrap().as_deref(),
Some("EVERYWOMAN")
);
assert_eq!(
message.query("PID-3.4.2").unwrap().as_deref(),
Some("1.2.840.114398.1.100")
);
assert_eq!(
message.query("OBR-4.2").unwrap().as_deref(),
Some("Lipid Panel")
);
assert_eq!(
message.query_all("OBX-3.2").unwrap(),
vec!["Cholesterol", "Triglycerides", "Comment"]
);
assert_eq!(message.query_all("OBX[2]-5").unwrap(), vec!["102"]);
assert_eq!(
message.query_all("PID-13").unwrap(),
vec!["555-555-1111~555-555-2222"]
);
assert_eq!(
message.query_all("PID-13[2]").unwrap(),
vec!["555-555-2222"]
);
}
#[test]
fn decodes_escape_sequences_only_on_request() {
let message = parse(ORU);
assert_eq!(
message.query("OBX[3]-5").unwrap().as_deref(),
Some("Fasting sample & hemolysed")
);
let path = "OBX[3]-5".parse::<er7::Path>().unwrap();
assert_eq!(
message.query_path_raw(&path),
vec![r"Fasting sample \T\ hemolysed"]
);
}
#[test]
fn reads_the_msh_routing_fields() {
let message = parse(ADT);
assert_eq!(message.message_code().as_deref(), Some("ADT"));
assert_eq!(message.trigger_event().as_deref(), Some("A08"));
assert_eq!(message.message_structure().as_deref(), Some("ADT_A01"));
assert_eq!(message.control_id().as_deref(), Some("MSG00001"));
assert_eq!(message.version().as_deref(), Some("2.5"));
}
#[test]
fn treats_a_local_segment_like_any_other() {
let message = parse(ADT);
assert_eq!(
message.query("ZPD-2.2").unwrap().as_deref(),
Some("EXTENSION")
);
assert!(message.to_er7().contains("ZPD|1|LOCAL^EXTENSION^SEGMENT"));
}
#[test]
fn splits_a_batch_into_messages_that_parse() {
let messages = er7::split_messages(BATCH);
assert_eq!(messages.len(), 2);
let ids: Vec<Option<String>> = messages.iter().map(|m| parse(m).control_id()).collect();
assert_eq!(ids, vec![Some("B1".into()), Some("B2".into())]);
let second = parse(messages[1]);
assert_eq!(second.query("MSA-1").unwrap().as_deref(), Some("AE"));
assert_eq!(second.query("ERR-2.1").unwrap().as_deref(), Some("PID"));
}
#[test]
fn edits_a_value_and_writes_the_message_back() {
let mut message = parse(ORU);
let separators = message.separators;
message
.segment_at_mut("PID", 1)
.and_then(|pid| pid.field_mut(5))
.and_then(|field| field.repetition_mut(1))
.and_then(|repetition| repetition.component_mut(1))
.and_then(|component| component.subcomponent_mut(1))
.expect("PID-5.1 exists")
.set("O'BRIEN & SONS", &separators);
assert!(message.to_er7().contains(r"O'BRIEN \T\ SONS^EVE^E^^MS"));
assert_eq!(
message.query("PID-5.1").unwrap().as_deref(),
Some("O'BRIEN & SONS")
);
assert_eq!(er7::parse(&message.to_er7()).unwrap(), message);
}
#[test]
fn only_a_missing_or_broken_header_is_an_error() {
assert!(er7::parse("").is_err());
assert!(er7::parse("PID|1").is_err());
assert!(er7::parse("MSH").is_err());
let odd = "MSH|^~\\&|LAB\rZZZ\rPID|||||||||||||||||||||||||||||||\rNTE|1||\r\r";
assert_eq!(er7::parse(odd).unwrap().segments.len(), 4);
}
#[test]
fn honors_a_message_that_chooses_its_own_delimiters() {
let text = "MSH#*!?@#LAB#*ACME#SMITH*JOHN@JR!DOE*JANE";
let message = er7::parse(text).unwrap();
assert_eq!(message.separators.field, '#');
assert_eq!(message.separators.subcomponent, '@');
assert_eq!(message.query("MSH-5.2.2").unwrap().as_deref(), Some("JR"));
assert_eq!(message.query_all("MSH-5[2].1").unwrap(), vec!["DOE"]);
assert_eq!(message.to_er7(), text);
}
#[test]
fn parses_a_fragment_that_has_no_header() {
let fragment = er7::parse_with("OBX|1|NM|2093-3^Cholesterol^LN||187", Separators::default());
assert_eq!(fragment.query("OBX-5").unwrap().as_deref(), Some("187"));
}
const EDGES: &str = "MSH|^~\\&#|LAB|ACME|EHR|CLINIC|20260815120000||ADT^A08^ADT_A01|N1|P|2.7\r\
PID|1||9^^^ACME^MR||\"\"||19800101|M\r\
NTE|1||Line one\\.br\\Line two \\S\\ more\\X0D\\tail#";
#[test]
fn carries_the_corners_through_unchanged() {
let message = parse(EDGES);
assert_eq!(message.separators.truncation, Some('#'));
let pid = message.segment("PID").unwrap();
assert!(pid.field(5).unwrap().is_null());
assert!(!pid.field(5).unwrap().is_empty());
assert_eq!(message.query("PID-5").unwrap().as_deref(), Some(""));
assert_eq!(
message.query("NTE-3").unwrap().as_deref(),
Some("Line one\\.br\\Line two ^ more\rtail#")
);
assert_eq!(message.to_er7(), EDGES);
}
fn cli(args: &[&str], stdin: &str) -> (bool, String, String) {
use std::io::Write;
let mut child = Command::new(env!("CARGO_BIN_EXE_er7"))
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("the command builds");
child
.stdin
.take()
.expect("stdin is piped")
.write_all(stdin.as_bytes())
.expect("writes to stdin");
let output = child.wait_with_output().expect("the command finishes");
(
output.status.success(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
#[test]
fn cli_outlines_stdin_by_default() {
let (ok, stdout, _) = cli(&[], ORU);
assert!(ok);
assert!(stdout.contains("PID-5.1 EVERYWOMAN"), "{stdout}");
assert!(stdout.contains("MSH-2 ^~\\&"), "{stdout}");
assert!(stdout.contains("OBX[2]-3.2 Triglycerides"), "{stdout}");
}
#[test]
fn cli_shows_nulls_and_control_characters_legibly() {
let (ok, stdout, _) = cli(&[], EDGES);
assert!(ok);
assert!(stdout.contains("PID-5 \"\"\n"), "{stdout}");
assert!(
stdout.contains("NTE-3 Line one\\.br\\Line two ^ more\\rtail#\n"),
"{stdout}"
);
let (ok, stdout, _) = cli(&["--raw", "-q", "NTE-3"], EDGES);
assert!(ok);
assert_eq!(stdout, "Line one\\.br\\Line two \\S\\ more\\X0D\\tail#\n");
}
#[test]
fn cli_queries_paths_in_order() {
let (ok, stdout, _) = cli(&["-q", "PID-5.1", "-q", "OBX-3.2"], ORU);
assert!(ok);
assert_eq!(stdout, "EVERYWOMAN\nCholesterol\nTriglycerides\nComment\n");
}
#[test]
fn cli_normalizes_to_the_chosen_terminator() {
let (ok, stdout, _) = cli(
&["--normalize", "--terminator", "lf"],
"MSH|^~\\&|A\r\n\r\nPID|1",
);
assert!(ok);
assert_eq!(stdout, "MSH|^~\\&|A\nPID|1\n");
}
#[test]
fn cli_selects_one_message_of_a_batch() {
let (ok, stdout, _) = cli(&["--message", "2", "-q", "MSH-10"], BATCH);
assert!(ok);
assert_eq!(stdout, "B2\n");
let (ok, _, stderr) = cli(&["--message", "9"], BATCH);
assert!(!ok);
assert!(stderr.contains("the input holds 2"), "{stderr}");
}
#[test]
fn cli_reports_errors_on_stderr() {
for (args, input, expected) in [
(vec![], "PID|1", "not the MSH, FHS, or BHS header"),
(vec![], "", "no HL7 segments"),
(vec!["-q", "PID-0"], ORU, "invalid HL7 path"),
(vec!["--nope"], ORU, "unknown option"),
(vec!["-q", "PID-5", "-n"], ORU, "choose one"),
] {
let (ok, stdout, stderr) = cli(&args, input);
assert!(!ok, "expected {args:?} to fail");
assert!(stdout.is_empty(), "{stdout}");
assert!(stderr.contains(expected), "{args:?}: {stderr}");
assert!(stderr.starts_with("er7: error: "), "{stderr}");
}
}
#[test]
fn cli_prints_help_and_version() {
for flag in ["-h", "--help"] {
let (ok, stdout, _) = cli(&[flag], "");
assert!(ok);
assert!(stdout.contains("Usage: er7 [OPTIONS] [FILE]"), "{stdout}");
}
let (ok, stdout, _) = cli(&["--version"], "");
assert!(ok);
assert!(stdout.starts_with("er7 "), "{stdout}");
}