File size: 7,678 Bytes
901bf8c | 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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """Documentation that must follow the code (the code audit of 2026-09-21,
follow-up of 2026-09-22: 'Remaining doc drift'). Offline; reads the source
tree and RUNBOOK.md.
- RUNBOOK's 'Environment variables' table lists every variable the code
reads (os.environ / env.get / getenv in canlex/ and tools/) and every one
the Dockerfile sets for CanLex or the Hugging Face library; it names no
CANLEX_* variable nothing reads or sets. (28 were undocumented, among them
CANLEX_HTTP and CANLEX_SEE_ALSO_DRAFT.)
- The refresh choreography names every ingester (irb_guidelines and conduct
were missing) and says every ingester takes --allow-shrink.
- The call-log section names every calllog.RESULT_KEYS field (it named five
of twelve).
- No RUNBOOK line cites a scratchpad script that is not in the repo (three
cited the a14/a15 before/after harness).
- canlex_list_acts' 'Bilingual' line counts French LEGISLATION only (it
counted 124 instruments -- the 80 Acts and regulations plus 44 French
guidance documents -- and 26,829 'articles' for 20,680).
"""
import re
import unittest
from pathlib import Path
from canlex import calllog, server
from canlex.index import LegislationIndex
ROOT = Path(__file__).resolve().parent.parent
RUNBOOK = (ROOT / "RUNBOOK.md").read_text(encoding="utf-8")
# os.environ.get("X"), os.environ["X"], env.get("X") (calllog.configure's
# env = os.environ), env["X"] (sweep.py), os.getenv("X"), and a module
# constant naming the variable a read uses, ENV_X = "CANLEX_X"
# (canlex/ratelimit.py reads its two variables through ENV_PER_MIN / ENV_BURST)
_ENV_READ = re.compile(
r"""\b(?:environ|env)(?:\.get\(|\[)\s*["']([A-Z][A-Z0-9_]+)["']"""
r"""|\bgetenv\(\s*["']([A-Z][A-Z0-9_]+)["']"""
r"""|^ENV_[A-Z0-9_]+\s*=\s*["']((?:CANLEX|HF|CANLII)_[A-Z0-9_]+)["']""", re.M)
_DOCKER_ENV = re.compile(r"^\s*(?:ENV\s+)?((?:CANLEX|HF|CANLII)_[A-Z0-9_]+|PORT)=", re.M)
def env_vars_read():
"""{name: [files]} for every environment variable the code reads."""
found = {}
for top in ("canlex", "tools"):
for path in sorted((ROOT / top).rglob("*.py")):
for m in _ENV_READ.finditer(path.read_text(encoding="utf-8")):
name = m.group(1) or m.group(2) or m.group(3)
found.setdefault(name, []).append(path.relative_to(ROOT).as_posix())
return found
def section(heading, level="##"):
"""The RUNBOOK text under `heading` up to the next heading of its level or above."""
start = RUNBOOK.index(heading)
stops = [m.start() for m in re.finditer(r"^#{1,%d} " % len(level), RUNBOOK, re.M)
if m.start() > start]
return RUNBOOK[start:stops[0] if stops else len(RUNBOOK)]
def table_names():
body = section("### Environment variables", "###")
return set(re.findall(r"^\| `([A-Z][A-Z0-9_]+)` \|", body, re.M))
class EnvironmentTableTests(unittest.TestCase):
def test_the_grep_finds_the_known_readers(self):
found = env_vars_read()
for name in ("CANLEX_HTTP", "CANLEX_SEE_ALSO_DRAFT", "CANLEX_LOG_DIR",
"CANLII_API_KEY", "PORT", "CANLEX_MCP_URL"):
self.assertIn(name, found)
self.assertGreaterEqual(len(found), 35)
def test_every_variable_the_code_reads_is_in_the_table(self):
missing = {n: f for n, f in env_vars_read().items() if n not in table_names()}
self.assertEqual(missing, {}, "add these to RUNBOOK 'Environment variables'")
def test_every_variable_the_dockerfile_sets_is_in_the_table(self):
docker = set(_DOCKER_ENV.findall((ROOT / "Dockerfile").read_text(encoding="utf-8")))
self.assertTrue({"HF_HOME", "HF_HUB_OFFLINE", "CANLEX_HTTP"} <= docker)
self.assertEqual(docker - table_names(), set())
def test_the_table_names_nothing_stale(self):
docker = set(_DOCKER_ENV.findall((ROOT / "Dockerfile").read_text(encoding="utf-8")))
stale = {n for n in table_names() if n.startswith("CANLEX_")
and n not in env_vars_read() and n not in docker}
self.assertEqual(stale, set())
def test_the_two_local_only_variables_say_so(self):
body = section("### Environment variables", "###")
http = next(l for l in body.splitlines() if l.startswith("| `CANLEX_HTTP`"))
draft = next(l for l in body.splitlines() if l.startswith("| `CANLEX_SEE_ALSO_DRAFT`"))
self.assertIn("Local use only", http)
self.assertIn("never set it on the Space", draft)
class ChoreographyTests(unittest.TestCase):
def ingesters(self):
"""Every canlex module whose command line takes --allow-shrink."""
return sorted(p.stem for p in (ROOT / "canlex").glob("*.py")
if '"--allow-shrink" in sys.argv' in p.read_text(encoding="utf-8"))
def test_every_ingester_is_in_the_choreography(self):
block = section("## The refresh choreography")
self.assertIn("irb_guidelines", self.ingesters())
missing = [m for m in self.ingesters() if f"py -m canlex.{m}" not in block]
self.assertEqual(missing, [])
def test_allow_shrink_is_documented_there(self):
self.assertIn("--allow-shrink", section("## The refresh choreography"))
class CallLogFieldTests(unittest.TestCase):
def test_every_result_key_is_named(self):
body = section("## Call log (2026-09-16)")
missing = [k for k in calllog.RESULT_KEYS if f"`{k}`" not in body]
self.assertEqual(missing, [])
def test_the_call_log_note_is_current(self):
# the 2026-09-21 audit's 'the live Space predates the call log' is gone
self.assertNotIn("live Space predates the call log", RUNBOOK)
self.assertIn("the live Space carries the call log", RUNBOOK)
class ScratchpadCitationTests(unittest.TestCase):
def test_no_line_cites_a_scratchpad_script(self):
cited = re.findall(r"scratchpad[^\n]{0,40}?\b\w+\.py", RUNBOOK)
self.assertEqual(cited, [])
def _chunk(cid, act_code, lang="en", doc_type="legislation", **extra):
c = {"id": cid, "doc_type": doc_type, "act_code": act_code, "act_short": act_code,
"act_name": act_code, "section": "1", "marginal_note": "t", "part": "",
"division": "", "heading": "", "text": "x", "history": "", "last_amended": "",
"in_force": "", "status": "in force", "current_to": "2026-03-31",
"citation": f"{act_code}, s. 1", "source_url": "https://example.test"}
if lang != "en":
c["lang"] = lang
c.update(extra)
return c
class ListActsBilingualLineTests(unittest.TestCase):
"""canlex_list_acts' coverage line (server.py; HELD: canlex_list_acts is
REVIEWED). Before: every French chunk counted, guidance included."""
def setUp(self):
idx = LegislationIndex.__new__(LegislationIndex)
idx.chunks = [
_chunk("I-2.5-s1", "I-2.5"), _chunk("I-2.5-fr-s1", "I-2.5", "fr"),
_chunk("I-2.5-fr-s2", "I-2.5", "fr"),
_chunk("C-46-s1", "C-46"), _chunk("C-46-fr-s1", "C-46", "fr"),
_chunk("dmemo-fr-1", "D-Memo", "fr", "memorandum", part="Mémorandum"),
_chunk("enf-fr-5-1", "ENF-5", "fr", "memorandum", part="ENF 5"),
_chunk("directive-fr-1", "d1", "fr", "directive"),
]
idx.related = lambda c: {}
server._INDEX = idx
self.addCleanup(setattr, server, "_INDEX", None)
def test_only_french_legislation_is_counted(self):
out = server.canlex_list_acts()
self.assertIn("_Bilingual: 2 instruments also carry their parallel French "
"text (3 articles)", out)
self.assertNotIn("5 instruments", out) # the count before the fix
|