"""Testes do parser de corpus USFX. O corpus é XML estruturado: não há OCR, nem página, nem reconstrução de capítulo/versículo. O que pode dar errado é o mapeamento estrutural (texto no `tail` de ``, capítulo corrente, nome do livro em ``) e a normalização do texto. Os testes usam um USFX sintético mínimo para isolar cada regra, mais uma verificação de integridade contra o corpus real versionado. """ from __future__ import annotations import sys from pathlib import Path import pytest BACKEND = Path(__file__).resolve().parents[1] REPO_ROOT = BACKEND.parent sys.path.insert(0, str(BACKEND)) from app.corpus import ( # noqa: E402 BOOKS, CorpusParseError, VerseChunk, canonical_book, parse_usfx, ) REAL_CORPUS = REPO_ROOT / "corpus" / "por-almeida.usfx.xml" def write_usfx(tmp_path: Path, body: str) -> Path: path = tmp_path / "corpus.usfx.xml" path.write_text( "\n" "\n" f"{body}\n\n", encoding="utf-8", ) return path # -------------------------------------------------------------------------- # Mapeamento estrutural # -------------------------------------------------------------------------- def test_verse_text_comes_from_the_tail_of_the_v_element(tmp_path): """`` é auto-fechado: o texto é o tail, não filho.""" path = write_usfx( tmp_path, """ Gênesis No princípio criou Deus os céus e a terra. E a terra era sem forma e vazia. """, ) chunks = list(parse_usfx(path)) assert [chunk.verse for chunk in chunks] == [1, 2] assert chunks[0].text == "No princípio criou Deus os céus e a terra." assert chunks[1].text == "E a terra era sem forma e vazia." def test_verse_numbers_restart_at_each_chapter(tmp_path): """O capítulo corrente vem do último ``; o verso reinicia em cada um.""" path = write_usfx( tmp_path, """ Gênesis Um. Dois. Três. """, ) chunks = list(parse_usfx(path)) assert [(c.chapter, c.verse, c.text) for c in chunks] == [ (1, 1, "Um."), (1, 2, "Dois."), (2, 1, "Três."), ] def test_book_name_comes_from_the_h_element(tmp_path): """A grafia canônica é a que o corpus declara em ``, não uma lista fixa.""" path = write_usfx( tmp_path, """ 1 Samuel Texto. """, ) chunks = list(parse_usfx(path)) assert chunks[0].book == "1 Samuel" def test_file_name_is_recorded_for_provenance(tmp_path): path = write_usfx( tmp_path, """ GênesisTexto. """, ) chunks = list(parse_usfx(path)) assert chunks[0].file == "corpus.usfx.xml" def test_explicit_file_name_overrides_the_path_basename(tmp_path): path = write_usfx( tmp_path, """ GênesisTexto. """, ) chunks = list(parse_usfx(path, file_name="almeida")) assert chunks[0].file == "almeida" # -------------------------------------------------------------------------- # Normalização de texto # -------------------------------------------------------------------------- def test_non_breaking_space_and_runs_of_whitespace_are_normalized(tmp_path): """O corpus real usa \\xa0; espaços múltiplos viram um só.""" path = write_usfx( tmp_path, """ Gênesis Primeira\u00a0parte segunda parte.\u00a0\u00a0 """, ) chunks = list(parse_usfx(path)) assert chunks[0].text == "Primeira parte segunda parte." assert "\xa0" not in chunks[0].text def test_placeholder_verses_are_skipped(tmp_path): """Versículo sem letra nem dígito é ruído: não pode virar chunk.""" path = write_usfx( tmp_path, """ Mateus Versículo real. — Outro versículo real. """, ) chunks = list(parse_usfx(path)) assert [chunk.verse for chunk in chunks] == [20, 22] def test_verse_without_closing_ve_still_yields_its_text(tmp_path): """`` é marcador de fim; sua ausência não pode descartar o versículo.""" path = write_usfx( tmp_path, """ Gênesis Texto sem marcador de fim. """, ) chunks = list(parse_usfx(path)) assert len(chunks) == 1 assert chunks[0].text == "Texto sem marcador de fim." # -------------------------------------------------------------------------- # Erros # -------------------------------------------------------------------------- def test_malformed_xml_raises_corpus_parse_error(tmp_path): path = tmp_path / "quebrado.usfx.xml" path.write_text("Gênesis", encoding="utf-8") with pytest.raises(CorpusParseError): list(parse_usfx(path)) def test_missing_file_raises_corpus_parse_error(tmp_path): with pytest.raises(CorpusParseError): list(parse_usfx(tmp_path / "nao-existe.usfx.xml")) def test_document_without_books_raises_corpus_parse_error(tmp_path): """Um XML válido mas sem nenhum livro é corpus inútil: falha explícita.""" path = write_usfx(tmp_path, "") with pytest.raises(CorpusParseError): list(parse_usfx(path)) # -------------------------------------------------------------------------- # Identidade do chunk # -------------------------------------------------------------------------- def test_reference_uses_livro_capitulo_versiculo(): chunk = VerseChunk( book="João", chapter=3, verse=16, file="almeida", text="Porque Deus amou" ) assert chunk.reference == "João 3:16" def test_chunk_id_is_deterministic_and_unique(): first = VerseChunk(book="João", chapter=3, verse=16, file="a", text="x") same = VerseChunk(book="João", chapter=3, verse=16, file="a", text="x") other_verse = VerseChunk(book="João", chapter=3, verse=17, file="a", text="x") other_file = VerseChunk(book="João", chapter=3, verse=16, file="b", text="x") assert first.chunk_id == same.chunk_id assert len({first.chunk_id, other_verse.chunk_id, other_file.chunk_id}) == 3 def test_chunk_id_does_not_depend_on_the_text(): """Reindexar com OCR/edição diferente não pode trocar a identidade do verso.""" a = VerseChunk(book="João", chapter=3, verse=16, file="a", text="versão 1") b = VerseChunk(book="João", chapter=3, verse=16, file="a", text="versão 2") assert a.chunk_id == b.chunk_id # -------------------------------------------------------------------------- # Nomes canônicos e aliases # -------------------------------------------------------------------------- @pytest.mark.parametrize( "name,expected", [ ("João", "João"), ("1 Samuel", "1 Samuel"), ("I Samuel", "1 Samuel"), ("II Reis", "2 Reis"), ("III João", "3 João"), ("Gn", "Gênesis"), ("Sl", "Salmos"), ("1Co", "1 Coríntios"), ("Ap", "Apocalipse"), ("Cântico dos Cânticos", "Cânticos"), ("Cantares", "Cânticos"), ("Lamentações de Jeremias", "Lamentações"), ("Atos dos Apóstolos", "Atos"), ("Salmo", "Salmos"), ], ) def test_canonical_book_resolves_variants(name, expected): assert canonical_book(name) == expected @pytest.mark.parametrize("name", ["joao", "JOÃO", " João ", "Isaias", "Miqueias"]) def test_canonical_book_is_case_and_accent_insensitive(name): assert canonical_book(name) is not None def test_canonical_book_rejects_unknown_names(): assert canonical_book("Evangelho de Tomé") is None assert canonical_book("") is None def test_books_covers_the_full_protestant_canon(): """66 livros, incluindo Neemias e Ester — ausentes na edição em PDF.""" assert len(BOOKS) == 66 assert "Neemias" in BOOKS assert "Ester" in BOOKS assert BOOKS[0] == "Gênesis" assert BOOKS[-1] == "Apocalipse" # -------------------------------------------------------------------------- # Integridade contra o corpus real versionado # -------------------------------------------------------------------------- @pytest.mark.skipif(not REAL_CORPUS.exists(), reason="corpus real não versionado") def test_real_corpus_parses_completely(): chunks = list(parse_usfx(REAL_CORPUS)) assert len(chunks) == 31098 assert len({chunk.book for chunk in chunks}) == 66 assert len({chunk.chunk_id for chunk in chunks}) == len(chunks), "ids duplicados" assert all(chunk.text for chunk in chunks) assert all(chunk.chapter >= 1 and chunk.verse >= 1 for chunk in chunks) @pytest.mark.skipif(not REAL_CORPUS.exists(), reason="corpus real não versionado") def test_real_corpus_keeps_known_verses_intact(): chunks = {(c.book, c.chapter, c.verse): c.text for c in parse_usfx(REAL_CORPUS)} assert chunks[("Gênesis", 1, 1)] == "No princípio criou Deus os céus e a terra." assert chunks[("Salmos", 23, 1)].startswith("O Senhor é o meu pastor") assert chunks[("João", 3, 16)].startswith("Porque Deus amou o mundo") @pytest.mark.skipif(not REAL_CORPUS.exists(), reason="corpus real não versionado") def test_real_corpus_declares_every_book_as_canonical(): """Nenhum `` do corpus pode ficar de fora de BOOKS/aliases.""" books = {chunk.book for chunk in parse_usfx(REAL_CORPUS)} assert books <= set(BOOKS)