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()