"""Tests for src/cmos/linter.py — deterministic CMOS 18 structural rules. Each rule has a positive and a negative unit test. Doc comments cite the source that justifies the rule (Purdue OWL page or CMOS quick guide URL). The linter only checks structural properties of the RAW output string (italic markers as emitted, punctuation as emitted). Fact survival (author/title/year/etc. matching canonical) is the diff's job, not the linter's, so we deliberately don't duplicate it here. """ from cmos.linter import ( LINTER_VERSION, check_passes, lint, rule_article_title_quoted, rule_book_title_italicized, rule_doi_is_https_url, rule_ends_with_period, rule_journal_title_italicized, rule_no_ibid, rule_no_place_of_publication_for_books, rule_page_range_uses_en_dash, rule_web_page_title_quoted, ) from harness.score import Exemplar BOOK = Exemplar( name="book", source="test", type="book", tags=["book"], canary=False, messy_input="", canonical={ "author": "Yu, Charles", "title": "Interior Chinatown", "publisher": "Pantheon Books", "year": 2020, }, expected_bibliography="Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020.", ) JOURNAL = Exemplar( name="journal", source="test", type="journal", tags=["journal", "doi"], canary=False, messy_input="", canonical={ "author": "Kwon, Hyeyoung", "article_title": "Inclusion Work: Children of Immigrants Claiming Membership in Everyday Life", "journal": "American Journal of Sociology", "volume": 127, "issue": 6, "year": 2022, "pages": "1818–59", "doi": "https://doi.org/10.1086/720277", }, expected_bibliography=( 'Kwon, Hyeyoung. "Inclusion Work: Children of Immigrants Claiming Membership in Everyday Life." ' "*American Journal of Sociology* 127, no. 6 (2022): 1818–59. https://doi.org/10.1086/720277." ), ) WEB = Exemplar( name="web", source="test", type="web", tags=["web"], canary=False, messy_input="", canonical={ "author": "Google", "title": "Privacy Policy", "site": "Privacy & Terms", "date": "November 15, 2023", "url": "https://policies.google.com/privacy", }, expected_bibliography='Google. "Privacy Policy." Privacy & Terms. Effective November 15, 2023. https://policies.google.com/privacy.', ) def test_linter_version_is_set(): assert LINTER_VERSION assert isinstance(LINTER_VERSION, str) def test_ends_with_period_passes_on_well_formed_entry(): # CMOS 18: bibliography entries separate major elements by periods and # end with a period. Source: chicagomanualofstyle.org quick guide. result = rule_ends_with_period("Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020.", BOOK) assert result.applicable is True assert result.passed is True def test_ends_with_period_fails_on_missing_period(): result = rule_ends_with_period("Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020", BOOK) assert result.applicable is True assert result.passed is False # --- No place of publication for books (CMOS 18 change from 17, CMOS 14.30) ---- def test_no_place_of_publication_passes_when_absent(): # CMOS 18: place of publication no longer required for books. # Source: CMOS 14.30 (cited on chicagomanualofstyle.org quick guide). result = rule_no_place_of_publication_for_books( "Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020.", BOOK ) assert result.applicable is True assert result.passed is True def test_no_place_of_publication_fails_when_city_prefix_present(): result = rule_no_place_of_publication_for_books( "Yu, Charles. *Interior Chinatown*. New York: Pantheon Books, 2020.", BOOK ) assert result.passed is False def test_no_place_of_publication_not_applicable_to_journals(): # The rule should not fire on non-book types even if their output happens # to contain a colon (which journals routinely do, e.g., article title). result = rule_no_place_of_publication_for_books("anything: anything.", JOURNAL) assert result.applicable is False # --- Book title italicized ---------------------------------------------------- def test_book_title_italicized_passes_when_wrapped_in_asterisks(): # CMOS 18 bibliography basics: titles of books and journals are italicized. # Source: chicagomanualofstyle.org quick guide; Purdue OWL CMOS 18 NB poster # page 3 "Bibliography: Basics". result = rule_book_title_italicized( "Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020.", BOOK ) assert result.passed is True def test_book_title_italicized_fails_when_title_plain(): result = rule_book_title_italicized( "Yu, Charles. Interior Chinatown. Pantheon Books, 2020.", BOOK ) assert result.passed is False def test_book_title_italicized_not_applicable_to_web(): result = rule_book_title_italicized("whatever", WEB) assert result.applicable is False # --- No "Ibid." (CMOS 18 deprecation) ----------------------------------------- def test_no_ibid_passes_on_clean_entry(): # CMOS 18 deprecates "Ibid." in favor of shortened notes. # Source: Widener "New in 18th" LibGuide. result = rule_no_ibid("Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020.", BOOK) assert result.passed is True def test_no_ibid_fails_when_ibid_present(): result = rule_no_ibid("Ibid., 42.", BOOK) assert result.passed is False # --- Journal title italicized -------------------------------------------------- def test_journal_title_italicized_passes_when_wrapped(): result = rule_journal_title_italicized(JOURNAL.expected_bibliography, JOURNAL) assert result.passed is True def test_journal_title_italicized_fails_when_plain(): bad = JOURNAL.expected_bibliography.replace( "*American Journal of Sociology*", "American Journal of Sociology" ) result = rule_journal_title_italicized(bad, JOURNAL) assert result.passed is False # --- Article title in quotes --------------------------------------------------- def test_article_title_quoted_passes_when_wrapped_in_double_quotes(): # CMOS 18: titles of articles, chapters, poems are in quotation marks. # Source: Purdue OWL CMOS 18 NB poster page 3 Bibliography Basics. result = rule_article_title_quoted(JOURNAL.expected_bibliography, JOURNAL) assert result.passed is True def test_article_title_quoted_fails_when_unquoted(): bad = JOURNAL.expected_bibliography.replace( '"Inclusion Work: Children of Immigrants Claiming Membership in Everyday Life."', "Inclusion Work: Children of Immigrants Claiming Membership in Everyday Life.", ) result = rule_article_title_quoted(bad, JOURNAL) assert result.passed is False # --- Page range uses en-dash --------------------------------------------------- def test_page_range_uses_en_dash_passes(): # CMOS convention: page ranges use en-dash (–), not ASCII hyphen (-). result = rule_page_range_uses_en_dash(JOURNAL.expected_bibliography, JOURNAL) assert result.passed is True def test_page_range_uses_en_dash_fails_on_ascii_hyphen(): bad = JOURNAL.expected_bibliography.replace("1818–59", "1818-59") result = rule_page_range_uses_en_dash(bad, JOURNAL) assert result.passed is False # --- DOI as https://doi.org/ URL ----------------------------------------------- def test_doi_is_https_url_passes_when_canonical_prefix_present(): # CMOS 18 prefers DOIs formatted as https://doi.org/... rather than bare # DOIs or "DOI:" prefixes. Source: CMOS 18 quick guide. result = rule_doi_is_https_url(JOURNAL.expected_bibliography, JOURNAL) assert result.passed is True def test_doi_is_https_url_fails_on_bare_doi(): bad = JOURNAL.expected_bibliography.replace("https://doi.org/10.1086/720277", "10.1086/720277") result = rule_doi_is_https_url(bad, JOURNAL) assert result.passed is False def test_doi_rule_not_applicable_without_doi(): # Web exemplar has no DOI field. result = rule_doi_is_https_url(WEB.expected_bibliography, WEB) assert result.applicable is False # --- Web page title in quotes -------------------------------------------------- def test_web_page_title_quoted_passes(): result = rule_web_page_title_quoted(WEB.expected_bibliography, WEB) assert result.passed is True def test_web_page_title_quoted_fails_when_unquoted(): bad = WEB.expected_bibliography.replace('"Privacy Policy."', "Privacy Policy.") result = rule_web_page_title_quoted(bad, WEB) assert result.passed is False # --- Aggregate check_passes --------------------------------------------------- def test_check_passes_on_canonical_outputs(): # All three seed exemplars' expected_bibliography strings must lint clean. # If any of them fail, either the linter is too strict or the exemplar is # wrong — either way, we want to know immediately. assert check_passes(BOOK.expected_bibliography, BOOK) assert check_passes(JOURNAL.expected_bibliography, JOURNAL) assert check_passes(WEB.expected_bibliography, WEB)