"""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''
f'{num}\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()