use std::path::Path;
use badness::bib::format;
use badness::bib::linter::{Severity, apply_fixes, check_document};
fn lint(src: &str) -> Vec<(&'static str, Severity)> {
check_document(Path::new("refs.bib"), src)
.into_iter()
.map(|d| (d.rule, d.severity))
.collect()
}
fn rules(src: &str) -> Vec<&'static str> {
lint(src).into_iter().map(|(rule, _)| rule).collect()
}
#[test]
fn clean_file_is_silent() {
let src = "\
@article{knuth1984,
author = {Knuth, Donald E.},
title = {Literate Programming},
journaltitle = {The Computer Journal},
year = 1984,
}
";
assert!(lint(src).is_empty(), "got: {:?}", lint(src));
}
#[test]
fn collects_findings_across_rules_sorted_by_position() {
let src = "\
@string{unused = {Cambridge University Press}}
@article{dup,
title = {First},
journaltitle = {J},
author = {A},
year = 2020,
}
@article{dup,
title = {Second},
bogusfield = {x},
note = {},
}
";
let found = rules(src);
assert_eq!(found.first(), Some(&"unused-string"));
for expected in [
"unused-string",
"duplicate-key",
"missing-required-field",
"unknown-field",
"empty-field",
] {
assert!(found.contains(&expected), "missing {expected}: {found:?}");
}
let diags = check_document(Path::new("refs.bib"), src);
assert!(
diags.windows(2).all(|w| w[0].start <= w[1].start),
"diagnostics not sorted by position"
);
}
#[test]
fn parse_errors_pass_through_as_diagnostics() {
let diags = check_document(Path::new("refs.bib"), "@article{k, title = {unterminated\n");
assert!(
diags
.iter()
.any(|d| d.rule == "parse" && d.severity == Severity::Error),
"expected a parse diagnostic, got: {:?}",
diags
.iter()
.map(|d| (d.rule, d.severity))
.collect::<Vec<_>>()
);
}
#[test]
fn paths_are_stamped() {
let diags = check_document(Path::new("refs.bib"), "@string{x = {y}}\n");
assert!(!diags.is_empty());
assert!(diags.iter().all(|d| d.path == Path::new("refs.bib")));
}
#[test]
fn phase_4b_rules_surface() {
let src = "@article{k,\n title = {The DNA of Erdős},\n publisher = nope,\n}\n";
let found = rules(src);
assert!(found.contains(&"undefined-string"), "{found:?}");
assert!(found.contains(&"title-capitalization"), "{found:?}");
assert!(found.contains(&"encoding-hints"), "{found:?}");
}
#[test]
fn comment_directive_suppresses_following_entry() {
let src = "\
@comment{badness-ignore unused-string: intentional}
@string{cup = {Cambridge University Press}}
@string{other = {O}}
";
let found = rules(src);
assert_eq!(
found.iter().filter(|r| **r == "unused-string").count(),
1,
"{found:?}"
);
}
#[test]
fn file_directive_suppresses_all() {
let src = "\
@comment{badness-ignore-file: quiet}
@string{a = {A}}
@misc{k, title = {DNA}}
";
assert!(rules(src).is_empty(), "got: {:?}", rules(src));
}
#[test]
fn empty_field_fix_survives_format_roundtrip() {
let messy = "@article{k, title = {T}, note = {}, year = 2020}\n";
let formatted = format(messy).unwrap();
let fixes: Vec<_> = check_document(Path::new("refs.bib"), &formatted)
.into_iter()
.filter_map(|d| d.fix)
.collect();
assert!(!fixes.is_empty(), "expected an empty-field fix");
let fixed = apply_fixes(&formatted, &fixes, false).output;
assert!(
!fixed.contains("note"),
"empty field not removed: {fixed:?}"
);
assert_eq!(
format(&fixed).unwrap(),
fixed,
"fix output is not format-clean"
);
let remaining = rules(&fixed);
assert!(
!remaining.contains(&"empty-field"),
"empty-field should be cleared: {remaining:?}"
);
}
#[test]
fn duplicate_field_fix_survives_format_roundtrip() {
let messy = "@article{k, author = {A}, author = {A}, title = {T}, year = 2020}\n";
let formatted = format(messy).unwrap();
let fixes: Vec<_> = check_document(Path::new("refs.bib"), &formatted)
.into_iter()
.filter_map(|d| d.fix)
.collect();
assert!(!fixes.is_empty(), "expected a duplicate-field fix");
let fixed = apply_fixes(&formatted, &fixes, false).output;
assert_eq!(fixed.matches("author").count(), 1, "got: {fixed:?}");
assert_eq!(
format(&fixed).unwrap(),
fixed,
"fix output is not format-clean"
);
let remaining = rules(&fixed);
assert!(
!remaining.contains(&"duplicate-field"),
"duplicate-field should be cleared: {remaining:?}"
);
}