use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use mrrc::{AuthorityMarcReader, HoldingsMarcReader, MarcReader, RecoveryMode, ValidationLevel};
fn fixture_bytes(name: &str) -> Vec<u8> {
let path: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/error_fixtures")
.join(name);
fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()))
}
fn data_bytes(name: &str) -> Vec<u8> {
let path: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data")
.join(name);
fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()))
}
fn build_reader(
bytes: &[u8],
validation: ValidationLevel,
recovery: RecoveryMode,
) -> MarcReader<Cursor<Vec<u8>>> {
MarcReader::new(Cursor::new(bytes.to_vec()))
.with_recovery_mode(recovery)
.with_validation_level(validation)
}
const E201_FIXTURE: &str = "e201_bad_indicator.bin";
fn assert_clean_via_iter(bytes: &[u8], validation: ValidationLevel, recovery: RecoveryMode) {
let mut reader = build_reader(bytes, validation, recovery);
let mut count = 0;
while let Ok(Some(rec)) = reader.read_record() {
let errs = &rec.errors;
assert!(
errs.is_empty(),
"({validation:?}, {recovery:?}, iter): expected empty errors, got {errs:?}",
);
count += 1;
}
assert!(
count > 0,
"({validation:?}, {recovery:?}, iter): yielded no records",
);
}
fn assert_clean_via_iter_with_errors(
bytes: &[u8],
validation: ValidationLevel,
recovery: RecoveryMode,
) {
let mut reader = build_reader(bytes, validation, recovery);
let mut count = 0;
for entry in reader.iter_with_errors() {
let (rec, errs) = entry.unwrap_or_else(|e| {
panic!(
"({validation:?}, {recovery:?}, iter_with_errors): expected clean parse, got {e:?}"
)
});
assert!(
errs.is_empty() && rec.errors.is_empty(),
"({validation:?}, {recovery:?}, iter_with_errors): expected empty errors, got {errs:?}",
);
count += 1;
}
assert!(
count > 0,
"({validation:?}, {recovery:?}, iter_with_errors): yielded no records",
);
}
#[test]
fn cell_structural_strict_iter() {
assert_clean_via_iter(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::Structural,
RecoveryMode::Strict,
);
}
#[test]
fn cell_structural_strict_iter_with_errors() {
assert_clean_via_iter_with_errors(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::Structural,
RecoveryMode::Strict,
);
}
#[test]
fn cell_structural_lenient_iter() {
assert_clean_via_iter(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::Structural,
RecoveryMode::Lenient,
);
}
#[test]
fn cell_structural_lenient_iter_with_errors() {
assert_clean_via_iter_with_errors(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::Structural,
RecoveryMode::Lenient,
);
}
#[test]
fn cell_structural_permissive_iter() {
assert_clean_via_iter(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::Structural,
RecoveryMode::Permissive,
);
}
#[test]
fn cell_structural_permissive_iter_with_errors() {
assert_clean_via_iter_with_errors(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::Structural,
RecoveryMode::Permissive,
);
}
#[test]
fn cell_strict_marc_strict_iter() {
let mut reader = build_reader(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::StrictMarc,
RecoveryMode::Strict,
);
let result = reader.read_record();
assert!(
result.is_err(),
"expected Err in StrictMarc+Strict; got {result:?}"
);
assert_eq!(result.unwrap_err().code(), "E201");
}
#[test]
fn cell_strict_marc_strict_iter_with_errors() {
let mut reader = build_reader(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::StrictMarc,
RecoveryMode::Strict,
);
let entry = reader.iter_with_errors().next();
let result = entry.expect("expected Some(Err)");
assert!(result.is_err(), "expected Err item from iter_with_errors");
assert_eq!(result.unwrap_err().code(), "E201");
}
#[test]
fn cell_strict_marc_lenient_iter() {
let mut reader = build_reader(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::StrictMarc,
RecoveryMode::Lenient,
);
let rec = reader
.read_record()
.expect("expected Ok(Some) in lenient")
.expect("expected a record");
assert!(
!rec.errors.is_empty(),
"expected populated errors in lenient"
);
assert_eq!(rec.errors[0].code(), "E201");
}
#[test]
fn cell_strict_marc_lenient_iter_with_errors() {
let mut reader = build_reader(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::StrictMarc,
RecoveryMode::Lenient,
);
let (rec, errs) = reader
.iter_with_errors()
.next()
.expect("expected one item")
.expect("expected Ok");
assert!(!errs.is_empty(), "expected populated errors tuple element");
assert!(
!rec.errors.is_empty(),
"expected record.errors also populated (same data, two surfaces)"
);
assert_eq!(errs[0].code(), "E201");
assert_eq!(rec.errors.len(), errs.len(), "two surfaces should agree");
}
#[test]
fn cell_strict_marc_permissive_iter() {
let mut reader = build_reader(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::StrictMarc,
RecoveryMode::Permissive,
);
let rec = reader
.read_record()
.expect("expected Ok(Some) in permissive")
.expect("expected a record");
assert!(
!rec.errors.is_empty(),
"expected populated errors in permissive"
);
assert_eq!(rec.errors[0].code(), "E201");
}
#[test]
fn cell_strict_marc_permissive_iter_with_errors() {
let mut reader = build_reader(
&fixture_bytes(E201_FIXTURE),
ValidationLevel::StrictMarc,
RecoveryMode::Permissive,
);
let (rec, errs) = reader
.iter_with_errors()
.next()
.expect("expected one item")
.expect("expected Ok");
assert!(!errs.is_empty(), "expected populated errors tuple element");
assert_eq!(errs[0].code(), "E201");
assert_eq!(rec.errors.len(), errs.len(), "two surfaces should agree");
}
fn assert_code_captured(fixture: &str, expected_code: &str) {
let mut reader = build_reader(
&fixture_bytes(fixture),
ValidationLevel::StrictMarc,
RecoveryMode::Lenient,
);
let rec = reader
.read_record()
.expect("expected Ok(Some)")
.expect("expected a record");
assert!(
!rec.errors.is_empty(),
"{fixture}: errors should be populated"
);
let err = &rec.errors[0];
assert_eq!(err.code(), expected_code, "{fixture}: wrong error code");
let dict = err.to_json_value();
assert!(
dict.get("record_index").is_some_and(|v| !v.is_null()),
"{fixture}: record_index missing in captured error: {dict}"
);
}
#[test]
fn captures_e201_with_context() {
assert_code_captured("e201_bad_indicator.bin", "E201");
}
#[test]
fn captures_e202_with_context() {
assert_code_captured("e202_non_printable_subfield_code.bin", "E202");
}
#[test]
fn captures_e301_with_context() {
assert_code_captured("e301_invalid_utf8_in_subfield.bin", "E301");
}
#[test]
fn captures_e005_with_context() {
assert_code_captured("e005_truncated_record.bin", "E005");
}
#[test]
fn captures_e101_with_context() {
assert_code_captured("e101_directory_non_digit_length.bin", "E101");
}
#[test]
fn captures_e106_with_context() {
assert_code_captured("e106_field_length_past_data.bin", "E106");
}
#[test]
fn authority_reader_exposes_diagnostic_surface() {
let bytes = data_bytes("simple_authority.mrc");
let mut reader = AuthorityMarcReader::new(Cursor::new(bytes));
let mut saw_record = false;
for entry in reader.iter_with_errors() {
let (rec, errs) = entry.expect("expected Ok on clean authority fixture");
assert!(errs.is_empty(), "expected empty errors on clean authority");
assert!(rec.errors.is_empty(), "record.errors should also be empty");
saw_record = true;
}
assert!(saw_record, "authority fixture yielded no records");
}
#[test]
fn holdings_reader_exposes_diagnostic_surface() {
use mrrc::{Leader, MarcWriter, Record};
let leader = Leader {
record_length: 0,
record_status: 'n',
record_type: 'x',
bibliographic_level: ' ',
control_record_type: ' ',
character_coding: ' ',
indicator_count: 2,
subfield_code_count: 2,
data_base_address: 0,
encoding_level: ' ',
cataloging_form: ' ',
multipart_level: ' ',
reserved: "4500".to_string(),
};
let mut record = Record::new(leader);
record.add_control_field("001".to_string(), "h001".to_string());
let mut buf: Vec<u8> = Vec::new();
{
let mut writer = MarcWriter::new(&mut buf);
writer
.write_record(&record)
.expect("write holdings test record");
}
let mut reader = HoldingsMarcReader::new(Cursor::new(buf));
let mut saw_record = false;
for entry in reader.iter_with_errors() {
let (rec, errs) = entry.expect("expected Ok on clean inline holdings record");
assert!(errs.is_empty(), "expected empty errors on clean holdings");
assert!(rec.errors.is_empty(), "record.errors should also be empty");
saw_record = true;
}
assert!(saw_record, "inline holdings record yielded nothing");
}