#![allow(missing_docs, reason = "test")]
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::string_slice,
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_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();
let sort6_pos = bib_result
.find("6. Robert SMITH, Patricia Jones")
.expect("SORT-6 should render as '6. Robert SMITH, Patricia Jones'");
let sort7_pos = bib_result
.find("7. Robert SMITH, David Williams")
.expect("SORT-7 should render as '7. Robert SMITH, David Williams'");
assert!(
sort6_pos < sort7_pos,
"SORT-6 (entry 6) must appear before SORT-7 (entry 7) 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]
#[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}"
);
}
#[test]
fn test_multiauthor_same_first_author_year_sorts_by_title() {
announce_behavior(
"Multi-author works with the same first author and year sort by title as the third key.",
);
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 data_science_pos = result
.find("Data Science Fundamentals")
.expect("'Data Science Fundamentals' should be in output");
let neural_networks_pos = result
.find("Neural Networks and Deep Learning")
.expect("'Neural Networks and Deep Learning' should be in output");
assert!(
data_science_pos < neural_networks_pos,
"Data Science (D) must sort before Neural Networks (N) — same Brown 2022 author/year, title is tiebreaker. Got:\n{result}"
);
let between = &result[data_science_pos..neural_networks_pos];
assert!(
!between.contains("Adams") && !between.contains("SMITH") && !between.contains("Zimring"),
"No non-Brown entry should appear between the two Brown 2022 entries. Between:\n{between}"
);
}