diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e659534..92a656f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ name: Continuous integration -on: [push, pull_request] +on: [ push, pull_request ] env: RUSTFLAGS: "-Dwarnings" @@ -7,22 +7,35 @@ env: jobs: test: + name: Build and test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.89.0 - - run: cargo build - - run: cargo test + - name: Check out + uses: actions/checkout@v6 + - name: Set up Rust + uses: dtolnay/rust-toolchain@1.89.0 + - name: Cache Cargo registry + uses: Swatinem/rust-cache@v2 + - name: Build + run: cargo build + - name: Test + run: cargo test checks: name: Check clippy, formatting, and documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.89.0 + - name: Check out + uses: actions/checkout@v6 + - name: Set up Rust + uses: dtolnay/rust-toolchain@1.89.0 with: components: clippy, rustfmt - - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --workspace --all-targets --all-features - - run: cargo fmt --check --all - - run: cargo doc --workspace --no-deps + - name: Cache Cargo registry + uses: Swatinem/rust-cache@v2 + - name: Run clippy + run: cargo clippy --workspace --all-targets --all-features + - name: Check formatting + run: cargo fmt --check --all + - name: Build documentation + run: cargo doc --workspace --no-deps diff --git a/bench/src/bench.rs b/bench/src/bench.rs index 3fc3666..3a5ae63 100644 --- a/bench/src/bench.rs +++ b/bench/src/bench.rs @@ -2,8 +2,8 @@ use criterion::{criterion_group, criterion_main, Criterion}; use biblatex::Bibliography; -const GRAL: &str = include_str!("../../tests/gral.bib"); -const CROSS: &str = include_str!("../../tests/cross.bib"); +const GRAL: &str = include_str!("../../tests/fixtures/valid/gral.bib"); +const CROSS: &str = include_str!("../../tests/fixtures/valid/cross.bib"); fn benchmarks(c: &mut Criterion) { macro_rules! bench { diff --git a/src/lib.rs b/src/lib.rs index 5df9870..b7364a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -413,11 +413,11 @@ impl Entry { } match reqs.author_eds_field { - AuthorMode::OneRequired => { - if self.author().is_err() && self.editors().unwrap_or_default().is_empty() - { - missing.push("author"); - } + AuthorMode::OneRequired + if self.author().is_err() + && self.editors().unwrap_or_default().is_empty() => + { + missing.push("author"); } AuthorMode::BothRequired => { if self.editors().unwrap_or_default().is_empty() { @@ -427,10 +427,10 @@ impl Entry { missing.push("author"); } } - AuthorMode::AuthorRequired | AuthorMode::AuthorRequiredEditorOptional => { - if self.author().is_err() { - missing.push("author"); - } + AuthorMode::AuthorRequired | AuthorMode::AuthorRequiredEditorOptional + if self.author().is_err() => + { + missing.push("author"); } AuthorMode::EditorRequiredAuthorForbidden => { if self.editors().unwrap_or_default().is_empty() { @@ -444,10 +444,10 @@ impl Entry { } match reqs.page_chapter_field { - PagesChapterMode::OneRequired => { - if self.pages().is_err() && self.chapter().is_err() { - missing.push("pages"); - } + PagesChapterMode::OneRequired + if self.pages().is_err() && self.chapter().is_err() => + { + missing.push("pages"); } PagesChapterMode::BothForbidden => { if self.pages().is_ok() { @@ -457,10 +457,8 @@ impl Entry { superfluous.push("chapter"); } } - PagesChapterMode::PagesRequired => { - if self.pages().is_err() { - missing.push("pages"); - } + PagesChapterMode::PagesRequired if self.pages().is_err() => { + missing.push("pages"); } _ => {} } @@ -955,440 +953,3 @@ impl Debug for Spanned { Ok(()) } } - -#[cfg(test)] -mod tests { - use std::fs; - - use super::*; - use crate::raw::Token; - - #[test] - fn test_correct_bib() { - let contents = fs::read_to_string("tests/gral.bib").unwrap(); - let bibliography = Bibliography::parse(&contents).unwrap(); - assert_eq!(bibliography.entries.len(), 83) - } - - #[test] - fn test_repeated_key() { - let contents = fs::read_to_string("tests/gral_rep_key.bib").unwrap(); - let bibliography = Bibliography::parse(&contents); - match bibliography { - Ok(_) => panic!("Should return Err"), - Err(s) => { - assert_eq!(s.kind, ParseErrorKind::DuplicateKey("ishihara2012".into())); - } - }; - } - - #[test] - fn test_parse_incorrect_result() { - let contents = fs::read_to_string("tests/incorrect_syntax.bib").unwrap(); - - let bibliography = Bibliography::parse(&contents); - match bibliography { - Ok(_) => { - panic!("Should return Err") - } - Err(s) => { - assert_eq!( - s, - ParseError::new(369..369, ParseErrorKind::Expected(Token::Equals)) - ); - } - }; - } - - #[test] - fn test_parse_incorrect_types() { - let contents = fs::read_to_string("tests/incorrect_data.bib").unwrap(); - - let bibliography = Bibliography::parse(&contents).unwrap(); - let rashid = bibliography.get("rashid2016").unwrap(); - match rashid.pagination() { - Err(RetrievalError::TypeError(s)) => { - assert_eq!(s, TypeError::new(352..359, TypeErrorKind::UnknownPagination)); - } - _ => { - panic!() - } - }; - } - - #[test] - fn test_keys() { - let contents = fs::read_to_string("tests/editortypes.bib").unwrap(); - - let bibliography = Bibliography::parse(&contents).unwrap(); - - assert_eq!( - bibliography.keys().collect::>(), - &["acerolaThisDifferenceGaussians2022", "mozart_KV183_1773", "Smith2018"] - ); - } - - #[test] - fn test_gral_paper() { - dump_debug("tests/gral.bib"); - } - - #[test] - fn test_ds_report() { - dump_debug("tests/ds.bib"); - } - - #[test] - fn test_libra_paper() { - dump_author_title("tests/libra.bib"); - } - - #[test] - fn test_rass_report() { - dump_author_title("tests/rass.bib"); - } - - #[test] - fn test_polar_report() { - dump_author_title("tests/polaritons.bib"); - } - - #[test] - fn test_comments() { - let contents = fs::read_to_string("tests/comments.bib").unwrap(); - - let bibliography = Bibliography::parse(&contents).unwrap(); - - assert_eq!( - bibliography.keys().collect::>(), - &[ - "mcelreath2007mathematical", - "fischer2022equivalence", - "roes2003belief", - "wong2016null", - ] - ); - - assert_eq!( - bibliography - .get("wong2016null") - .unwrap() - .title() - .unwrap() - .format_verbatim(), - "Null hypothesis testing (I)-5% significance level" - ); - } - - #[test] - fn test_extended_name_format() { - dump_author_title("tests/extended_name_format.bib"); - } - - #[test] - fn test_alias() { - let contents = fs::read_to_string("tests/cross.bib").unwrap(); - let mut bibliography = Bibliography::parse(&contents).unwrap(); - - assert_eq!(bibliography.get("issue201"), bibliography.get("github")); - bibliography.alias("issue201", "crap"); - assert_eq!(bibliography.get("crap"), bibliography.get("unstable")); - bibliography.remove("crap").unwrap(); - - let entry = bibliography.get("cannonfodder").unwrap(); - assert_eq!(entry.key, "cannonfodder"); - assert_eq!(entry.entry_type, EntryType::Misc); - } - - #[test] - fn test_bibtex_conversion() { - let contents = fs::read_to_string("tests/cross.bib").unwrap(); - let mut bibliography = Bibliography::parse(&contents).unwrap(); - - let biblatex = bibliography.get_mut("haug2019").unwrap().to_biblatex_string(); - assert!(biblatex.contains("institution = {Technische Universität Berlin},")); - - let bibtex = - bibliography.get_mut("haug2019").unwrap().to_bibtex_string().unwrap(); - assert!(bibtex.contains("school = {Technische Universität Berlin},")); - assert!(bibtex.contains("year = {2019},")); - assert!(bibtex.contains("month = {10},")); - assert!(!bibtex.contains("institution")); - assert!(!bibtex.contains("date")); - } - - #[test] - fn test_verify() { - let mut contents = fs::read_to_string("tests/gral.bib").unwrap(); - let mut bibliography = Bibliography::parse(&contents).unwrap(); - assert!(bibliography.get_mut("lin_sida:_2007").unwrap().verify().is_ok()); - - contents = fs::read_to_string("tests/cross.bib").unwrap(); - bibliography = Bibliography::parse(&contents).unwrap(); - - assert!(bibliography.get_mut("haug2019").unwrap().verify().is_ok()); - assert!(bibliography.get_mut("cannonfodder").unwrap().verify().is_ok()); - - let ill = bibliography.get("ill-defined").unwrap(); - let report = ill.verify(); - assert_eq!(report.missing.len(), 3); - assert_eq!(report.superfluous.len(), 3); - assert_eq!(report.malformed.len(), 1); - assert!(report.missing.contains(&"title")); - assert!(report.missing.contains(&"year")); - assert!(report.missing.contains(&"editor")); - assert!(report.superfluous.contains(&"maintitle")); - assert!(report.superfluous.contains(&"author")); - assert!(report.superfluous.contains(&"chapter")); - assert_eq!(report.malformed[0].0.as_str(), "gender"); - } - - #[test] - fn test_crossref() { - let contents = fs::read_to_string("tests/cross.bib").unwrap(); - let bibliography = Bibliography::parse(&contents).unwrap(); - - let e = bibliography.get("macmillan").unwrap(); - assert_eq!(e.publisher().unwrap()[0].format_verbatim(), "Macmillan"); - assert_eq!(e.location().unwrap().format_verbatim(), "New York and London"); - - let book = bibliography.get("recursive").unwrap(); - assert_eq!(book.publisher().unwrap()[0].format_verbatim(), "Macmillan"); - assert_eq!(book.location().unwrap().format_verbatim(), "New York and London"); - assert_eq!( - book.title().unwrap().format_verbatim(), - "Recursive shennenigans and other important stuff" - ); - - assert_eq!( - bibliography.get("arrgh").unwrap().parents().unwrap(), - vec!["polecon".to_string()] - ); - let arrgh = bibliography.get("arrgh").unwrap(); - assert_eq!(arrgh.entry_type, EntryType::Article); - assert_eq!(arrgh.volume().unwrap(), PermissiveType::Typed(115)); - assert_eq!(arrgh.editors().unwrap()[0].0[0].name, "Uhlig"); - assert_eq!(arrgh.number().unwrap().format_verbatim(), "6"); - assert_eq!( - arrgh.journal().unwrap().format_verbatim(), - "Journal of Political Economy" - ); - assert_eq!( - arrgh.title().unwrap().format_verbatim(), - "An‐arrgh‐chy: The Law and Economics of Pirate Organization" - ); - } - - fn dump_debug(file: &str) { - let contents = fs::read_to_string(file).unwrap(); - let bibliography = Bibliography::parse(&contents).unwrap(); - println!("{:#?}", bibliography); - } - - fn dump_author_title(file: &str) { - let contents = fs::read_to_string(file).unwrap(); - let bibliography = Bibliography::parse(&contents).unwrap(); - - println!("{}", bibliography.to_biblatex_string()); - - for x in bibliography { - let authors = x.author().unwrap_or_default(); - for a in authors { - print!("{}, ", a); - } - println!("\"{}\".", x.title().unwrap().format_sentence()); - } - } - - #[test] - fn linebreak_field() { - let contents = r#"@book{key, title = {Hello -Martin}}"#; - let bibliography = Bibliography::parse(contents).unwrap(); - let entry = bibliography.get("key").unwrap(); - assert_eq!(entry.title().unwrap().format_verbatim(), "Hello Martin"); - } - - #[test] - fn test_verbatim_fields() { - let contents = fs::read_to_string("tests/libra.bib").unwrap(); - let bibliography = Bibliography::parse(&contents).unwrap(); - - // Import an entry/field with escaped colons - let e = bibliography.get("dierksmeierJustHODLMoral2018").unwrap(); - assert_eq!(e.doi().unwrap(), "10.1007/s41463-018-0036-z"); - assert_eq!( - e.file().unwrap(), - "C:\\Users\\mhaug\\Zotero\\storage\\DTPR7TES\\Dierksmeier - 2018 - Just HODL On the Moral Claims of Bitcoin and Ripp.pdf" - ); - - // Import an entry/field with unescaped colons - let e = bibliography.get("LibraAssociationIndependent").unwrap(); - assert_eq!(e.url().unwrap(), "https://libra.org/association/"); - - // Test export of entry (not escaping colons) - let e = bibliography.get("finextraFedGovernorChallenges2019").unwrap(); - assert_eq!( - e.to_biblatex_string(), - "@online{finextraFedGovernorChallenges2019,\nauthor = {FinExtra},\ndate = {2019-12-18},\nfile = {C:\\\\Users\\\\mhaug\\\\Zotero\\\\storage\\\\VY9LAKFE\\\\fed-governor-challenges-facebooks-libra-project.html},\ntitle = {Fed {Governor} Challenges {Facebook}'s {Libra} Project},\nurl = {https://www.finextra.com/newsarticle/34986/fed-governor-challenges-facebooks-libra-project},\nurldate = {2020-08-22},\n}" - ); - - // Test URLs with math and backslashes - let e = bibliography.get("weirdUrl2023").unwrap(); - assert_eq!(e.url().unwrap(), r#"example.com?A=$B\%\{}"#); - assert_eq!(e.doi().unwrap(), r#"example.com?A=$B\%\{}"#); - } - - #[test] - fn test_synthesized_entry() { - let mut e = Entry::new("Test123".to_owned(), EntryType::Article); - let brian = vec![Person { - name: "Monroe".to_string(), - given_name: "Brian Albert".to_string(), - prefix: "".to_string(), - suffix: "".to_string(), - }]; - - e.set_author(brian.clone()); - - assert_eq!(Ok(brian), e.author()); - } - - #[test] - fn test_case_sensitivity() { - let contents = fs::read_to_string("tests/case.bib").unwrap(); - let bibliography = Bibliography::parse(&contents).unwrap(); - - let entry = bibliography.get("biblatex2023").unwrap(); - let author = entry.author(); - - match author { - Ok(a) => assert_eq!(a[0].name, "Kime"), - Err(RetrievalError::Missing(_)) => { - panic!("Tags should be case insensitive."); - } - _ => panic!(), - } - } - - #[test] - fn test_whitespace_collapse() { - let raw = r#"@article{aksin, - title = {Effect of immobilization on catalytic characteristics of - saturated {Pd-N}-heterocyclic carbenes in {Mizoroki-Heck} - reactions}, - }"#; - - let bibliography = Bibliography::parse(raw).unwrap(); - let entry = bibliography.get("aksin").unwrap(); - assert_eq!( - entry.title().unwrap().first().map(|s| s.as_ref().v), - Some(Chunk::Normal( - "Effect of immobilization on catalytic characteristics of saturated " - .to_string() - )) - .as_ref() - ); - } - - #[test] - fn test_empty_date_fields() { - let raw = r#"@article{test, - year = 2000, - day = {}, - month = {}, - }"#; - - let bibliography = Bibliography::parse(raw).unwrap(); - assert_eq!( - bibliography.get("test").unwrap().date(), - Err(TypeError::new(74..74, TypeErrorKind::MissingNumber).into()) - ); - } - - #[test] - #[allow(clippy::single_range_in_vec_init)] - fn test_page_ranges() { - let raw = r#"@article{test, - pages = {1---2}, - } - @article{test1, - pages = {2--3}, - } - @article{test2, - pages = {1}, - }"#; - - let bibliography = Bibliography::parse(raw).unwrap(); - assert_eq!( - bibliography.get("test").unwrap().pages(), - Ok(PermissiveType::Typed(vec![1..2])) - ); - assert_eq!( - bibliography.get("test1").unwrap().pages(), - Ok(PermissiveType::Typed(vec![2..3])) - ); - assert_eq!( - bibliography.get("test2").unwrap().pages(), - Ok(PermissiveType::Typed(vec![1..1])) - ); - } - - #[test] - fn test_editor_types() { - let contents = fs::read_to_string("tests/editortypes.bib").unwrap(); - let bibliography = Bibliography::parse(&contents).unwrap(); - let video = bibliography.get("acerolaThisDifferenceGaussians2022").unwrap(); - assert_eq!( - video.editors(), - Ok(vec![( - vec![Person { - name: "Acerola".into(), - given_name: "".into(), - prefix: "".into(), - suffix: "".into() - }], - EditorType::Director - )]) - ); - - let music = bibliography.get("mozart_KV183_1773").unwrap(); - assert_eq!( - music.editors(), - Ok(vec![( - vec![Person { - name: "Mozart".into(), - given_name: "Wolfgang Amadeus".into(), - prefix: "".into(), - suffix: "".into() - }], - EditorType::Unknown("pianist".into()), - )]) - ); - - let audio = bibliography.get("Smith2018").unwrap(); - assert_eq!( - audio.editors(), - Ok(vec![ - ( - vec![Person { - name: "Smith".into(), - given_name: "Stacey Vanek".into(), - prefix: "".into(), - suffix: "".into() - }], - EditorType::Unknown("host".into()), - ), - ( - vec![Person { - name: "Plotkin".into(), - given_name: "Stanley".into(), - prefix: "".into(), - suffix: "".into() - }], - EditorType::Unknown("participant".into()), - ) - ]) - ); - } -} diff --git a/src/mechanics.rs b/src/mechanics.rs index 898bd1d..b5cd898 100644 --- a/src/mechanics.rs +++ b/src/mechanics.rs @@ -58,7 +58,7 @@ pub enum EntryType { } /// Describes the optionality mode of the `author` and `editor` fields. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub enum AuthorMode { /// Neither of the fields are required to be set. NoneRequired, @@ -67,6 +67,7 @@ pub enum AuthorMode { /// Both fields must be set. BothRequired, /// The `author` field must be present. + #[default] AuthorRequired, /// The `author` field must be present, the `editor` field is optional. AuthorRequiredEditorOptional, @@ -74,12 +75,6 @@ pub enum AuthorMode { EditorRequiredAuthorForbidden, } -impl Default for AuthorMode { - fn default() -> Self { - Self::AuthorRequired - } -} - impl AuthorMode { pub(crate) fn possible(&self) -> &'static [&'static str] { match self { @@ -94,9 +89,10 @@ impl AuthorMode { } /// Describes the optionality mode of the `pages` and `chapter` field -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub enum PagesChapterMode { /// No specification for the `page` and `chapter` field is given. + #[default] None, /// At least one of the fields must be present. OneRequired, @@ -112,12 +108,6 @@ pub enum PagesChapterMode { PagesRequired, } -impl Default for PagesChapterMode { - fn default() -> Self { - Self::None - } -} - impl PagesChapterMode { pub(crate) fn possible(&self) -> &'static [&'static str] { match self { diff --git a/src/types/mod.rs b/src/types.rs similarity index 100% rename from src/types/mod.rs rename to src/types.rs diff --git a/tests/dump.rs b/tests/dump.rs new file mode 100644 index 0000000..c77a55d --- /dev/null +++ b/tests/dump.rs @@ -0,0 +1,54 @@ +use std::fs; + +use biblatex::{Bibliography, ChunksExt}; + +#[test] +fn test_gral_paper() { + dump_debug("tests/fixtures/valid/gral.bib"); +} + +#[test] +fn test_ds_report() { + dump_debug("tests/fixtures/valid/ds.bib"); +} + +#[test] +fn test_libra_paper() { + dump_author_title("tests/fixtures/valid/libra.bib"); +} + +#[test] +fn test_rass_report() { + dump_author_title("tests/fixtures/valid/rass.bib"); +} + +#[test] +fn test_polar_report() { + dump_author_title("tests/fixtures/valid/polaritons.bib"); +} + +fn dump_debug(file: &str) { + let contents = fs::read_to_string(file).unwrap(); + let bibliography = Bibliography::parse(&contents).unwrap(); + println!("{:#?}", bibliography); +} + +fn dump_author_title(file: &str) { + let contents = fs::read_to_string(file).unwrap(); + let bibliography = Bibliography::parse(&contents).unwrap(); + + println!("{}", bibliography.to_biblatex_string()); + + for x in bibliography { + let authors = x.author().unwrap_or_default(); + for a in authors { + print!("{}, ", a); + } + println!("\"{}\".", x.title().unwrap().format_sentence()); + } +} + +#[test] +fn test_extended_name_format() { + dump_author_title("tests/fixtures/valid/extended_name_format.bib"); +} diff --git a/tests/gral_rep_key.bib b/tests/fixtures/invalid/gral_rep_key.bib similarity index 100% rename from tests/gral_rep_key.bib rename to tests/fixtures/invalid/gral_rep_key.bib diff --git a/tests/incorrect_data.bib b/tests/fixtures/invalid/incorrect_data.bib similarity index 100% rename from tests/incorrect_data.bib rename to tests/fixtures/invalid/incorrect_data.bib diff --git a/tests/incorrect_syntax.bib b/tests/fixtures/invalid/incorrect_syntax.bib similarity index 100% rename from tests/incorrect_syntax.bib rename to tests/fixtures/invalid/incorrect_syntax.bib diff --git a/tests/case.bib b/tests/fixtures/valid/case.bib similarity index 100% rename from tests/case.bib rename to tests/fixtures/valid/case.bib diff --git a/tests/comments.bib b/tests/fixtures/valid/comments.bib similarity index 100% rename from tests/comments.bib rename to tests/fixtures/valid/comments.bib diff --git a/tests/cross.bib b/tests/fixtures/valid/cross.bib similarity index 100% rename from tests/cross.bib rename to tests/fixtures/valid/cross.bib diff --git a/tests/ds.bib b/tests/fixtures/valid/ds.bib similarity index 100% rename from tests/ds.bib rename to tests/fixtures/valid/ds.bib diff --git a/tests/editortypes.bib b/tests/fixtures/valid/editortypes.bib similarity index 100% rename from tests/editortypes.bib rename to tests/fixtures/valid/editortypes.bib diff --git a/tests/extended_name_format.bib b/tests/fixtures/valid/extended_name_format.bib similarity index 100% rename from tests/extended_name_format.bib rename to tests/fixtures/valid/extended_name_format.bib diff --git a/tests/gral.bib b/tests/fixtures/valid/gral.bib similarity index 100% rename from tests/gral.bib rename to tests/fixtures/valid/gral.bib diff --git a/tests/libra.bib b/tests/fixtures/valid/libra.bib similarity index 100% rename from tests/libra.bib rename to tests/fixtures/valid/libra.bib diff --git a/tests/polaritons.bib b/tests/fixtures/valid/polaritons.bib similarity index 100% rename from tests/polaritons.bib rename to tests/fixtures/valid/polaritons.bib diff --git a/tests/rass.bib b/tests/fixtures/valid/rass.bib similarity index 100% rename from tests/rass.bib rename to tests/fixtures/valid/rass.bib diff --git a/tests/invalid.rs b/tests/invalid.rs new file mode 100644 index 0000000..0992d40 --- /dev/null +++ b/tests/invalid.rs @@ -0,0 +1,65 @@ +use std::fs; + +use biblatex::{ + Bibliography, ParseError, ParseErrorKind, RetrievalError, Token, TypeError, + TypeErrorKind, +}; + +#[test] +fn test_repeated_key() { + let contents = fs::read_to_string("tests/fixtures/invalid/gral_rep_key.bib").unwrap(); + let bibliography = Bibliography::parse(&contents); + match bibliography { + Ok(_) => panic!("Should return Err"), + Err(s) => { + assert_eq!(s.kind, ParseErrorKind::DuplicateKey("ishihara2012".into())); + } + }; +} + +#[test] +fn test_parse_incorrect_result() { + let contents = fs::read_to_string("tests/fixtures/invalid/incorrect_syntax.bib") + .unwrap() + .replace("\r\n", "\n"); + + let bibliography = Bibliography::parse(&contents); + match bibliography { + Ok(_) => { + panic!("Should return Err") + } + Err(s) => { + assert_eq!( + s, + ParseError { + span: 369..369, + kind: ParseErrorKind::Expected(Token::Equals) + } + ); + } + }; +} + +#[test] +fn test_parse_incorrect_types() { + let contents = fs::read_to_string("tests/fixtures/invalid/incorrect_data.bib") + .unwrap() + .replace("\r\n", "\n"); + + let bibliography = Bibliography::parse(&contents).unwrap(); + let rashid = bibliography.get("rashid2016").unwrap(); + match rashid.pagination() { + Err(RetrievalError::TypeError(s)) => { + assert_eq!( + s, + TypeError { + span: 352..359, + kind: TypeErrorKind::UnknownPagination + } + ) + } + _ => { + panic!() + } + }; +} diff --git a/tests/valid.rs b/tests/valid.rs new file mode 100644 index 0000000..8aa94bc --- /dev/null +++ b/tests/valid.rs @@ -0,0 +1,337 @@ +use std::fs; + +use biblatex::{ + Bibliography, Chunk, ChunksExt, EditorType, Entry, EntryType, PermissiveType, Person, + RetrievalError, TypeError, TypeErrorKind, +}; + +#[test] +fn test_correct_bib() { + let contents = fs::read_to_string("tests/fixtures/valid/gral.bib").unwrap(); + let bibliography = Bibliography::parse(&contents).unwrap(); + assert_eq!(bibliography.len(), 83) +} + +#[test] +fn test_keys() { + let contents = fs::read_to_string("tests/fixtures/valid/editortypes.bib").unwrap(); + + let bibliography = Bibliography::parse(&contents).unwrap(); + + assert_eq!( + bibliography.keys().collect::>(), + &["acerolaThisDifferenceGaussians2022", "mozart_KV183_1773", "Smith2018"] + ); +} + +#[test] +fn test_comments() { + let contents = fs::read_to_string("tests/fixtures/valid/comments.bib").unwrap(); + + let bibliography = Bibliography::parse(&contents).unwrap(); + + assert_eq!( + bibliography.keys().collect::>(), + &[ + "mcelreath2007mathematical", + "fischer2022equivalence", + "roes2003belief", + "wong2016null", + ] + ); + + assert_eq!( + bibliography + .get("wong2016null") + .unwrap() + .title() + .unwrap() + .format_verbatim(), + "Null hypothesis testing (I)-5% significance level" + ); +} + +#[test] +fn test_alias() { + let contents = fs::read_to_string("tests/fixtures/valid/cross.bib").unwrap(); + let mut bibliography = Bibliography::parse(&contents).unwrap(); + + assert_eq!(bibliography.get("issue201"), bibliography.get("github")); + bibliography.alias("issue201", "crap"); + assert_eq!(bibliography.get("crap"), bibliography.get("unstable")); + bibliography.remove("crap").unwrap(); + + let entry = bibliography.get("cannonfodder").unwrap(); + assert_eq!(entry.key, "cannonfodder"); + assert_eq!(entry.entry_type, EntryType::Misc); +} + +#[test] +fn test_bibtex_conversion() { + let contents = fs::read_to_string("tests/fixtures/valid/cross.bib").unwrap(); + let mut bibliography = Bibliography::parse(&contents).unwrap(); + + let biblatex = bibliography.get_mut("haug2019").unwrap().to_biblatex_string(); + assert!(biblatex.contains("institution = {Technische Universität Berlin},")); + + let bibtex = bibliography.get_mut("haug2019").unwrap().to_bibtex_string().unwrap(); + assert!(bibtex.contains("school = {Technische Universität Berlin},")); + assert!(bibtex.contains("year = {2019},")); + assert!(bibtex.contains("month = {10},")); + assert!(!bibtex.contains("institution")); + assert!(!bibtex.contains("date")); +} + +#[test] +fn test_verify() { + let mut contents = fs::read_to_string("tests/fixtures/valid/gral.bib").unwrap(); + let mut bibliography = Bibliography::parse(&contents).unwrap(); + assert!(bibliography.get_mut("lin_sida:_2007").unwrap().verify().is_ok()); + + contents = fs::read_to_string("tests/fixtures/valid/cross.bib").unwrap(); + bibliography = Bibliography::parse(&contents).unwrap(); + + assert!(bibliography.get_mut("haug2019").unwrap().verify().is_ok()); + assert!(bibliography.get_mut("cannonfodder").unwrap().verify().is_ok()); + + let ill = bibliography.get("ill-defined").unwrap(); + let report = ill.verify(); + assert_eq!(report.missing.len(), 3); + assert_eq!(report.superfluous.len(), 3); + assert_eq!(report.malformed.len(), 1); + assert!(report.missing.contains(&"title")); + assert!(report.missing.contains(&"year")); + assert!(report.missing.contains(&"editor")); + assert!(report.superfluous.contains(&"maintitle")); + assert!(report.superfluous.contains(&"author")); + assert!(report.superfluous.contains(&"chapter")); + assert_eq!(report.malformed[0].0.as_str(), "gender"); +} + +#[test] +fn test_crossref() { + let contents = fs::read_to_string("tests/fixtures/valid/cross.bib").unwrap(); + let bibliography = Bibliography::parse(&contents).unwrap(); + + let e = bibliography.get("macmillan").unwrap(); + assert_eq!(e.publisher().unwrap()[0].format_verbatim(), "Macmillan"); + assert_eq!(e.location().unwrap().format_verbatim(), "New York and London"); + + let book = bibliography.get("recursive").unwrap(); + assert_eq!(book.publisher().unwrap()[0].format_verbatim(), "Macmillan"); + assert_eq!(book.location().unwrap().format_verbatim(), "New York and London"); + assert_eq!( + book.title().unwrap().format_verbatim(), + "Recursive shennenigans and other important stuff" + ); + + assert_eq!( + bibliography.get("arrgh").unwrap().parents().unwrap(), + vec!["polecon".to_string()] + ); + let arrgh = bibliography.get("arrgh").unwrap(); + assert_eq!(arrgh.entry_type, EntryType::Article); + assert_eq!(arrgh.volume().unwrap(), PermissiveType::Typed(115)); + assert_eq!(arrgh.editors().unwrap()[0].0[0].name, "Uhlig"); + assert_eq!(arrgh.number().unwrap().format_verbatim(), "6"); + assert_eq!( + arrgh.journal().unwrap().format_verbatim(), + "Journal of Political Economy" + ); + assert_eq!( + arrgh.title().unwrap().format_verbatim(), + "An‐arrgh‐chy: The Law and Economics of Pirate Organization" + ); +} + +#[test] +fn linebreak_field() { + let contents = r#"@book{key, title = {Hello +Martin}}"#; + let bibliography = Bibliography::parse(contents).unwrap(); + let entry = bibliography.get("key").unwrap(); + assert_eq!(entry.title().unwrap().format_verbatim(), "Hello Martin"); +} + +#[test] +fn test_verbatim_fields() { + let contents = fs::read_to_string("tests/fixtures/valid/libra.bib").unwrap(); + let bibliography = Bibliography::parse(&contents).unwrap(); + + // Import an entry/field with escaped colons + let e = bibliography.get("dierksmeierJustHODLMoral2018").unwrap(); + assert_eq!(e.doi().unwrap(), "10.1007/s41463-018-0036-z"); + assert_eq!( + e.file().unwrap(), + "C:\\Users\\mhaug\\Zotero\\storage\\DTPR7TES\\Dierksmeier - 2018 - Just HODL On the Moral Claims of Bitcoin and Ripp.pdf" + ); + + // Import an entry/field with unescaped colons + let e = bibliography.get("LibraAssociationIndependent").unwrap(); + assert_eq!(e.url().unwrap(), "https://libra.org/association/"); + + // Test export of entry (not escaping colons) + let e = bibliography.get("finextraFedGovernorChallenges2019").unwrap(); + assert_eq!( + e.to_biblatex_string(), + "@online{finextraFedGovernorChallenges2019,\nauthor = {FinExtra},\ndate = {2019-12-18},\nfile = {C:\\\\Users\\\\mhaug\\\\Zotero\\\\storage\\\\VY9LAKFE\\\\fed-governor-challenges-facebooks-libra-project.html},\ntitle = {Fed {Governor} Challenges {Facebook}'s {Libra} Project},\nurl = {https://www.finextra.com/newsarticle/34986/fed-governor-challenges-facebooks-libra-project},\nurldate = {2020-08-22},\n}" + ); + + // Test URLs with math and backslashes + let e = bibliography.get("weirdUrl2023").unwrap(); + assert_eq!(e.url().unwrap(), r#"example.com?A=$B\%\{}"#); + assert_eq!(e.doi().unwrap(), r#"example.com?A=$B\%\{}"#); +} + +#[test] +fn test_synthesized_entry() { + let mut e = Entry::new("Test123".to_owned(), EntryType::Article); + let brian = vec![Person { + name: "Monroe".to_string(), + given_name: "Brian Albert".to_string(), + prefix: "".to_string(), + suffix: "".to_string(), + }]; + + e.set_author(brian.clone()); + + assert_eq!(Ok(brian), e.author()); +} + +#[test] +fn test_case_sensitivity() { + let contents = fs::read_to_string("tests/fixtures/valid/case.bib").unwrap(); + let bibliography = Bibliography::parse(&contents).unwrap(); + + let entry = bibliography.get("biblatex2023").unwrap(); + let author = entry.author(); + + match author { + Ok(a) => assert_eq!(a[0].name, "Kime"), + Err(RetrievalError::Missing(_)) => { + panic!("Tags should be case insensitive."); + } + _ => panic!(), + } +} + +#[test] +fn test_whitespace_collapse() { + let raw = r#"@article{aksin, + title = {Effect of immobilization on catalytic characteristics of + saturated {Pd-N}-heterocyclic carbenes in {Mizoroki-Heck} + reactions}, + }"#; + + let bibliography = Bibliography::parse(raw).unwrap(); + let entry = bibliography.get("aksin").unwrap(); + assert_eq!( + entry.title().unwrap().first().map(|s| s.as_ref().v), + Some(Chunk::Normal( + "Effect of immobilization on catalytic characteristics of saturated " + .to_string() + )) + .as_ref() + ); +} + +#[test] +fn test_empty_date_fields() { + let raw = r#"@article{test, + year = 2000, + day = {}, + month = {}, + }"#; + + let bibliography = Bibliography::parse(raw).unwrap(); + assert_eq!( + bibliography.get("test").unwrap().date(), + Err(TypeError { span: 74..74, kind: TypeErrorKind::MissingNumber }.into()) + ); +} + +#[test] +#[allow(clippy::single_range_in_vec_init)] +fn test_page_ranges() { + let raw = r#"@article{test, + pages = {1---2}, + } + @article{test1, + pages = {2--3}, + } + @article{test2, + pages = {1}, + }"#; + + let bibliography = Bibliography::parse(raw).unwrap(); + assert_eq!( + bibliography.get("test").unwrap().pages(), + Ok(PermissiveType::Typed(vec![1..2])) + ); + assert_eq!( + bibliography.get("test1").unwrap().pages(), + Ok(PermissiveType::Typed(vec![2..3])) + ); + assert_eq!( + bibliography.get("test2").unwrap().pages(), + Ok(PermissiveType::Typed(vec![1..1])) + ); +} + +#[test] +fn test_editor_types() { + let contents = fs::read_to_string("tests/fixtures/valid/editortypes.bib").unwrap(); + let bibliography = Bibliography::parse(&contents).unwrap(); + let video = bibliography.get("acerolaThisDifferenceGaussians2022").unwrap(); + assert_eq!( + video.editors(), + Ok(vec![( + vec![Person { + name: "Acerola".into(), + given_name: "".into(), + prefix: "".into(), + suffix: "".into() + }], + EditorType::Director + )]) + ); + + let music = bibliography.get("mozart_KV183_1773").unwrap(); + assert_eq!( + music.editors(), + Ok(vec![( + vec![Person { + name: "Mozart".into(), + given_name: "Wolfgang Amadeus".into(), + prefix: "".into(), + suffix: "".into() + }], + EditorType::Unknown("pianist".into()), + )]) + ); + + let audio = bibliography.get("Smith2018").unwrap(); + assert_eq!( + audio.editors(), + Ok(vec![ + ( + vec![Person { + name: "Smith".into(), + given_name: "Stacey Vanek".into(), + prefix: "".into(), + suffix: "".into() + }], + EditorType::Unknown("host".into()), + ), + ( + vec![Person { + name: "Plotkin".into(), + given_name: "Stanley".into(), + prefix: "".into(), + suffix: "".into() + }], + EditorType::Unknown("participant".into()), + ) + ]) + ); +}