Skip to content

Commit 2c1adf1

Browse files
committed
Add conformance results invariant validator
1 parent 56b7944 commit 2c1adf1

File tree

5 files changed

+101
-0
lines changed

5 files changed

+101
-0
lines changed

.github/workflows/conformance.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ jobs:
3232
uv sync --python 3.12 --frozen
3333
uv run --python 3.12 --frozen python src/main.py
3434
35+
- name: Validate conformance invariants
36+
working-directory: conformance
37+
run: |
38+
uv run --python 3.12 --frozen python src/validate_results.py
39+
3540
- name: Assert conformance results are up to date
3641
run: |
3742
if [ -n "$(git status --porcelain -- conformance/results)" ]; then

conformance/results/mypy/generics_defaults.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
conformant = "Partial"
2+
notes = """
3+
Does not detect a TypeVar with a default used after a TypeVarTuple.
4+
Does not fully support defaults on TypeVarTuple and ParamSpec.
5+
"""
26
output = """
37
generics_defaults.py:24: error: "T" cannot appear after "DefaultStrT" in type parameter list because it has no default type [misc]
48
generics_defaults.py:66: error: "AllTheDefaults" expects between 2 and 5 type arguments, but 1 given [type-arg]

conformance/results/mypy/generics_defaults_referential.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
conformant = "Partial"
2+
notes = """
3+
Does not correctly handle defaults referencing other TypeVars.
4+
"""
25
output = """
36
generics_defaults_referential.py:23: error: Expression is of type "type[slice[StartT, StopT, StepT]]", not "type[slice[int, int, int | None]]" [assert-type]
47
generics_defaults_referential.py:37: error: Argument 1 to "Foo" has incompatible type "str"; expected "int" [arg-type]

conformance/results/mypy/generics_defaults_specialization.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
conformant = "Partial"
2+
notes = """
3+
Does not correctly resolve defaults when classes are used directly.
4+
"""
25
output = """
36
generics_defaults_specialization.py:30: error: Bad number of arguments for type alias, expected between 0 and 1, given 2 [type-arg]
47
generics_defaults_specialization.py:45: error: Expression is of type "type[Bar[DefaultStrT]]", not "type[Bar[str]]" [assert-type]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""
2+
Validate invariants for conformance result files.
3+
"""
4+
5+
from pathlib import Path
6+
import sys
7+
import tomllib
8+
from typing import Any
9+
10+
11+
def main() -> int:
12+
results_dir = Path(__file__).resolve().parent.parent / "results"
13+
issues: list[str] = []
14+
checked = 0
15+
16+
for type_checker_dir in sorted(results_dir.iterdir()):
17+
if not type_checker_dir.is_dir():
18+
continue
19+
for file in sorted(type_checker_dir.iterdir()):
20+
if file.name == "version.toml":
21+
continue
22+
checked += 1
23+
try:
24+
with file.open("rb") as f:
25+
info = tomllib.load(f)
26+
except Exception as e:
27+
issues.append(f"{file.relative_to(results_dir)}: failed to parse TOML ({e})")
28+
continue
29+
30+
issues.extend(_validate_result(file, results_dir, info))
31+
32+
if issues:
33+
print(f"Found {len(issues)} invariant violation(s) across {checked} file(s):")
34+
for issue in issues:
35+
print(f"- {issue}")
36+
return 1
37+
38+
print(f"Validated {checked} conformance result file(s); no invariant violations found.")
39+
return 0
40+
41+
42+
def _validate_result(file: Path, results_dir: Path, info: dict[str, Any]) -> list[str]:
43+
issues: list[str] = []
44+
rel_path = file.relative_to(results_dir)
45+
46+
automated = info.get("conformance_automated")
47+
if automated not in {"Pass", "Fail"}:
48+
issues.append(
49+
f"{rel_path}: conformance_automated must be 'Pass' or 'Fail' (got {automated!r})"
50+
)
51+
return issues
52+
automated_is_pass = automated == "Pass"
53+
54+
conformant = info.get("conformant")
55+
if conformant is None:
56+
if automated_is_pass:
57+
conformant_is_pass = True
58+
else:
59+
issues.append(
60+
f"{rel_path}: conformant is required when conformance_automated is 'Fail'"
61+
)
62+
return issues
63+
elif isinstance(conformant, str):
64+
conformant_is_pass = conformant == "Pass"
65+
else:
66+
issues.append(f"{rel_path}: conformant must be a string when present")
67+
return issues
68+
69+
if conformant_is_pass != automated_is_pass:
70+
issues.append(
71+
f"{rel_path}: conformant={conformant!r} does not match "
72+
f"conformance_automated={automated!r}"
73+
)
74+
75+
if not conformant_is_pass:
76+
notes = info.get("notes", "")
77+
if not isinstance(notes, str) or not notes.strip():
78+
issues.append(
79+
f"{rel_path}: notes must be present when checker is not fully conformant"
80+
)
81+
82+
return issues
83+
84+
85+
if __name__ == "__main__":
86+
raise SystemExit(main())

0 commit comments

Comments
 (0)