#![allow(missing_docs, reason = "test")]
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in test, benchmark, and example code."
)]
mod common;
use common::announce_behavior;
use citum_engine::Processor;
use citum_io::load_bibliography;
use citum_schema::Style;
use citum_schema::citation::{Citation, CitationItem};
use std::fs;
use std::path::{Path, PathBuf};
fn project_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn load_style(path: &Path) -> Style {
let bytes = fs::read(path).expect("style fixture should be readable");
serde_yaml::from_slice(&bytes).expect("style fixture should parse")
}
fn load_sort_oracle_bibliography()
-> indexmap::IndexMap<String, citum_schema::reference::InputReference> {
let root = project_root();
let path = root.join("tests/fixtures/sort-oracle.json");
load_bibliography(&path).expect("sort-oracle fixture should load")
}
#[test]
fn test_apa_7th_sort_same_author_year_by_title() {
announce_behavior(
"Works by the same author in the same year are sorted alphabetically by title.",
);
let root = project_root();
let style = load_style(&root.join("styles/embedded/apa-7th.yaml"));
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let result = processor.render_bibliography();
let academic_pos = result
.find("Academic Enterprise")
.expect("Academic Enterprise should be in output");
let digital_pos = result
.find("Digital transformation")
.or_else(|| result.find("Digital Transformation"))
.expect("Digital Transformation should be in output");
let ethics_pos = result
.find("Ethics in Research")
.expect("Ethics in Research should be in output");
assert!(
academic_pos < digital_pos,
"Academic should come before Digital"
);
assert!(
digital_pos < ethics_pos,
"Digital should come before Ethics"
);
}
#[test]
fn test_apa_7th_sort_anonymous_works_by_title() {
announce_behavior("Anonymous works sort by title with leading articles stripped.");
let root = project_root();
let style = load_style(&root.join("styles/embedded/apa-7th.yaml"));
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let result = processor.render_bibliography();
let chicago_pos = result
.find("Chicago Manual")
.expect("Chicago Manual should be in output");
let guide_pos = result
.find("A Brief Guide")
.expect("A Brief Guide should be in output");
assert!(
guide_pos < chicago_pos,
"Anonymous works should file under title with article stripping. Got: {result}"
);
}
#[test]
fn test_numeric_sort_by_citation_order() {
announce_behavior(
"Numeric style assigns citation numbers by fixture insertion order, not by author or title.",
);
let style = {
use citum_schema::options::Processing;
use citum_schema::{BibliographySpec, CitationSpec, StyleInfo, options::Config};
Style {
info: StyleInfo {
title: Some("Numeric Test".to_string()),
id: Some("numeric-test".into()),
..Default::default()
},
options: Some(Config {
processing: Some(Processing::Numeric),
..Default::default()
}),
citation: Some(CitationSpec {
template: Some(vec![citum_schema::tc_number!(CitationNumber)]),
wrap: Some(citum_schema::template::WrapPunctuation::Brackets.into()),
..Default::default()
}),
bibliography: Some(BibliographySpec {
template: Some(vec![
citum_schema::tc_number!(CitationNumber, suffix = ". "),
citum_schema::tc_contributor!(Author, Long),
]),
..Default::default()
}),
..Default::default()
}
};
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let cit1 = Citation {
items: vec![CitationItem {
id: "SORT-6".to_string(),
..Default::default()
}],
..Default::default()
};
let result1 = processor
.process_citation(&cit1)
.expect("citation 1 should process");
assert_eq!(
result1, "[6]",
"SORT-6 should be numbered [6] (6th in fixture)"
);
let cit2 = Citation {
items: vec![CitationItem {
id: "SORT-7".to_string(),
..Default::default()
}],
..Default::default()
};
let result2 = processor
.process_citation(&cit2)
.expect("citation 2 should process");
assert_eq!(
result2, "[7]",
"SORT-7 should be numbered [7] (7th in fixture)"
);
let bib_result = processor.render_bibliography();
assert!(!bib_result.is_empty(), "Bibliography should render");
}
#[test]
fn test_uppercase_surname_sort_order() {
announce_behavior("All-caps surnames sort in the same order as normally-cased surnames.");
let root = project_root();
let style = load_style(&root.join("styles/embedded/apa-7th.yaml"));
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let result = processor.render_bibliography();
if let (Some(smith_pos), Some(williams_pos)) =
(result.find("Smith, Robert"), result.find("Williams, David"))
{
assert!(
smith_pos < williams_pos,
"Smith should come before Williams in sort order"
);
}
}
#[test]
fn test_multiauthor_same_year_sort() {
announce_behavior(
"Multi-author works with the same year appear together in author-date sort order.",
);
let root = project_root();
let style = load_style(&root.join("styles/embedded/apa-7th.yaml"));
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let result = processor.render_bibliography();
let brown_refs = result.matches("Brown").count();
assert!(
brown_refs > 0,
"Brown references should appear in bibliography"
);
}
#[test]
#[cfg(feature = "icu")]
fn test_apa_7th_sort_unicode_accented_surnames() {
announce_behavior(
"Accented surnames sort near their ASCII peers in author-date bibliographies.",
);
let root = project_root();
let style = load_style(&root.join("styles/embedded/apa-7th.yaml"));
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let result = processor.render_bibliography();
let celik_pos = result.find("Çelik, Z.").expect("Çelik should be in output");
let o_tuathail_pos = result
.find("Ó Tuathail, G.")
.expect("Ó Tuathail should be in output");
let zimring_pos = result
.find("Zimring, C. A.")
.expect("Zimring should be in output");
assert!(
celik_pos < o_tuathail_pos,
"Çelik should sort before Ó Tuathail. Got: {result}"
);
assert!(
o_tuathail_pos < zimring_pos,
"Ó Tuathail should sort before Zimring. Got: {result}"
);
}
#[test]
fn test_numeric_style_volume_issue_independence() {
announce_behavior(
"Numeric style numbering is determined by citation order, not by volume or issue.",
);
let style = {
use citum_schema::options::Processing;
use citum_schema::{BibliographySpec, CitationSpec, StyleInfo, options::Config};
Style {
info: StyleInfo {
title: Some("Numeric Test".to_string()),
id: Some("numeric-test".into()),
..Default::default()
},
options: Some(Config {
processing: Some(Processing::Numeric),
..Default::default()
}),
citation: Some(CitationSpec {
template: Some(vec![citum_schema::tc_number!(CitationNumber)]),
wrap: Some(citum_schema::template::WrapPunctuation::Brackets.into()),
..Default::default()
}),
bibliography: Some(BibliographySpec {
template: Some(vec![
citum_schema::tc_number!(CitationNumber, suffix = ". "),
citum_schema::tc_contributor!(Author, Long),
]),
..Default::default()
}),
..Default::default()
}
};
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let cit6 = Citation {
items: vec![CitationItem {
id: "SORT-6".to_string(),
..Default::default()
}],
..Default::default()
};
let result6 = processor
.process_citation(&cit6)
.expect("citation should process");
assert_eq!(
result6, "[6]",
"SORT-6 should be [6] (citation order, not volume)"
);
let cit7 = Citation {
items: vec![CitationItem {
id: "SORT-7".to_string(),
..Default::default()
}],
..Default::default()
};
let result7 = processor
.process_citation(&cit7)
.expect("citation should process");
assert_eq!(
result7, "[7]",
"SORT-7 should be [7] (citation order, not volume)"
);
}
#[test]
#[cfg(feature = "icu")]
fn test_mixed_script_sort_order() {
announce_behavior(
"Mixed-script bibliography: Latin entries sort alphabetically first, Arabic after Latin, Hangul after Arabic (en-US collator).",
);
let root = project_root();
let style = load_style(&root.join("styles/embedded/apa-7th.yaml"));
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let result = processor.render_bibliography();
let celik_pos = result.find("Çelik").expect("Çelik should be in output");
let zimring_pos = result.find("Zimring").expect("Zimring should be in output");
assert!(
celik_pos < zimring_pos,
"Çelik (C) must sort before Zimring (Z) in Latin ordering. Got:\n{result}"
);
let ghazali_pos = result
.find("الغزالي")
.expect("Arabic-script author الغزالي must appear in output unchanged");
assert!(
zimring_pos < ghazali_pos,
"Arabic-script entry must sort after Latin entries (Zimring). Got:\n{result}"
);
let hangul_pos = result
.find("김")
.expect("Hangul author 김 must appear in output unchanged");
assert!(
ghazali_pos < hangul_pos,
"Hangul entry must sort after Arabic-script entry (الغزالي). Got:\n{result}"
);
}
#[test]
fn test_mixed_script_sort_determinism() {
announce_behavior(
"Mixed-script bibliography produces identical output on repeated calls (deterministic sort).",
);
let root = project_root();
let style = load_style(&root.join("styles/embedded/apa-7th.yaml"));
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let first = processor.render_bibliography();
let second = processor.render_bibliography();
assert_eq!(
first, second,
"Bibliography sort must be identical across repeated calls"
);
}
#[test]
fn test_allcaps_surname_sorts_case_insensitively() {
announce_behavior(
"All-caps surnames (SMITH, WILLIAMS) sort case-insensitively alongside mixed-case surnames without lowercasing source text.",
);
let root = project_root();
let style = load_style(&root.join("styles/embedded/apa-7th.yaml"));
let bib = load_sort_oracle_bibliography();
let processor = Processor::new(style, bib);
let result = processor.render_bibliography();
let brown_pos = result.find("Brown").expect("Brown should be in output");
let smith_pos = result
.find("SMITH")
.expect("SMITH (all-caps) should be in output");
let zimring_pos = result.find("Zimring").expect("Zimring should be in output");
assert!(
brown_pos < smith_pos,
"Brown must sort before SMITH. Got:\n{result}"
);
assert!(
smith_pos < zimring_pos,
"SMITH must sort before Zimring — all-caps must not push it to end of list. Got:\n{result}"
);
}