"""Unit tests for the Charter / Constitution ingester (canlex/charter.py). Offline only -- every test drives parse() with a fixture cut from the real Justice Laws markup. The point is the failure mode that emptied dmemos.json on 2026-07-27: if the upstream page is re-shaped the scrape returns nothing, so parse() must be provably capable of returning [] and build() must refuse to write that over the stored corpus. """ import sys import unittest from unittest import mock from canlex import charter def _note(text): return (f'

Marginal ' f'note:{text}

') def _section(num, body): return (f'

\xa0{body}

') # Both Acts in sequence, as the page serves them. Section numbers restart at # the 1982 heading, so "1" appears in each -- only the 1982 one is in scope. FIXTURE = "".join([ "

CONSTITUTION ACT, 1867

", _note("Short title"), _section(1, "This Act may be cited as the Constitution Act, 1867."), _note("Legislative Authority of Parliament of Canada"), _section(91, "It shall be lawful for the Queen to make Laws for the " "Peace, Order, and good Government of Canada."), _note("Subjects of exclusive Provincial Legislation"), _section(92, "In each Province the Legislature may exclusively make Laws."), "

CONSTITUTION ACT, 1982

", _note("Rights and freedoms in Canada"), _section(1, "The Canadian Charter of Rights and Freedoms guarantees the " "rights and freedoms set out in it."), _note("Fundamental freedoms"), _section(2, "Everyone has the following fundamental freedoms:"), '", _note("Detention or imprisonment"), _section(9, "Everyone has the right not to be arbitrarily detained."), _note("Commitment to promote equal opportunities"), _section(36, "Parliament and the legislatures are committed to promoting " "equal opportunities."), _note("Primacy of Constitution of Canada"), _section(52, "The Constitution of Canada is the supreme law of Canada."), ]) class ParseTests(unittest.TestCase): def setUp(self): self.chunks = charter.parse(FIXTURE) self.by_id = {c["id"]: c for c in self.chunks} def test_sections_land_in_the_act_they_belong_to(self): # s. 1 exists in both Acts; the 1982 one is the Charter's. self.assertIn("CONST-1982-s1", self.by_id) self.assertNotIn("CONST-1867-s1", self.by_id) self.assertIn("CONST-1867-s91", self.by_id) def test_ids_stay_unique_across_the_restart_in_numbering(self): ids = [c["id"] for c in self.chunks] self.assertEqual(len(ids), len(set(ids))) def test_out_of_scope_sections_are_dropped(self): # 1982 stops at s. 35 (plus s. 52); 1867 keeps only ss. 91-92. self.assertNotIn("CONST-1982-s36", self.by_id) self.assertIn("CONST-1982-s52", self.by_id) self.assertEqual(sorted(c["section"] for c in self.chunks if c["act_code"] == "CONST-1867"), ["91", "92"]) def test_marginal_note_drops_the_invisible_label(self): self.assertEqual(self.by_id["CONST-1982-s9"]["marginal_note"], "Detention or imprisonment") def test_section_text_runs_to_the_next_marginal_note(self): # s. 2's paragraphs live in a sibling list, outside the Section

. self.assertIn("freedom of conscience", self.by_id["CONST-1982-s2"]["text"]) self.assertNotIn("arbitrarily detained", self.by_id["CONST-1982-s2"]["text"]) def test_chunks_are_citable_legislation(self): chunk = self.by_id["CONST-1982-s9"] self.assertEqual(chunk["doc_type"], "legislation") self.assertEqual(chunk["citation"], "Charter, s. 9") self.assertEqual(chunk["source_url"], charter.URL) def test_marginal_note_without_a_section_label_is_skipped(self): html = ("

CONSTITUTION ACT, 1982

" + _note("Schedule heading") + "

prose with no section label

") self.assertEqual(charter.parse(html), []) def test_a_reshaped_page_scrapes_to_nothing(self): # What a JS-rendered rebuild would look like: heading still there, the # MarginalNote/Section markup gone. This is the empty write the guard # in build() exists to refuse. html = ("

CONSTITUTION ACT, 1982

" '
') self.assertEqual(charter.parse(html), []) def test_losing_the_1982_heading_is_an_error_not_a_half_corpus(self): # Without the split every 1982 section would be filed under 1867. with self.assertRaises(ValueError): charter.parse("

CONSTITUTION ACT, 1867

" + _note("x") + _section(91, "text")) class MainTests(unittest.TestCase): """main() has to make a refused write visible to refresh.py, which runs this module as a subprocess and only sees the exit status.""" def _run(self, argv, build_result=True): with mock.patch.object(charter, "build") as build: build.return_value = build_result with mock.patch.object(sys, "argv", argv): with self.assertRaises(SystemExit) as caught: charter.main() return caught.exception.code, build def test_refused_write_exits_nonzero(self): code, _build = self._run(["charter"], build_result=False) self.assertEqual(code, 1) def test_successful_write_exits_zero(self): code, _build = self._run(["charter"], build_result=True) self.assertEqual(code, 0) def test_allow_shrink_flag_reaches_build(self): _code, build = self._run(["charter", "--allow-shrink"]) build.assert_called_once_with(allow_shrink=True) def test_guard_is_on_by_default(self): _code, build = self._run(["charter"]) build.assert_called_once_with(allow_shrink=False) if __name__ == "__main__": unittest.main()