use std::io::Write;
use std::process::{Command, Output, Stdio};
const BIN: &str = env!("CARGO_BIN_EXE_hl7probe");
fn run(args: &[&str]) -> Output {
Command::new(BIN)
.args(args)
.env("NO_COLOR", "1")
.output()
.expect("binary should run")
}
fn pipe(args: &[&str], stdin: &str) -> Output {
let mut child = Command::new(BIN)
.args(args)
.env("NO_COLOR", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("binary should run");
child
.stdin
.as_mut()
.unwrap()
.write_all(stdin.as_bytes())
.unwrap();
child.wait_with_output().unwrap()
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).to_string()
}
fn code(out: &Output) -> i32 {
out.status.code().unwrap_or(-1)
}
#[test]
fn reports_a_clean_message_and_exits_zero() {
let out = run(&["examples/adt_a01.hl7"]);
let text = stdout(&out);
assert_eq!(code(&out), 0, "{}", text);
assert!(text.contains("HL7 v2.5.1"));
assert!(text.contains("ADT^A01"));
assert!(text.contains("Admit / Visit Notification"));
assert!(text.contains("Patient Name"));
assert!(text.contains("John A Smith"));
assert!(text.contains("message passes all checks"));
}
#[test]
fn invalid_message_lists_findings_and_exits_one() {
let out = run(&["examples/invalid.hl7"]);
let text = stdout(&out);
assert_eq!(code(&out), 1, "{}", text);
for expected in [
"required segment is missing",
"PID-7",
"not a valid date/time",
"PV1-3",
"invalid location",
"OBX-5",
] {
assert!(
text.contains(expected),
"missing {:?} in:\n{}",
expected,
text
);
}
}
#[test]
fn strict_mode_fails_on_warnings_only() {
let warning_only = "MSH|^~\\&|HIS|MERCY|LIS|LAB|20240115143200||ADT^A01|MSG1|P|2.5.1\r\
EVN|A01|20240115143200||||20240115143000\r\
PID|1||123456^^^MERCY^MR||Smith^John||19850312|M\r\
PV1|1|O|CLINIC^1\r";
assert_eq!(code(&pipe(&["-q"], warning_only)), 0);
assert_eq!(code(&pipe(&["-q", "--strict"], warning_only)), 1);
}
#[test]
fn unreadable_input_exits_two() {
let out = run(&["no/such/file.hl7"]);
assert_eq!(code(&out), 2);
assert!(String::from_utf8_lossy(&out.stderr).contains("no/such/file.hl7"));
let out = pipe(&[], "this is not an HL7 message\n");
assert_eq!(code(&out), 2);
assert!(String::from_utf8_lossy(&out.stderr).contains("no MSH segment"));
}
#[test]
fn quiet_mode_prints_one_line_per_message() {
let out = run(&["-q", "examples/adt_a01.hl7", "examples/batch.hl7"]);
let text = stdout(&out);
let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
assert_eq!(lines.len(), 3, "{}", text);
assert!(lines[0].starts_with("adt_a01.hl7"));
assert!(lines[1].starts_with("batch.hl7#1"));
assert!(lines[2].starts_with("batch.hl7#2"));
}
#[test]
fn segment_filter_limits_the_field_tables() {
let text = stdout(&run(&["-s", "PID", "examples/adt_a01.hl7"]));
assert!(text.contains("Patient Identifier List"));
assert!(
!text.contains("Assigned Patient Location"),
"PV1 table should be hidden"
);
assert!(
text.contains("PV1"),
"the segment overview still lists every segment"
);
}
#[test]
fn field_query_prints_raw_values() {
assert_eq!(
stdout(&run(&["-f", "PID-5.1", "examples/adt_a01.hl7"])).trim(),
"Smith"
);
assert_eq!(
stdout(&run(&["-f", "MSH-10", "examples/adt_a01.hl7"])).trim(),
"MSG00001"
);
assert_eq!(
stdout(&run(&["-f", "OBX[2]-5", "examples/oru_r01.hl7"])).trim(),
"39.1"
);
assert_eq!(
stdout(&run(&["-f", "PV1-3.2", "examples/adt_a01.hl7"])).trim(),
"101"
);
let ids = stdout(&run(&["-f", "PID-3.1", "examples/adt_a01.hl7"]));
assert_eq!(ids.lines().collect::<Vec<_>>(), vec!["123456", "987654321"]);
assert_eq!(code(&run(&["-f", "ZZZ-1", "examples/adt_a01.hl7"])), 1);
assert_eq!(code(&run(&["-f", "nonsense", "examples/adt_a01.hl7"])), 2);
}
#[test]
fn json_output_is_machine_readable() {
let out = run(&["--json", "examples/invalid.hl7"]);
assert_eq!(code(&out), 1);
let text = stdout(&out);
let value: serde_json::Value = serde_json::from_str(&text).expect("valid JSON");
assert_eq!(value["status"], "error");
let message = &value["files"][0]["messages"][0];
assert_eq!(message["type"], "ADT^A01");
assert_eq!(message["version"], "2.5.1");
assert_eq!(message["control_id"], "MSG00002");
assert!(message["summary"]["errors"].as_u64().unwrap() >= 4);
let findings = message["findings"].as_array().unwrap();
assert!(findings
.iter()
.any(|f| f["location"] == "PV1-3" && f["severity"] == "error"));
let segments = message["segments"].as_array().unwrap();
assert_eq!(segments[0]["name"], "MSH");
assert!(segments[0]["fields"]
.as_array()
.unwrap()
.iter()
.any(|f| f["name"] == "Version ID"));
}
#[test]
fn reads_a_message_from_a_pipe() {
let out = pipe(
&[],
"MSH|^~\\&|A|B|C|D|20240115143200||ACK|1|P|2.5.1\rMSA|AA|MSG00001\r",
);
assert_eq!(code(&out), 0, "{}", stdout(&out));
assert!(stdout(&out).contains("General Acknowledgment"));
}
#[test]
fn selects_a_single_message_from_a_batch() {
let text = stdout(&run(&["-m", "2", "--summary", "examples/batch.hl7"]));
assert!(text.contains("ADT^A03"));
assert!(!text.contains("ADT^A01"));
assert_eq!(code(&run(&["-m", "9", "examples/batch.hl7"])), 2);
}
#[test]
fn escape_sequences_are_resolved_for_display() {
let out = pipe(
&[],
"MSH|^~\\&|A|B|C|D|20240115143200||ADT^A08|1|P|2.5.1\r\
EVN|A08|20240115143200||||20240115143000\r\
PID|1||1^^^A^MR||O\\T\\Brien^Se\\S\\an||19850312|M|||1 St^^X^IL^1\r\
PV1|1|O|CL^1\r",
);
let text = stdout(&out);
assert!(text.contains("O&Brien^Se^an"), "{}", text);
}
#[test]
fn help_and_version_are_available() {
let help = stdout(&run(&["--help"]));
assert!(help.contains("Reads HL7 v2.x messages"));
assert!(help.contains("Exit status: 0 clean, 1 validation errors, 2 unreadable input"));
assert!(help.contains("--tui"));
assert!(help.contains("EXAMPLES"));
assert!(stdout(&run(&["--version"])).contains(env!("CARGO_PKG_VERSION")));
}