File size: 6,400 Bytes
6ce7899 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """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>')
# 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([
"<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):
# 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 <p>.
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):
# 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 = ("<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):
# Without the split every 1982 section would be filed under 1867.
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()
|