| """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'<p class="MarginalNote"><span class="wb-invisible">Marginal ' |
| f'note:</span>{text}</p>') |
|
|
|
|
| def _section(num, body): |
| return (f'<p class="Section"><strong><a class="sectionLabel" id="s-{num}">' |
| f'<span class="sectionLabel">{num}</span></a></strong>\xa0{body}</p>') |
|
|
|
|
| |
| |
| FIXTURE = "".join([ |
| "<h1>CONSTITUTION ACT, 1867</h1>", |
| _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."), |
| "<h1>CONSTITUTION ACT, 1982</h1>", |
| _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:"), |
| '<ul class="ProvisionList"><li><p class="Paragraph">' |
| '<span class="lawlabel">(a)</span>\xa0freedom of conscience and religion;' |
| "</p></li></ul>", |
| _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): |
| |
| 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): |
| |
| 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): |
| |
| 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 = ("<h1>CONSTITUTION ACT, 1982</h1>" + _note("Schedule heading") + |
| "<p>prose with no section label</p>") |
| self.assertEqual(charter.parse(html), []) |
|
|
| def test_a_reshaped_page_scrapes_to_nothing(self): |
| |
| |
| |
| html = ("<h1>CONSTITUTION ACT, 1982</h1>" |
| '<table id="const-tbl" class="wb-tables"></table>') |
| self.assertEqual(charter.parse(html), []) |
|
|
| def test_losing_the_1982_heading_is_an_error_not_a_half_corpus(self): |
| |
| with self.assertRaises(ValueError): |
| charter.parse("<h1>CONSTITUTION ACT, 1867</h1>" + _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() |
|
|