arxo-codegen 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arxo_codegen/__init__.py +5 -0
- arxo_codegen/__main__.py +7 -0
- arxo_codegen/active-outcomes.json +6837 -0
- arxo_codegen/check.py +158 -0
- arxo_codegen/check_active_canaries.py +262 -0
- arxo_codegen/check_api.py +76 -0
- arxo_codegen/check_conformance.py +414 -0
- arxo_codegen/check_matrix.py +131 -0
- arxo_codegen/check_packages.py +281 -0
- arxo_codegen/check_semantic_probes.py +293 -0
- arxo_codegen/check_xlsx.py +320 -0
- arxo_codegen/consumer_runner.py +316 -0
- arxo_codegen/core_support.py +203 -0
- arxo_codegen/coverage.py +341 -0
- arxo_codegen/deliver.py +180 -0
- arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
- arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
- arxo_codegen/excel_plan.py +602 -0
- arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
- arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
- arxo_codegen/inert-nodes.json +62 -0
- arxo_codegen/inert.py +91 -0
- arxo_codegen/inputs.py +59 -0
- arxo_codegen/package.py +565 -0
- arxo_codegen/plan.py +4053 -0
- arxo_codegen/recertify_registry.py +100 -0
- arxo_codegen/runtime-cert.json +5259 -0
- arxo_codegen/runtime_cert.py +233 -0
- arxo_codegen/survey.py +272 -0
- arxo_codegen/target_runner.py +101 -0
- arxo_codegen/targets/bsl/cert.bsl +113 -0
- arxo_codegen/targets/bsl/emit.py +356 -0
- arxo_codegen/targets/bsl/json.bsl +243 -0
- arxo_codegen/targets/bsl/runner.bsl +210 -0
- arxo_codegen/targets/bsl/runtime.bsl +858 -0
- arxo_codegen/targets/cs/Cert.cs +90 -0
- arxo_codegen/targets/cs/Runner.cs +154 -0
- arxo_codegen/targets/cs/Runtime.cs +243 -0
- arxo_codegen/targets/cs/emit.py +223 -0
- arxo_codegen/targets/dart/cert.dart +99 -0
- arxo_codegen/targets/dart/emit.py +206 -0
- arxo_codegen/targets/dart/json.dart +166 -0
- arxo_codegen/targets/dart/runner.dart +154 -0
- arxo_codegen/targets/dart/runtime.dart +293 -0
- arxo_codegen/targets/erl/cert.erl +64 -0
- arxo_codegen/targets/erl/emit.py +216 -0
- arxo_codegen/targets/erl/rt.erl +238 -0
- arxo_codegen/targets/erl/runner.erl +130 -0
- arxo_codegen/targets/ex/cert.exs +62 -0
- arxo_codegen/targets/ex/emit.py +204 -0
- arxo_codegen/targets/ex/runner.exs +124 -0
- arxo_codegen/targets/ex/runtime.ex +227 -0
- arxo_codegen/targets/go/apiBase.go +483 -0
- arxo_codegen/targets/go/apicheck.go +97 -0
- arxo_codegen/targets/go/cert.go +218 -0
- arxo_codegen/targets/go/emit.py +1806 -0
- arxo_codegen/targets/go/evaluate.go +1826 -0
- arxo_codegen/targets/go/runner.go +38 -0
- arxo_codegen/targets/go/runtime.go +2612 -0
- arxo_codegen/targets/java/Cert.java +91 -0
- arxo_codegen/targets/java/Json.java +148 -0
- arxo_codegen/targets/java/Runner.java +151 -0
- arxo_codegen/targets/java/Runtime.java +243 -0
- arxo_codegen/targets/java/emit.py +198 -0
- arxo_codegen/targets/kt/Cert.kt +67 -0
- arxo_codegen/targets/kt/Json.kt +103 -0
- arxo_codegen/targets/kt/Runner.kt +126 -0
- arxo_codegen/targets/kt/Runtime.kt +196 -0
- arxo_codegen/targets/kt/emit.py +195 -0
- arxo_codegen/targets/lua/cert.lua +82 -0
- arxo_codegen/targets/lua/emit.py +250 -0
- arxo_codegen/targets/lua/json.lua +133 -0
- arxo_codegen/targets/lua/runner.lua +167 -0
- arxo_codegen/targets/lua/runtime.lua +541 -0
- arxo_codegen/targets/php/cert.php +84 -0
- arxo_codegen/targets/php/emit.py +229 -0
- arxo_codegen/targets/php/json.php +144 -0
- arxo_codegen/targets/php/runner.php +189 -0
- arxo_codegen/targets/php/runtime.php +660 -0
- arxo_codegen/targets/py/api_base.py +334 -0
- arxo_codegen/targets/py/apicheck.py +51 -0
- arxo_codegen/targets/py/cert.py +140 -0
- arxo_codegen/targets/py/emit.py +1140 -0
- arxo_codegen/targets/py/evaluate.py +980 -0
- arxo_codegen/targets/py/runner.py +38 -0
- arxo_codegen/targets/py/runtime.py +1634 -0
- arxo_codegen/targets/rb/cert.rb +73 -0
- arxo_codegen/targets/rb/emit.py +203 -0
- arxo_codegen/targets/rb/runner.rb +130 -0
- arxo_codegen/targets/rb/runtime.rb +255 -0
- arxo_codegen/targets/rs/Cargo.lock +142 -0
- arxo_codegen/targets/rs/Cargo.toml +16 -0
- arxo_codegen/targets/rs/cert.rs +106 -0
- arxo_codegen/targets/rs/emit.py +233 -0
- arxo_codegen/targets/rs/prefetch.rs +1 -0
- arxo_codegen/targets/rs/runner.rs +181 -0
- arxo_codegen/targets/rs/runtime.rs +448 -0
- arxo_codegen/targets/scala/Cert.scala +77 -0
- arxo_codegen/targets/scala/Json.scala +140 -0
- arxo_codegen/targets/scala/Runner.scala +121 -0
- arxo_codegen/targets/scala/Runtime.scala +269 -0
- arxo_codegen/targets/scala/emit.py +193 -0
- arxo_codegen/targets/swift/cert.swift +105 -0
- arxo_codegen/targets/swift/emit.py +231 -0
- arxo_codegen/targets/swift/json.swift +208 -0
- arxo_codegen/targets/swift/runner.swift +133 -0
- arxo_codegen/targets/swift/runtime.swift +528 -0
- arxo_codegen/targets/ts/apiBase.ts +250 -0
- arxo_codegen/targets/ts/apicheck.ts +37 -0
- arxo_codegen/targets/ts/cert.ts +96 -0
- arxo_codegen/targets/ts/emit.py +1214 -0
- arxo_codegen/targets/ts/evaluate.ts +894 -0
- arxo_codegen/targets/ts/package-lock.json +45 -0
- arxo_codegen/targets/ts/package.json +9 -0
- arxo_codegen/targets/ts/runner.ts +31 -0
- arxo_codegen/targets/ts/runtime.ts +1369 -0
- arxo_codegen/targets/xlsx/README.ru.md +176 -0
- arxo_codegen/targets/xlsx/emit.py +198 -0
- arxo_codegen/temporal.py +59 -0
- arxo_codegen/test_alias_types.py +171 -0
- arxo_codegen/test_api_contract.py +89 -0
- arxo_codegen/test_calendar_policy.py +48 -0
- arxo_codegen/test_check.py +49 -0
- arxo_codegen/test_check_active_canaries.py +96 -0
- arxo_codegen/test_check_conformance.py +132 -0
- arxo_codegen/test_check_semantic_probes.py +85 -0
- arxo_codegen/test_check_xlsx.py +52 -0
- arxo_codegen/test_consumer_runner.py +175 -0
- arxo_codegen/test_core_support.py +80 -0
- arxo_codegen/test_e0212_decision_time.py +63 -0
- arxo_codegen/test_head_aggregate.py +71 -0
- arxo_codegen/test_inert.py +143 -0
- arxo_codegen/test_instant_types.py +113 -0
- arxo_codegen/test_key_conflict_scope.py +92 -0
- arxo_codegen/test_key_conflict_targets.py +17 -0
- arxo_codegen/test_package_layout.py +163 -0
- arxo_codegen/test_plan_language_version.py +44 -0
- arxo_codegen/test_positions_refusal.py +43 -0
- arxo_codegen/test_recertify_registry.py +77 -0
- arxo_codegen/test_ruby_target.py +41 -0
- arxo_codegen/test_runtime_cert.py +83 -0
- arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
- arxo_codegen/test_stage_r.py +108 -0
- arxo_codegen/test_standalone_inputs.py +71 -0
- arxo_codegen/test_support.py +150 -0
- arxo_codegen/test_survey_inputs.py +115 -0
- arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
- arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
- arxo_codegen-0.4.0.dist-info/RECORD +152 -0
- arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
- arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
- arxo_codegen-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check exported XLSX fixtures without importing the Law DSL oracle.
|
|
3
|
+
|
|
4
|
+
The publisher-side exporter computes ``expectations.json`` with lawref. This
|
|
5
|
+
consumer verifies fixture hashes and workbook structure, then optionally asks
|
|
6
|
+
LibreOffice to recalculate copies of the exported workbooks. It compares only
|
|
7
|
+
against those pinned expected results; it never imports lawref or implements
|
|
8
|
+
Excel formulae in Python.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
import datetime as dt
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import zipfile
|
|
23
|
+
from xml.etree import ElementTree as ET
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
FORMAT = "law.codegen-xlsx-fixtures/0.1"
|
|
27
|
+
CURRENT_REFUSAL = "цель xlsx не поддерживает возможности плана: admission:current"
|
|
28
|
+
XMLNS = {"s": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
|
|
29
|
+
REL = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id"
|
|
30
|
+
EPOCH = dt.date(1899, 12, 30)
|
|
31
|
+
HERE = Path(__file__).resolve().parent
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FixtureError(RuntimeError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sha(data: bytes) -> str:
|
|
39
|
+
return "sha256:" + hashlib.sha256(data).hexdigest()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load(fixtures: Path) -> tuple[dict, dict]:
|
|
43
|
+
fixtures = Path(fixtures)
|
|
44
|
+
if fixtures.is_symlink() or not fixtures.is_dir():
|
|
45
|
+
raise FixtureError("fixtures are not a directory")
|
|
46
|
+
for path in fixtures.rglob("*"):
|
|
47
|
+
if path.is_symlink():
|
|
48
|
+
raise FixtureError("fixtures contain a symlink")
|
|
49
|
+
try:
|
|
50
|
+
manifest = json.loads((fixtures / "manifest.json").read_bytes())
|
|
51
|
+
expected = json.loads((fixtures / "expectations.json").read_bytes())
|
|
52
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
53
|
+
raise FixtureError("invalid fixture JSON") from error
|
|
54
|
+
if manifest.get("format") != FORMAT or expected.get("format") != FORMAT:
|
|
55
|
+
raise FixtureError("unsupported fixture format")
|
|
56
|
+
files = manifest.get("files")
|
|
57
|
+
if not isinstance(files, list):
|
|
58
|
+
raise FixtureError("fixture manifest has no inventory")
|
|
59
|
+
seen = set()
|
|
60
|
+
for row in files:
|
|
61
|
+
if not isinstance(row, dict) or not isinstance(row.get("path"), str):
|
|
62
|
+
raise FixtureError("invalid fixture inventory row")
|
|
63
|
+
rel = row["path"]
|
|
64
|
+
if not rel or rel.startswith("/") or ".." in Path(rel).parts or rel in seen:
|
|
65
|
+
raise FixtureError("invalid fixture inventory path")
|
|
66
|
+
seen.add(rel); path = fixtures / rel
|
|
67
|
+
if path.is_symlink() or not path.is_file() or row.get("sha256") != sha(path.read_bytes()) or row.get("bytes") != path.stat().st_size:
|
|
68
|
+
raise FixtureError("fixture inventory hash mismatch")
|
|
69
|
+
actual = {path.relative_to(fixtures).as_posix() for path in fixtures.rglob("*") if path.is_file()}
|
|
70
|
+
if actual != seen | {"manifest.json"}:
|
|
71
|
+
raise FixtureError("fixture inventory differs from files")
|
|
72
|
+
if expected.get("semanticVersion") != "0.2.11":
|
|
73
|
+
raise FixtureError("fixture does not declare current semanticVersion 0.2.11")
|
|
74
|
+
if not isinstance(expected.get("programs"), dict) or not expected["programs"]:
|
|
75
|
+
raise FixtureError("fixture has no programs")
|
|
76
|
+
return manifest, expected
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def cached(path: Path) -> dict[str, object]:
|
|
80
|
+
with zipfile.ZipFile(path) as archive:
|
|
81
|
+
strings = []
|
|
82
|
+
if "xl/sharedStrings.xml" in archive.namelist():
|
|
83
|
+
strings = ["".join(node.itertext()) for node in ET.fromstring(archive.read("xl/sharedStrings.xml")).findall("s:si", XMLNS)]
|
|
84
|
+
relationships = {row.attrib["Id"]: row.attrib["Target"].lstrip("/") for row in ET.fromstring(archive.read("xl/_rels/workbook.xml.rels"))}
|
|
85
|
+
book = ET.fromstring(archive.read("xl/workbook.xml")); values: dict[str, object] = {}
|
|
86
|
+
for sheet in book.findall("s:sheets/s:sheet", XMLNS):
|
|
87
|
+
target = relationships[sheet.attrib[REL]]
|
|
88
|
+
target = target if target.startswith("xl/") else "xl/" + target
|
|
89
|
+
for cell in ET.fromstring(archive.read(target)).findall(".//s:c", XMLNS):
|
|
90
|
+
value = cell.find("s:v", XMLNS)
|
|
91
|
+
data: object = value.text if value is not None else None
|
|
92
|
+
if cell.get("t") == "inlineStr": data = "".join(cell.find("s:is", XMLNS).itertext())
|
|
93
|
+
elif cell.get("t") == "s" and data is not None: data = strings[int(data)]
|
|
94
|
+
elif cell.get("t") == "b": data = data == "1"
|
|
95
|
+
elif cell.get("t") not in ("str", "e") and data is not None: data = Decimal(data)
|
|
96
|
+
values[sheet.attrib["name"] + "!" + cell.attrib["r"]] = data
|
|
97
|
+
return values
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _sheets(archive: zipfile.ZipFile) -> dict[str, str]:
|
|
101
|
+
rels = {row.attrib["Id"]: row.attrib["Target"].lstrip("/") for row in ET.fromstring(archive.read("xl/_rels/workbook.xml.rels"))}
|
|
102
|
+
book = ET.fromstring(archive.read("xl/workbook.xml"))
|
|
103
|
+
return {sheet.attrib["name"]: (rels[sheet.attrib[REL]] if rels[sheet.attrib[REL]].startswith("xl/") else "xl/" + rels[sheet.attrib[REL]]) for sheet in book.findall("s:sheets/s:sheet", XMLNS)}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def edit_copy(source: Path, destination: Path, changes: dict[str, object]) -> None:
|
|
107
|
+
"""Edit only declared Inputs cells; formula XML is copied byte-for-byte."""
|
|
108
|
+
with zipfile.ZipFile(source) as original, zipfile.ZipFile(destination, "w", zipfile.ZIP_DEFLATED) as output:
|
|
109
|
+
sheets = _sheets(original)
|
|
110
|
+
for name in original.namelist():
|
|
111
|
+
data = original.read(name)
|
|
112
|
+
for sheet, target in sheets.items():
|
|
113
|
+
if name != target:
|
|
114
|
+
continue
|
|
115
|
+
document = ET.fromstring(data)
|
|
116
|
+
for cell in document.findall(".//s:c", XMLNS):
|
|
117
|
+
key = sheet + "!" + cell.get("r")
|
|
118
|
+
if key not in changes:
|
|
119
|
+
continue
|
|
120
|
+
for child in list(cell): cell.remove(child)
|
|
121
|
+
cell.attrib.pop("t", None); value = changes[key]
|
|
122
|
+
if isinstance(value, str):
|
|
123
|
+
cell.set("t", "inlineStr")
|
|
124
|
+
ET.SubElement(ET.SubElement(cell, "{" + XMLNS["s"] + "}is"), "{" + XMLNS["s"] + "}t").text = value
|
|
125
|
+
elif value is not None:
|
|
126
|
+
ET.SubElement(cell, "{" + XMLNS["s"] + "}v").text = str(value)
|
|
127
|
+
data = ET.tostring(document, encoding="utf-8", xml_declaration=True)
|
|
128
|
+
output.writestr(name, data)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def result_rows(values: dict[str, object], mapping: dict, predicate: str) -> list[list[object]]:
|
|
132
|
+
rows = []
|
|
133
|
+
for row in mapping["tables"][predicate]:
|
|
134
|
+
if values.get(row["status"]) == "PRESENT":
|
|
135
|
+
rows.append([values.get(address) for address in row["args"]])
|
|
136
|
+
return rows
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def same_rows(actual: list[list[object]], expected: list[list[str]]) -> bool:
|
|
140
|
+
"""Compare Excel numeric cells to oracle decimal strings by value, not scale."""
|
|
141
|
+
if len(actual) != len(expected): return False
|
|
142
|
+
def equivalent(left: object, right: str) -> bool:
|
|
143
|
+
if isinstance(left, Decimal):
|
|
144
|
+
try: return left == Decimal(right)
|
|
145
|
+
except Exception: return False
|
|
146
|
+
return left == right
|
|
147
|
+
unmatched = list(expected)
|
|
148
|
+
for row in actual:
|
|
149
|
+
for index, candidate in enumerate(unmatched):
|
|
150
|
+
if len(row) == len(candidate) and all(equivalent(left, right) for left, right in zip(row, candidate)):
|
|
151
|
+
unmatched.pop(index); break
|
|
152
|
+
else: return False
|
|
153
|
+
return not unmatched
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def changes_for(name: str, case: dict, mapping: dict, prefix: str) -> dict[str, object]:
|
|
157
|
+
change = case["changes"]
|
|
158
|
+
if name == "pricing":
|
|
159
|
+
first, second = mapping["inputs"][prefix + "quote"]
|
|
160
|
+
out = {}
|
|
161
|
+
for index, key in enumerate(("ident", "category", "amount")):
|
|
162
|
+
if key in change: out[first["args"][index]] = change[key]
|
|
163
|
+
if "enabled" in change: out[first["enabled"]] = change["enabled"]
|
|
164
|
+
if "legalTime" in change: out["Inputs!B4"] = None if change["legalTime"] is None else (dt.date.fromisoformat(change["legalTime"]) - EPOCH).days
|
|
165
|
+
if "second" in change:
|
|
166
|
+
out[second["enabled"]] = 1
|
|
167
|
+
out.update(dict(zip(second["args"], change["second"])))
|
|
168
|
+
return out
|
|
169
|
+
if name == "arithmetic":
|
|
170
|
+
row = mapping["inputs"][prefix + "operands"][0]
|
|
171
|
+
return {address: Decimal(value) for address, value in zip(row["args"], change.get("operands", []))}
|
|
172
|
+
if name == "discrete":
|
|
173
|
+
out = {}
|
|
174
|
+
for table, values in change.items():
|
|
175
|
+
row = mapping["inputs"][prefix + table][0]
|
|
176
|
+
if table == "dates":
|
|
177
|
+
values = [(dt.date.fromisoformat(value) - EPOCH).days for value in values]
|
|
178
|
+
out.update(dict(zip(row["args"], values)))
|
|
179
|
+
return out
|
|
180
|
+
raise FixtureError("unknown exported workbook")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def structural_tree(fixtures: Path, expected: dict) -> None:
|
|
184
|
+
for name, program in expected["programs"].items():
|
|
185
|
+
directory = fixtures / name
|
|
186
|
+
if program.get("expectedRefusal"):
|
|
187
|
+
if program["expectedRefusal"] != CURRENT_REFUSAL:
|
|
188
|
+
raise FixtureError(f"{name}: unqualified current XLSX refusal")
|
|
189
|
+
ir = json.loads((directory / "program.lawir.json").read_bytes())
|
|
190
|
+
if (ir.get("semanticVersion"), ir.get("schemaVersion")) != ("0.2.11", "law.core.ir/0.7"):
|
|
191
|
+
raise FixtureError(f"{name}: refusal fixture is not current")
|
|
192
|
+
if {p.name for p in directory.iterdir()} != {"program.lawir.json", "config.json"}:
|
|
193
|
+
raise FixtureError(f"{name}: refused workbook must not contain generated output")
|
|
194
|
+
continue
|
|
195
|
+
for required in ("model.xlsx", "cells.json", "law-backend.json", "program.lawir.json", "config.json"):
|
|
196
|
+
if not (directory / required).is_file(): raise FixtureError(f"{name}: missing {required}")
|
|
197
|
+
with zipfile.ZipFile(directory / "model.xlsx") as book:
|
|
198
|
+
names = set(book.namelist())
|
|
199
|
+
if not {"xl/workbook.xml", "xl/_rels/workbook.xml.rels"} <= names or any("vba" in item.lower() or "externallink" in item.lower() for item in names):
|
|
200
|
+
raise FixtureError(f"{name}: unsafe or incomplete workbook")
|
|
201
|
+
passport = json.loads((directory / "law-backend.json").read_bytes())
|
|
202
|
+
if passport.get("profile") != "excel-calculator/0.1": raise FixtureError(f"{name}: unexpected delivery profile")
|
|
203
|
+
for rel, digest in passport.get("files", {}).items():
|
|
204
|
+
if digest != sha((directory / rel).read_bytes()): raise FixtureError(f"{name}: delivery passport hash mismatch")
|
|
205
|
+
program_ir = json.loads((directory / "program.lawir.json").read_bytes())
|
|
206
|
+
if program_ir.get("semanticVersion") != "0.2.11": raise FixtureError(f"{name}: archived program leaked into active fixtures")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def structural(fixtures: Path, expected: dict) -> None:
|
|
210
|
+
structural_tree(fixtures, expected)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _backend_modules():
|
|
214
|
+
"""Support both a direct script and installed ``arxo_codegen`` package."""
|
|
215
|
+
try:
|
|
216
|
+
from . import core_support, deliver
|
|
217
|
+
except ImportError:
|
|
218
|
+
if str(HERE) not in sys.path: sys.path.insert(0, str(HERE))
|
|
219
|
+
import core_support, deliver
|
|
220
|
+
return core_support, deliver
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def regenerate(fixtures: Path, expected: dict, core_kit: Path, output: Path) -> None:
|
|
224
|
+
"""Regenerate pinned deliveries with the installed backend and core kit."""
|
|
225
|
+
core_support, deliver = _backend_modules()
|
|
226
|
+
core_support.configure_core_kit(core_kit)
|
|
227
|
+
core_support.validate_core_kit(core_kit)
|
|
228
|
+
try:
|
|
229
|
+
for name in expected["programs"]:
|
|
230
|
+
source, target = fixtures / name, output / name
|
|
231
|
+
target.mkdir(parents=True)
|
|
232
|
+
program = json.loads((source / "program.lawir.json").read_bytes())
|
|
233
|
+
config = json.loads((source / "config.json").read_bytes())
|
|
234
|
+
# Resources are deliberately explicit and empty: these fixtures do
|
|
235
|
+
# not rely on a neighbouring corpus or an implicit resolver.
|
|
236
|
+
if expected["programs"][name].get("expectedRefusal"):
|
|
237
|
+
try:
|
|
238
|
+
deliver.deliver(program, "xlsx", target, options=config, resources={})
|
|
239
|
+
except deliver.P.Refused as exc:
|
|
240
|
+
if str(exc) != CURRENT_REFUSAL:
|
|
241
|
+
raise FixtureError(f"{name}: wrong current XLSX refusal") from exc
|
|
242
|
+
else:
|
|
243
|
+
raise FixtureError(f"{name}: uncertified XLSX unexpectedly admitted")
|
|
244
|
+
if any(target.iterdir()):
|
|
245
|
+
raise FixtureError(f"{name}: refusal wrote output")
|
|
246
|
+
for asset in ("program.lawir.json", "config.json"):
|
|
247
|
+
shutil.copyfile(source / asset, target / asset)
|
|
248
|
+
continue
|
|
249
|
+
deliver.deliver(program, "xlsx", target, options=config, resources={})
|
|
250
|
+
for asset in ("program.lawir.json", "config.json"):
|
|
251
|
+
shutil.copyfile(source / asset, target / asset)
|
|
252
|
+
for asset in ("model.xlsx", "plan.json", "cells.json", "law-backend.json"):
|
|
253
|
+
if sha((source / asset).read_bytes()) != sha((target / asset).read_bytes()):
|
|
254
|
+
raise FixtureError(f"{name}: installed backend regenerated different {asset}")
|
|
255
|
+
structural_tree(output, expected)
|
|
256
|
+
finally:
|
|
257
|
+
core_support.configure_core_kit(None)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def recalculate(fixtures: Path, expected: dict, soffice: str) -> int:
|
|
261
|
+
if not Path(soffice).is_file(): raise FixtureError("LibreOffice executable is unavailable")
|
|
262
|
+
checked = 0
|
|
263
|
+
with tempfile.TemporaryDirectory(prefix="arxo-xlsx-check-") as temporary:
|
|
264
|
+
work = Path(temporary); incoming, outgoing = work / "incoming", work / "outgoing"; incoming.mkdir(); outgoing.mkdir()
|
|
265
|
+
pending = []
|
|
266
|
+
for name, program in expected["programs"].items():
|
|
267
|
+
mapping = json.loads((fixtures / name / "cells.json").read_bytes()); prefix = program["predicatePrefix"]
|
|
268
|
+
for case in program["cases"]:
|
|
269
|
+
target = incoming / f"{name}--{case['id']}.xlsx"
|
|
270
|
+
edit_copy(fixtures / name / "model.xlsx", target, changes_for(name, case, mapping, prefix))
|
|
271
|
+
pending.append((name, case, mapping, prefix, target.name))
|
|
272
|
+
command = [soffice, "-env:UserInstallation=" + (work / "profile").as_uri(), "--headless", "--convert-to", "xlsx", "--outdir", str(outgoing)] + [str(incoming / item[-1]) for item in pending]
|
|
273
|
+
result = subprocess.run(command, capture_output=True, text=True, timeout=120)
|
|
274
|
+
if result.returncode or len(list(outgoing.glob("*.xlsx"))) != len(pending):
|
|
275
|
+
raise FixtureError(f"LibreOffice recalculation failed: {result.returncode}\n{result.stdout}\n{result.stderr}")
|
|
276
|
+
for name, case, mapping, prefix, filename in pending:
|
|
277
|
+
values = cached(outgoing / filename)
|
|
278
|
+
if values.get("Results!B3") != case["status"]: raise FixtureError(f"{name}/{case['id']}: status mismatch")
|
|
279
|
+
if case["status"] == "READY" and any(isinstance(value, str) and value.startswith(("#REF!", "#DIV/0!", "#VALUE!", "#NAME?", "#N/A", "#NUM!")) for value in values.values()):
|
|
280
|
+
raise FixtureError(f"{name}/{case['id']}: spreadsheet formula error")
|
|
281
|
+
for table, expected_rows in case["results"].items():
|
|
282
|
+
actual = result_rows(values, mapping, prefix + table)
|
|
283
|
+
if table == "before":
|
|
284
|
+
actual = [[(EPOCH + dt.timedelta(days=int(Decimal(value)))).isoformat() for value in row] for row in actual]
|
|
285
|
+
if not same_rows(actual, expected_rows): raise FixtureError(f"{name}/{case['id']}/{table}: result mismatch: {actual!r} != {expected_rows!r}")
|
|
286
|
+
checked += 1
|
|
287
|
+
return checked
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def main() -> None:
|
|
291
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
292
|
+
parser.add_argument("--fixtures", required=True, type=Path)
|
|
293
|
+
parser.add_argument("--core-kit", type=Path, help="validate the selected core kit without importing lawref")
|
|
294
|
+
parser.add_argument("--recalculate", action="store_true")
|
|
295
|
+
parser.add_argument("--soffice", default=shutil.which("soffice") or "/Applications/LibreOffice.app/Contents/MacOS/soffice")
|
|
296
|
+
args = parser.parse_args()
|
|
297
|
+
try:
|
|
298
|
+
_, expected = load(args.fixtures); structural(args.fixtures, expected)
|
|
299
|
+
refused = sum(bool(p.get("expectedRefusal")) for p in expected["programs"].values())
|
|
300
|
+
print(f"check_xlsx: fixture integrity — PASS; qualified current refusals {refused}")
|
|
301
|
+
with tempfile.TemporaryDirectory(prefix="arxo-xlsx-regenerated-") as temporary:
|
|
302
|
+
active = args.fixtures
|
|
303
|
+
if args.core_kit:
|
|
304
|
+
active = Path(temporary) / "delivery"
|
|
305
|
+
regenerate(args.fixtures, expected, args.core_kit, active)
|
|
306
|
+
print("check_xlsx: installed backend regenerated pinned XLSX delivery — PASS")
|
|
307
|
+
if refused:
|
|
308
|
+
if not args.core_kit:
|
|
309
|
+
raise FixtureError("--core-kit required to execute qualified refusals")
|
|
310
|
+
print("check_xlsx: current XLSX unsupported; no workbook/recalculation PASS claimed")
|
|
311
|
+
elif args.recalculate:
|
|
312
|
+
print(f"check_xlsx: LibreOffice recalculated {recalculate(active, expected, args.soffice)} cases — PASS")
|
|
313
|
+
else:
|
|
314
|
+
print("check_xlsx: recalculation NOT CHECKED (pass --recalculate with --soffice to verify formulas)")
|
|
315
|
+
except (OSError, ValueError, FixtureError, zipfile.BadZipFile, subprocess.SubprocessError) as error:
|
|
316
|
+
parser.exit(1, f"check_xlsx: REFUSED: {error}\n")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
main()
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run an already-lowered consumer bundle without corpus, lawref, or CI code."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
HERE = Path(__file__).resolve().parent
|
|
15
|
+
if str(HERE) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(HERE))
|
|
17
|
+
import core_support
|
|
18
|
+
import deliver as D
|
|
19
|
+
import inputs
|
|
20
|
+
import plan as P
|
|
21
|
+
import target_runner as T
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
FORMAT = "law.codegen-consumer/0.1"
|
|
25
|
+
SET_FORMAT = "law.codegen-consumer-set/0.1"
|
|
26
|
+
FIELDS = ("truth_status", "evaluation_status", "result_kind", "normative_status")
|
|
27
|
+
UNSUPPORTED_FIELDS = frozenset(("applicability_status", "trigger_status", "value"))
|
|
28
|
+
POSITION_OBS = ("position", "position_status", "position_count", "positions_present")
|
|
29
|
+
UNSUPPORTED_OBS = frozenset(("applied", "any_applied", "issue", "any_issue", "issues_within", "issues_exactly",
|
|
30
|
+
"certificate", "conflicts", "judgment_requests", "judgment_request", "interpretations",
|
|
31
|
+
"argumentation_class", "refused", "collected", "collected_exactly", "collected_count",
|
|
32
|
+
"any_position", "blocked_by", "blockers", "deadline_trace_present", "deadline_trace",
|
|
33
|
+
"position_proved", "status_support"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def sha(data: bytes) -> str:
|
|
37
|
+
return "sha256:" + hashlib.sha256(data).hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _file(root: Path, entry: dict, label: str) -> Path:
|
|
41
|
+
if not isinstance(entry, dict) or set(entry) != {"path", "sha256"} or not all(isinstance(value, str) for value in entry.values()):
|
|
42
|
+
raise ValueError(f"{label}: expected {{path, sha256}}")
|
|
43
|
+
path = (root / entry["path"]).resolve()
|
|
44
|
+
if not path.is_relative_to(root) or not path.is_file():
|
|
45
|
+
raise ValueError(f"{label}: file is absent or outside bundle")
|
|
46
|
+
if sha(path.read_bytes()) != entry["sha256"]:
|
|
47
|
+
raise ValueError(f"{label}: SHA-256 differs")
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_bundle(path: Path, *, allow_set: bool = True) -> dict:
|
|
52
|
+
root = path.resolve()
|
|
53
|
+
manifest = root / "bundle.json"
|
|
54
|
+
try:
|
|
55
|
+
doc = json.loads(manifest.read_text(encoding="utf-8"))
|
|
56
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
57
|
+
raise ValueError(f"consumer bundle: {error}") from error
|
|
58
|
+
if isinstance(doc, dict) and doc.get("format") == SET_FORMAT and allow_set:
|
|
59
|
+
members = doc.get("groups")
|
|
60
|
+
skipped = doc.get("skipped", [])
|
|
61
|
+
if not isinstance(members, list) or not members:
|
|
62
|
+
raise ValueError("groups: non-empty list required")
|
|
63
|
+
if not isinstance(skipped, list) or not all(isinstance(value, str) for value in skipped):
|
|
64
|
+
raise ValueError("skipped: list of named source skips required")
|
|
65
|
+
manifests = [_file(root, item, "group") for item in members]
|
|
66
|
+
if any(item.name != "bundle.json" for item in manifests) or len(set(manifests)) != len(manifests):
|
|
67
|
+
raise ValueError("groups: distinct bundle.json manifests required")
|
|
68
|
+
return {"groups": [load_bundle(item.parent, allow_set=False) for item in manifests],
|
|
69
|
+
"source_skipped": skipped}
|
|
70
|
+
if not isinstance(doc, dict) or doc.get("format") != FORMAT:
|
|
71
|
+
raise ValueError(f"consumer bundle: expected format {FORMAT}")
|
|
72
|
+
program = _file(root, doc.get("program"), "program")
|
|
73
|
+
world_rows = doc.get("world", [])
|
|
74
|
+
if not isinstance(world_rows, list):
|
|
75
|
+
raise ValueError("world: expected list")
|
|
76
|
+
world = [_file(root, row, "world") for row in world_rows]
|
|
77
|
+
mode = doc.get("programMode", "source")
|
|
78
|
+
if mode not in ("source", "linked") or (mode == "linked" and world):
|
|
79
|
+
raise ValueError("programMode: source or prelinked program with empty world required")
|
|
80
|
+
tests = doc.get("tests")
|
|
81
|
+
if not isinstance(tests, list) or not tests:
|
|
82
|
+
raise ValueError("tests: non-empty list required")
|
|
83
|
+
loaded = []
|
|
84
|
+
for row in tests:
|
|
85
|
+
if not isinstance(row, dict) or not isinstance(row.get("id"), str):
|
|
86
|
+
raise ValueError("test: id required")
|
|
87
|
+
case, query = _file(root, row.get("case"), f"test {row['id']} case"), _file(root, row.get("query"), f"test {row['id']} query")
|
|
88
|
+
expectations = row.get("expectations", [])
|
|
89
|
+
if not isinstance(expectations, list):
|
|
90
|
+
raise ValueError(f"test {row['id']}: expectations must be a list")
|
|
91
|
+
query_doc = json.loads(query.read_text(encoding="utf-8"))
|
|
92
|
+
validate_expectations(row["id"], query_doc.get("kind"), expectations)
|
|
93
|
+
loaded.append(dict(row, case=case, query=query, expectations=expectations))
|
|
94
|
+
resources = doc.get("resources")
|
|
95
|
+
resource_file = _file(root, resources, "resources") if resources is not None else None
|
|
96
|
+
group = doc.get("group")
|
|
97
|
+
if not isinstance(group, dict) or set(group) != {"world", "concat", "resolve", "merge"} or group.get("merge") != "link" \
|
|
98
|
+
or any(not isinstance(group.get(key), list) or not all(isinstance(value, str) for value in group[key])
|
|
99
|
+
for key in ("world", "concat", "resolve")):
|
|
100
|
+
raise ValueError("group: explicit {world, concat, resolve, merge:'link'} required")
|
|
101
|
+
skipped_at_source = doc.get("skipped", [])
|
|
102
|
+
if not isinstance(skipped_at_source, list) or not all(isinstance(value, str) for value in skipped_at_source):
|
|
103
|
+
raise ValueError("skipped: list of named source skips required")
|
|
104
|
+
return {"root": root, "doc": doc, "program": program, "world": world, "tests": loaded,
|
|
105
|
+
"group": group, "source_skipped": skipped_at_source, "program_mode": mode,
|
|
106
|
+
"resource_file": resource_file}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def validate_expectations(identifier: str, query_kind: object, expectations: list) -> None:
|
|
110
|
+
allowed_fields = {"truth": {"truth_status", "evaluation_status", "result_kind"},
|
|
111
|
+
"positions": {"result_kind", "normative_status", "evaluation_status"}}
|
|
112
|
+
for item in expectations:
|
|
113
|
+
if not isinstance(item, dict):
|
|
114
|
+
raise ValueError(f"test {identifier}: expectation must be an object")
|
|
115
|
+
field, observation = item.get("field"), item.get("observation")
|
|
116
|
+
if field is not None:
|
|
117
|
+
if item.get("op") != "eq":
|
|
118
|
+
raise ValueError(f"test {identifier}: unsupported expectation op {item.get('op')!r}")
|
|
119
|
+
if field not in FIELDS and field not in UNSUPPORTED_FIELDS:
|
|
120
|
+
raise ValueError(f"test {identifier}: unknown expectation field {field!r}")
|
|
121
|
+
if query_kind in allowed_fields and field in FIELDS and field not in allowed_fields[query_kind]:
|
|
122
|
+
raise ValueError(f"test {identifier}: field {field!r} is inapplicable to query {query_kind!r}")
|
|
123
|
+
elif observation is not None:
|
|
124
|
+
if observation in POSITION_OBS:
|
|
125
|
+
if query_kind != "positions":
|
|
126
|
+
raise ValueError(f"test {identifier}: observation {observation!r} requires positions query")
|
|
127
|
+
elif observation not in UNSUPPORTED_OBS:
|
|
128
|
+
raise ValueError(f"test {identifier}: unknown unsupported observation {observation!r}")
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError(f"test {identifier}: expectation needs field or observation")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def expected(test: dict) -> tuple[dict, list[str], list[dict]]:
|
|
134
|
+
fields, skipped, positions = {}, [], []
|
|
135
|
+
for item in test["expectations"]:
|
|
136
|
+
if not isinstance(item, dict):
|
|
137
|
+
raise ValueError(f"test {test['id']}: expectation must be an object")
|
|
138
|
+
if item.get("op") == "eq" and item.get("field") in FIELDS:
|
|
139
|
+
fields[item["field"]] = item.get("value")
|
|
140
|
+
elif item.get("observation") in POSITION_OBS:
|
|
141
|
+
positions.append(item)
|
|
142
|
+
else:
|
|
143
|
+
skipped.append(str(item.get("observation") or item.get("field") or "?"))
|
|
144
|
+
return fields, skipped, positions
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def observe(item: dict, positions: list[dict]) -> bool:
|
|
148
|
+
value, kind = item.get("value"), item["observation"]
|
|
149
|
+
if kind == "positions_present":
|
|
150
|
+
found = bool(positions)
|
|
151
|
+
elif kind == "position_status":
|
|
152
|
+
found = any(row.get("status") == value for row in positions)
|
|
153
|
+
elif kind == "position_count":
|
|
154
|
+
found = sum(row.get("template") == value and row.get("status") == item.get("status", "") for row in positions) == item.get("count")
|
|
155
|
+
else:
|
|
156
|
+
found = any(row.get("template") == value and row.get("status") == item.get("status", "") for row in positions)
|
|
157
|
+
return found != bool(item.get("negated"))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def compare(answer: dict, test: dict) -> list[str]:
|
|
161
|
+
fields, _skips, position_expectations = expected(test)
|
|
162
|
+
kind = json.loads(test["query"].read_text(encoding="utf-8")).get("kind")
|
|
163
|
+
if kind == "truth":
|
|
164
|
+
actual = {"truth_status": answer.get("truthStatus"), "evaluation_status": answer.get("evaluationStatus"),
|
|
165
|
+
"result_kind": answer.get("resultKind", "PROPOSITION" if answer.get("truthStatus") in
|
|
166
|
+
{"TRUE_ONLY", "FALSE_ONLY", "BOTH", "NEITHER"} else None)}
|
|
167
|
+
elif kind == "positions":
|
|
168
|
+
actual = {"result_kind": answer.get("resultKind"), "normative_status": answer.get("normativeStatus"),
|
|
169
|
+
"evaluation_status": answer.get("evaluationStatus")}
|
|
170
|
+
bad_positions = [item["observation"] for item in position_expectations if not observe(item, answer.get("positions") or [])]
|
|
171
|
+
if bad_positions:
|
|
172
|
+
return ["наблюдения позиций не совпали: " + ", ".join(bad_positions)]
|
|
173
|
+
else:
|
|
174
|
+
return [f"пропуск: запрос {kind}"]
|
|
175
|
+
return [f"{name}: ожидалось {value!r}, получено {actual.get(name)!r}" for name, value in fields.items() if actual.get(name) != value]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def checkable(test: dict) -> bool:
|
|
179
|
+
fields, _skipped, positions = expected(test)
|
|
180
|
+
return bool(fields or positions)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def run(bundle: dict, targets: list[str], work: Path) -> tuple[dict, list[str]]:
|
|
184
|
+
if "groups" in bundle:
|
|
185
|
+
summaries, failures = [], []
|
|
186
|
+
for number, member in enumerate(bundle["groups"]):
|
|
187
|
+
summary, errors = run(member, targets, work / f"group{number:03d}")
|
|
188
|
+
summaries.append(summary)
|
|
189
|
+
failures.extend(f"group {number}: {error}" for error in errors)
|
|
190
|
+
if not errors and summary.get("matched", 0) == 0:
|
|
191
|
+
failures.append(f"group {number}: нет проверенных ожиданий")
|
|
192
|
+
return {"groups": summaries, "tests": sum(s.get("tests", 0) for s in summaries),
|
|
193
|
+
"matched": sum(s.get("matched", 0) for s in summaries),
|
|
194
|
+
"sourceSkipped": bundle["source_skipped"]}, failures
|
|
195
|
+
ir = json.loads(bundle["program"].read_text(encoding="utf-8"))
|
|
196
|
+
world = [json.loads(path.read_text(encoding="utf-8")) for path in bundle["world"]]
|
|
197
|
+
# Validate names/duplicates even though resolve_world receives documents as
|
|
198
|
+
# explicit extras rather than its path-index argument.
|
|
199
|
+
inputs.dependency_index(world)
|
|
200
|
+
resources = inputs.load_resources(bundle["resource_file"]) if bundle["resource_file"] else {}
|
|
201
|
+
negative = any(any((item.get("literal") or {}).get("polarity") == "negative" for item in
|
|
202
|
+
(json.loads(test["case"].read_text(encoding="utf-8")).get("assertions") or [])) for test in bundle["tests"])
|
|
203
|
+
# ``extra`` is documents, not the path index accepted by resolve_world.
|
|
204
|
+
# Retain every explicitly delivered world member afterwards: concat worlds
|
|
205
|
+
# may not be imported by the root but still change its program.
|
|
206
|
+
resolved = P.resolve_world(ir, {}, world) if ir.get("imports") and bundle.get("program_mode") != "linked" else []
|
|
207
|
+
names = {(document.get("package") or {}).get("name") for document in resolved}
|
|
208
|
+
for document in world:
|
|
209
|
+
name = (document.get("package") or {}).get("name")
|
|
210
|
+
if name not in names:
|
|
211
|
+
resolved.append(document)
|
|
212
|
+
names.add(name)
|
|
213
|
+
plan = P.build_plan(ir, resolved, {"negation"} if negative else None, resources)
|
|
214
|
+
known = D.targets(protocol="runner")
|
|
215
|
+
unknown = sorted(set(targets) - set(known))
|
|
216
|
+
if unknown:
|
|
217
|
+
return {}, ["неизвестные цели: " + ", ".join(unknown)]
|
|
218
|
+
emit = {name: D.emitter(name) for name in targets}
|
|
219
|
+
unavailable = [f"{name}: {emit[name].available()[1]}" for name in targets if not emit[name].available()[0]]
|
|
220
|
+
active = [name for name in targets if emit[name].available()[0]]
|
|
221
|
+
if not active:
|
|
222
|
+
return {}, ["нет доступных целей"]
|
|
223
|
+
projects = {}
|
|
224
|
+
for name in active:
|
|
225
|
+
project = work / "module" / name
|
|
226
|
+
D.emit_project(plan, name, project)
|
|
227
|
+
projects[name] = [project]
|
|
228
|
+
environment = dict(os.environ, GOCACHE=str(work.resolve() / "gocache"),
|
|
229
|
+
GOMODCACHE=str(work.resolve() / "gomodcache"),
|
|
230
|
+
CLANG_MODULE_CACHE_PATH=str(work.resolve() / "clang-cache"),
|
|
231
|
+
SWIFT_MODULECACHE_PATH=str(work.resolve() / "swift-cache"))
|
|
232
|
+
failures = T.build_projects(emit, projects, work / "module", environment)
|
|
233
|
+
summary = {"tests": len(bundle["tests"]), "matched": 0, "unchecked": 0, "refused": {},
|
|
234
|
+
"skipped": {}, "sourceSkipped": bundle["source_skipped"], "unavailable": unavailable}
|
|
235
|
+
if failures:
|
|
236
|
+
return summary, failures
|
|
237
|
+
for test in bundle["tests"]:
|
|
238
|
+
query = json.loads(test["query"].read_text(encoding="utf-8"))
|
|
239
|
+
if query.get("kind") not in ("truth", "positions"):
|
|
240
|
+
summary["skipped"][f"запрос {query.get('kind')}"] = summary["skipped"].get(f"запрос {query.get('kind')}", 0) + 1
|
|
241
|
+
continue
|
|
242
|
+
_fields, skipped, _positions = expected(test)
|
|
243
|
+
for name in skipped:
|
|
244
|
+
summary["skipped"][f"наблюдение {name}"] = summary["skipped"].get(f"наблюдение {name}", 0) + 1
|
|
245
|
+
got = {name: T.run_target(emit[name], work / "module" / name, "runner",
|
|
246
|
+
[str(test["case"]), str(test["query"])], environment) for name in active}
|
|
247
|
+
crashes = {name: value for name, (kind, value) in got.items() if kind == "crash"}
|
|
248
|
+
if crashes:
|
|
249
|
+
failures.append(f"{test['id']}: КРАХ {crashes}")
|
|
250
|
+
continue
|
|
251
|
+
refusals = {name: value for name, (kind, value) in got.items() if kind == "refused"}
|
|
252
|
+
if refusals:
|
|
253
|
+
if len(refusals) != len(got):
|
|
254
|
+
failures.append(f"{test['id']}: отказ не единогласен {refusals}")
|
|
255
|
+
elif len(set(refusals.values())) != 1:
|
|
256
|
+
failures.append(f"{test['id']}: тексты отказа различаются {refusals}")
|
|
257
|
+
else:
|
|
258
|
+
text = next(iter(refusals.values()))
|
|
259
|
+
summary["refused"][text] = summary["refused"].get(text, 0) + 1
|
|
260
|
+
continue
|
|
261
|
+
answers = {name: value for name, (_kind, value) in got.items()}
|
|
262
|
+
if len({T.answer_key(value) for value in answers.values()}) != 1:
|
|
263
|
+
failures.append(f"{test['id']}: ответы целей разошлись")
|
|
264
|
+
continue
|
|
265
|
+
if not checkable(test):
|
|
266
|
+
summary["unchecked"] += 1
|
|
267
|
+
continue
|
|
268
|
+
differences = compare(next(iter(answers.values())), test)
|
|
269
|
+
if differences:
|
|
270
|
+
failures.extend(f"{test['id']}: {line}" for line in differences)
|
|
271
|
+
else:
|
|
272
|
+
summary["matched"] += 1
|
|
273
|
+
return summary, failures
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def main(argv: list[str] | None = None) -> int:
|
|
277
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
278
|
+
parser.add_argument("--bundle", required=True, type=Path)
|
|
279
|
+
parser.add_argument("--core-kit", type=Path, required=True)
|
|
280
|
+
parser.add_argument("--targets", default="ts,py,go")
|
|
281
|
+
parser.add_argument("--keep", type=Path)
|
|
282
|
+
parser.add_argument("--allow-source-skips", action="store_true", help="explicitly accept named source omissions")
|
|
283
|
+
args = parser.parse_args(argv)
|
|
284
|
+
previous_core_kit = core_support.core_kit_path()
|
|
285
|
+
try:
|
|
286
|
+
core_support.validate_core_kit(args.core_kit)
|
|
287
|
+
core_support.configure_core_kit(args.core_kit)
|
|
288
|
+
bundle = load_bundle(args.bundle)
|
|
289
|
+
source_skips = list(bundle["source_skipped"])
|
|
290
|
+
for member in bundle.get("groups", []):
|
|
291
|
+
source_skips.extend(member["source_skipped"])
|
|
292
|
+
if source_skips and not args.allow_source_skips:
|
|
293
|
+
raise ValueError("source omissions require --allow-source-skips: " + "; ".join(source_skips))
|
|
294
|
+
work = args.keep or Path(tempfile.mkdtemp(prefix="arxo-codegen-consumer-"))
|
|
295
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
summary, failures = run(bundle, [name for name in args.targets.split(",") if name], work)
|
|
297
|
+
except (ValueError, P.Refused, core_support.CoreKitError) as error:
|
|
298
|
+
print(f"arxo-codegen-consumer: {error}")
|
|
299
|
+
return 1
|
|
300
|
+
finally:
|
|
301
|
+
if not args.keep and 'work' in locals():
|
|
302
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
303
|
+
# CLI invocations may share an interpreter with other verification
|
|
304
|
+
# modules. Selecting a temporary kit must not poison the next call.
|
|
305
|
+
core_support.configure_core_kit(previous_core_kit)
|
|
306
|
+
print("arxo-codegen-consumer: " + json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
|
307
|
+
for failure in failures:
|
|
308
|
+
print(" " + failure)
|
|
309
|
+
if not failures and summary["matched"] == 0:
|
|
310
|
+
print(" ничего не проверено: нет совпавших проверяемых ожиданий")
|
|
311
|
+
return 1
|
|
312
|
+
return 1 if failures else 0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
raise SystemExit(main())
|