mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 16:48:54 +01:00
Add design conformance counter, score TODO
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWXTUM9dpnCS2wtFkx4
This commit is contained in:
parent
5be9a22970
commit
254c8ce240
@ -11,8 +11,8 @@ overwrite it with `repo_contract/templates/check_repo_contract.shim.sh`.
|
||||
## Commands
|
||||
|
||||
- run: n/a: collection of gates and libraries, no single entry point
|
||||
- test: `PYTHONPATH=. python -m pytest -q agent_ready/tests coverage_gaps/tests dep_freshness/tests file_length/tests flutter_web_drive/tests home_paths/tests md_naming/tests repo_contract/tests scripts/tests`
|
||||
- test: `PYTHONPATH=. python -m pytest -q agent_ready/tests coverage_gaps/tests dep_freshness/tests file_length/tests flutter_web_drive/tests home_paths/tests md_naming/tests repo_contract/tests scripts/tests unified-design-system/tests`
|
||||
- test-changed: `scripts/test_changed.sh`
|
||||
- lint: `ruff check agent_ready coverage_gaps flutter_web_drive repo_contract`
|
||||
- coverage: `PYTHONPATH=. python -m pytest -q agent_ready/tests flutter_web_drive/tests repo_contract/tests --cov=agent_ready --cov=flutter_web_drive --cov=repo_contract --cov-branch --cov-report=xml --cov-fail-under=100`
|
||||
- coverage: `PYTHONPATH=. python -m pytest -q agent_ready/tests flutter_web_drive/tests repo_contract/tests unified-design-system/tests --cov=agent_ready --cov=flutter_web_drive --cov=repo_contract --cov=conformance --cov=conformance_diff --cov-branch --cov-report=xml --cov-fail-under=100`
|
||||
- coverage-gaps: `coverage-gaps coverage.xml`
|
||||
|
||||
31
music_theory/TODO-multivoice-score.md
Normal file
31
music_theory/TODO-multivoice-score.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Add a plain-text multi-voice score format
|
||||
|
||||
## what
|
||||
Add a plain-text score format that `compose.py` and `sequencer.py` can
|
||||
render: named voices, a noise-drum voice, a `meter` field (3/4, 6/8) and
|
||||
A/B sections. Today `sequencer.BEATS_PER_BAR` is fixed at 4 and a `Recipe`
|
||||
has one bass, one arpeggio and one melody.
|
||||
Source: simonwillison.net/2026/Oct/6/scrimshaw-jukebox.
|
||||
|
||||
## done
|
||||
```
|
||||
cd ~/src/utils/music_theory && PYTHONPATH=. python -m pytest music_theory/tests/test_score.py music_theory/tests/test_sequencer.py music_theory/tests/test_compose.py --cov=music_theory.score --cov=music_theory.sequencer --cov=music_theory.compose --cov-branch --cov-fail-under=100
|
||||
```
|
||||
exits 0. It covers: a 3/4 and a 6/8 score render bar-exact loops, two named
|
||||
voices stack, the noise-drum voice is deterministic for a fixed seed, and an
|
||||
A/B-section score renders A then B in order.
|
||||
|
||||
## verify
|
||||
Desktop. Render one 3/4 and one 6/8 example score with
|
||||
`PYTHONPATH=. python -m music_theory render --score examples/waltz.txt --out /tmp/waltz.wav`,
|
||||
check `analysis.dominant_frequency` and the frame count equal
|
||||
`sequencer.loop_frames` for the bar count, and re-render for byte-identical
|
||||
output. The final listen is kuhy's.
|
||||
|
||||
## read first
|
||||
- `music_theory/compose.py` (`Recipe`), `music_theory/sequencer.py`
|
||||
(`NoteEvent`, `BEATS_PER_BAR`), `music_theory/noise.py` (drum source).
|
||||
- `DOCS-article-walkthrough.md` (article section 10, notation as format).
|
||||
- `~/.claude/skills/procedural-music/SKILL.md`.
|
||||
|
||||
REMOVE ME AFTER FINISH
|
||||
206
unified-design-system/scripts/conformance.py
Normal file
206
unified-design-system/scripts/conformance.py
Normal file
@ -0,0 +1,206 @@
|
||||
"""Design-token conformance per UI surface: tokens used vs raw literals.
|
||||
|
||||
Install counts and component counts are vanity metrics. What shows whether a
|
||||
design system is working is how often a surface reaches for a token versus a
|
||||
hard-coded value (learn.thedesignsystem.guide, "Half my design system metrics
|
||||
don't work anymore"). This script counts, per UI surface of one repo:
|
||||
|
||||
* token usages: ``var(--x)`` in CSS/TS/TSX/Astro, ``AppPalette.x`` style
|
||||
constants and ``Theme.of`` / ``colorScheme`` / ``textTheme`` in Dart
|
||||
* raw literals: hex colours (``#rrggbb``, ``0xAARRGGBB``) and non-zero
|
||||
``px`` / ``pt`` numbers, outside the token files
|
||||
|
||||
A surface is the nearest ancestor directory holding a manifest
|
||||
(``pubspec.yaml``, ``package.json``, ``astro.config.*``), else the repo root.
|
||||
Token files (tokens/theme/palette/colors), custom-property definitions,
|
||||
comments, tests, ``@media`` lines and generated/build dirs are skipped.
|
||||
The counts are a heuristic trend signal, not an audit.
|
||||
|
||||
Run: python3 conformance.py <repo> [--json] [--previous <earlier.json>]
|
||||
Exit 0 = report printed; 2 = bad arguments or unreadable previous file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from conformance_diff import delta, load_previous, table
|
||||
|
||||
Json = dict[str, Any]
|
||||
|
||||
EXTENSIONS = frozenset({".dart", ".css", ".ts", ".tsx", ".astro"})
|
||||
SKIP_DIRS = frozenset({
|
||||
".git", "node_modules", "build", "dist", ".dart_tool", ".astro", ".next",
|
||||
"coverage", "generated", "__pycache__", ".venv", "test", "tests",
|
||||
"__tests__", ".fvm", ".pub-cache",
|
||||
})
|
||||
SKIP_NAME = re.compile(
|
||||
r"(\.g\.dart|\.freezed\.dart|\.d\.ts|\.test\.[jt]sx?|_test\.dart)$"
|
||||
)
|
||||
TOKEN_FILE = re.compile(r"^_?(tokens?|theme|palette|colou?rs?)\b", re.IGNORECASE)
|
||||
MANIFESTS = ("pubspec.yaml", "package.json")
|
||||
MAX_BYTES = 1_000_000
|
||||
|
||||
BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
||||
LINE_COMMENT = re.compile(r"(?<![:\w])//.*")
|
||||
TOKEN_DEFINITION = re.compile(r"--[\w-]+\s*:[^;}]*;?")
|
||||
MEDIA_LINE = re.compile(r"@(media|container)\b")
|
||||
HEX = re.compile(
|
||||
r"#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b|\b0x[0-9A-Fa-f]{8}\b"
|
||||
)
|
||||
UNIT = re.compile(r"(?<![\w.#-])(?!0+(?:\.0+)?(?:px|pt)\b)\d+(?:\.\d+)?(?:px|pt)\b")
|
||||
TOKEN_USE = re.compile(
|
||||
r"var\(--|\bApp(?:Palette|Spacing|Radius|TextSize|Duration|Curve)\.\w+"
|
||||
r"|\bTheme\.of\(|\bcolorScheme\.|\btextTheme\."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Surface:
|
||||
"""Counts for one UI surface."""
|
||||
|
||||
name: str
|
||||
files: int = 0
|
||||
token_usages: int = 0
|
||||
raw_literals: int = 0
|
||||
|
||||
@property
|
||||
def conformance(self) -> float | None:
|
||||
"""Percent of style values that are tokens, or None with no values."""
|
||||
total = self.token_usages + self.raw_literals
|
||||
if total == 0:
|
||||
return None
|
||||
return round(100.0 * self.token_usages / total, 1)
|
||||
|
||||
def as_dict(self) -> Json:
|
||||
"""JSON-ready form."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"files": self.files,
|
||||
"token_usages": self.token_usages,
|
||||
"raw_literals": self.raw_literals,
|
||||
"conformance": self.conformance,
|
||||
}
|
||||
|
||||
|
||||
def strip_comments(text: str, suffix: str) -> str:
|
||||
"""Remove comments so a hex in prose is not counted as a literal."""
|
||||
text = BLOCK_COMMENT.sub("", text)
|
||||
if suffix == ".css":
|
||||
return text
|
||||
return LINE_COMMENT.sub("", text)
|
||||
|
||||
|
||||
def count_text(text: str, suffix: str) -> tuple[int, int]:
|
||||
"""Return (token usages, raw literals) for one file's text."""
|
||||
tokens = 0
|
||||
literals = 0
|
||||
for line in strip_comments(text, suffix).splitlines():
|
||||
tokens += len(TOKEN_USE.findall(line))
|
||||
if MEDIA_LINE.search(line):
|
||||
continue
|
||||
line = TOKEN_DEFINITION.sub("", line)
|
||||
literals += len(HEX.findall(line)) + len(UNIT.findall(line))
|
||||
return tokens, literals
|
||||
|
||||
|
||||
def _has_manifest(directory: Path) -> bool:
|
||||
"""True when ``directory`` holds a package/app manifest."""
|
||||
return any((directory / m).exists() for m in MANIFESTS) or any(
|
||||
directory.glob("astro.config.*")
|
||||
)
|
||||
|
||||
|
||||
def surface_root(path: Path, repo: Path) -> Path:
|
||||
"""Nearest ancestor of ``path`` (within ``repo``) holding a manifest."""
|
||||
parent = path.parent
|
||||
while parent != repo and not _has_manifest(parent):
|
||||
parent = parent.parent
|
||||
return parent
|
||||
|
||||
|
||||
def ui_files(repo: Path) -> list[Path]:
|
||||
"""UI source files, skipping build/generated dirs and token files."""
|
||||
found: list[Path] = []
|
||||
for root, dirs, names in os.walk(repo):
|
||||
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
|
||||
for name in sorted(names):
|
||||
path = Path(root) / name
|
||||
if path.suffix not in EXTENSIONS or SKIP_NAME.search(name):
|
||||
continue
|
||||
if TOKEN_FILE.match(path.stem):
|
||||
continue
|
||||
if path.is_file() and path.stat().st_size <= MAX_BYTES:
|
||||
found.append(path)
|
||||
return found
|
||||
|
||||
|
||||
def scan(repo: Path) -> list[Surface]:
|
||||
"""Count every UI surface under ``repo``, sorted by name."""
|
||||
surfaces: dict[Path, Surface] = {}
|
||||
for path in ui_files(repo):
|
||||
root = surface_root(path, repo)
|
||||
name = str(root.relative_to(repo))
|
||||
surface = surfaces.setdefault(root, Surface(name))
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
tokens, literals = count_text(text, path.suffix)
|
||||
surface.files += 1
|
||||
surface.token_usages += tokens
|
||||
surface.raw_literals += literals
|
||||
return sorted(surfaces.values(), key=lambda s: s.name)
|
||||
|
||||
|
||||
def totals(surfaces: list[Surface]) -> Surface:
|
||||
"""Sum of all surfaces."""
|
||||
return Surface(
|
||||
"TOTAL",
|
||||
sum(s.files for s in surfaces),
|
||||
sum(s.token_usages for s in surfaces),
|
||||
sum(s.raw_literals for s in surfaces),
|
||||
)
|
||||
|
||||
|
||||
def report(repo: Path) -> Json:
|
||||
"""Whole-repo report as a JSON-ready dict."""
|
||||
surfaces = scan(repo)
|
||||
return {
|
||||
"repo": str(repo),
|
||||
"surfaces": [s.as_dict() for s in surfaces],
|
||||
"totals": totals(surfaces).as_dict(),
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""CLI entry point."""
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
||||
parser.add_argument("repo", type=Path)
|
||||
parser.add_argument("--json", action="store_true", help="print JSON")
|
||||
parser.add_argument("--previous", type=Path, help="earlier --json output")
|
||||
args = parser.parse_args(argv)
|
||||
if not args.repo.is_dir():
|
||||
print(f"not a directory: {args.repo}", file=sys.stderr)
|
||||
return 2
|
||||
rep = report(args.repo.resolve())
|
||||
diff = None
|
||||
if args.previous:
|
||||
try:
|
||||
diff = delta(rep, load_previous(args.previous))
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"cannot read previous report: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
if args.json:
|
||||
print(json.dumps({**rep, **({"delta": diff} if diff else {})}, indent=2))
|
||||
else:
|
||||
print(table(rep, diff))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
74
unified-design-system/scripts/conformance_diff.py
Normal file
74
unified-design-system/scripts/conformance_diff.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""Delta and table rendering for conformance reports (see conformance.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
Json = dict[str, Any]
|
||||
|
||||
|
||||
def _num(value: object) -> float:
|
||||
"""Numeric value from loosely typed JSON, 0 when absent."""
|
||||
return float(value) if isinstance(value, (int, float)) else 0.0
|
||||
|
||||
|
||||
def load_previous(path: Path) -> Json:
|
||||
"""Read an earlier ``--json`` report; ValueError if its shape is wrong."""
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
surfaces = data.get("surfaces") if isinstance(data, dict) else None
|
||||
ok = isinstance(surfaces, list) and isinstance(data.get("totals"), dict)
|
||||
if not ok or not all(isinstance(s, dict) and "name" in s for s in surfaces):
|
||||
msg = "not a conformance report"
|
||||
raise ValueError(msg)
|
||||
return data
|
||||
|
||||
|
||||
def delta(current: Json, previous: Json) -> Json:
|
||||
"""Per-surface and total change versus a previous report."""
|
||||
before = {str(s["name"]): s for s in previous["surfaces"]}
|
||||
rows: list[Json] = []
|
||||
entries = [*current["surfaces"], current["totals"]]
|
||||
prev_total = previous["totals"]
|
||||
for row in entries:
|
||||
old = prev_total if row["name"] == "TOTAL" else before.get(str(row["name"]), {})
|
||||
rows.append({
|
||||
"name": row["name"],
|
||||
"token_usages": _num(row["token_usages"]) - _num(old.get("token_usages")),
|
||||
"raw_literals": _num(row["raw_literals"]) - _num(old.get("raw_literals")),
|
||||
"conformance": None
|
||||
if row["conformance"] is None or old.get("conformance") is None
|
||||
else round(_num(row["conformance"]) - _num(old["conformance"]), 1),
|
||||
})
|
||||
return {"rows": rows}
|
||||
|
||||
|
||||
def _fmt(value: object, signed: bool = False) -> str:
|
||||
"""Table cell text for a count, percent or missing value."""
|
||||
if value is None:
|
||||
return "-"
|
||||
return f"{_num(value):+g}" if signed else f"{_num(value):g}"
|
||||
|
||||
|
||||
def table(rep: Json, diff: Json | None) -> str:
|
||||
"""Plain-text table, with delta columns when ``diff`` is given."""
|
||||
head = ["surface", "files", "tokens", "raw", "conf%"]
|
||||
if diff:
|
||||
head += ["d.tokens", "d.raw", "d.conf"]
|
||||
rows = [head]
|
||||
deltas = {str(r["name"]): r for r in diff["rows"]} if diff else {}
|
||||
for row in [*rep["surfaces"], rep["totals"]]:
|
||||
cells = [str(row["name"]), _fmt(row["files"]), _fmt(row["token_usages"]),
|
||||
_fmt(row["raw_literals"]), _fmt(row["conformance"])]
|
||||
if diff:
|
||||
d = deltas.get(str(row["name"]), {})
|
||||
cells += [_fmt(d.get("token_usages", 0), True),
|
||||
_fmt(d.get("raw_literals", 0), True),
|
||||
_fmt(d.get("conformance"), True)]
|
||||
rows.append(cells)
|
||||
widths = [max(len(r[i]) for r in rows) for i in range(len(head))]
|
||||
return "\n".join(
|
||||
" ".join(c.ljust(w) for c, w in zip(r, widths, strict=True)).rstrip()
|
||||
for r in rows
|
||||
)
|
||||
6
unified-design-system/tests/conftest.py
Normal file
6
unified-design-system/tests/conftest.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Make ``scripts/`` importable: the folder name has a hyphen, so no package."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
118
unified-design-system/tests/test_conformance.py
Normal file
118
unified-design-system/tests/test_conformance.py
Normal file
@ -0,0 +1,118 @@
|
||||
"""Tests for scripts/conformance.py (100 % branch coverage)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import runpy
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import conformance
|
||||
import pytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def write(root: Path, rel: str, text: str) -> None:
|
||||
path = root / rel
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def test_count_text_css_tokens_literals_and_skips() -> None:
|
||||
css = (
|
||||
"/* #123456 in a comment */\n"
|
||||
":root { --brand: #b8862e; }\n"
|
||||
"@media (min-width: 600px) { a { color: red; } }\n"
|
||||
".a { color: var(--brand); margin: 12px 0px; border: 1.5pt solid #fff; }\n"
|
||||
)
|
||||
assert conformance.count_text(css, ".css") == (1, 3)
|
||||
|
||||
|
||||
def test_count_text_line_comments_and_urls_in_ts() -> None:
|
||||
ts = 'const u = "http://x.test"; // #abcdef\nconst c = "#a1b2c3";\n'
|
||||
assert conformance.count_text(ts, ".ts") == (0, 1)
|
||||
|
||||
|
||||
def test_count_text_dart_tokens_and_literals() -> None:
|
||||
dart = (
|
||||
"final c = Color(0xFF112233);\n"
|
||||
"final d = AppPalette.accent;\n"
|
||||
"final e = Theme.of(context).colorScheme.primary;\n"
|
||||
)
|
||||
assert conformance.count_text(dart, ".dart") == (3, 1)
|
||||
|
||||
|
||||
def test_conformance_percent_and_none() -> None:
|
||||
assert conformance.Surface("x").conformance is None
|
||||
assert conformance.Surface("x", 1, 3, 1).conformance == 75.0
|
||||
|
||||
|
||||
def test_scan_groups_by_manifest_and_skips_generated(tmp_path: Path) -> None:
|
||||
write(tmp_path, "pubspec.yaml", "name: a")
|
||||
write(tmp_path, "lib/ui.dart", "final c = AppPalette.bg; final d = '#fff';")
|
||||
write(tmp_path, "lib/tokens.dart", "const c = Color(0xFF000000);")
|
||||
write(tmp_path, "lib/a.g.dart", "const c = '#ffffff';")
|
||||
write(tmp_path, "build/out.css", "a { color: #fff }")
|
||||
write(tmp_path, "test/x_test.dart", "const c = '#fff';")
|
||||
write(tmp_path, "web/package.json", "{}")
|
||||
write(tmp_path, "web/s.css", "a { margin: 8px; color: var(--x); }")
|
||||
write(tmp_path, "site/astro.config.mjs", "export default {}")
|
||||
write(tmp_path, "site/p.astro", "<p style='top: 4pt'>hi</p>")
|
||||
write(tmp_path, "loose/readme.txt", "#fff 3px")
|
||||
write(tmp_path, "root.tsx", "const a = 'var(--r)';")
|
||||
write(tmp_path, "big.css", "a{}" + " " * conformance.MAX_BYTES)
|
||||
surfaces = {s.name: s for s in conformance.scan(tmp_path)}
|
||||
assert sorted(surfaces) == [".", "site", "web"]
|
||||
assert (surfaces["."].token_usages, surfaces["."].raw_literals) == (2, 1)
|
||||
assert surfaces["."].files == 2
|
||||
assert (surfaces["web"].token_usages, surfaces["web"].raw_literals) == (1, 1)
|
||||
assert (surfaces["site"].token_usages, surfaces["site"].raw_literals) == (0, 1)
|
||||
|
||||
|
||||
def test_scan_ignores_non_regular_file(tmp_path: Path) -> None:
|
||||
(tmp_path / "dangling.css").symlink_to(tmp_path / "missing.css")
|
||||
assert conformance.scan(tmp_path) == []
|
||||
|
||||
|
||||
def test_totals_and_report(tmp_path: Path) -> None:
|
||||
write(tmp_path, "a.css", "a { margin: 8px; }")
|
||||
write(tmp_path, "web/package.json", "{}")
|
||||
write(tmp_path, "web/b.css", "b { color: var(--x); }")
|
||||
rep = conformance.report(tmp_path)
|
||||
assert rep["totals"]["token_usages"] == 1
|
||||
assert rep["totals"]["raw_literals"] == 1
|
||||
assert rep["totals"]["conformance"] == 50.0
|
||||
assert [s["name"] for s in rep["surfaces"]] == [".", "web"]
|
||||
|
||||
|
||||
def test_main_table_json_and_delta(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
write(repo, "a.css", "a { margin: 8px; color: var(--x); }")
|
||||
assert conformance.main([str(repo)]) == 0
|
||||
assert "TOTAL" in capsys.readouterr().out
|
||||
assert conformance.main([str(repo), "--json"]) == 0
|
||||
first = capsys.readouterr().out
|
||||
assert "delta" not in json.loads(first)
|
||||
prev = tmp_path / "prev.json"
|
||||
prev.write_text(first, encoding="utf-8")
|
||||
write(repo, "b.css", "b { color: var(--y); }")
|
||||
assert conformance.main([str(repo), "--json", "--previous", str(prev)]) == 0
|
||||
assert json.loads(capsys.readouterr().out)["delta"]["rows"][-1]["token_usages"] == 1.0
|
||||
assert conformance.main([str(repo), "--previous", str(prev)]) == 0
|
||||
assert "d.tokens" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_main_errors(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert conformance.main([str(tmp_path / "nope")]) == 2
|
||||
assert "not a directory" in capsys.readouterr().err
|
||||
assert conformance.main([str(tmp_path), "--previous", str(tmp_path / "none.json")]) == 2
|
||||
assert "cannot read previous" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_script_entry_point(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(sys, "argv", ["conformance.py", str(tmp_path)])
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
runpy.run_path(conformance.__file__, run_name="__main__")
|
||||
assert exit_info.value.code == 0
|
||||
71
unified-design-system/tests/test_conformance_diff.py
Normal file
71
unified-design-system/tests/test_conformance_diff.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""Tests for scripts/conformance_diff.py (100 % branch coverage)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import conformance_diff
|
||||
import pytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def previous_report(conf: float | None = 50.0) -> dict:
|
||||
return {
|
||||
"surfaces": [{"name": ".", "token_usages": 1, "raw_literals": 1, "conformance": conf}],
|
||||
"totals": {"name": "TOTAL", "token_usages": 1, "raw_literals": 1, "conformance": conf},
|
||||
}
|
||||
|
||||
|
||||
def current_report() -> dict:
|
||||
return {
|
||||
"surfaces": [
|
||||
{"name": ".", "files": 1, "token_usages": 3, "raw_literals": 1, "conformance": 75.0},
|
||||
{"name": "new", "files": 1, "token_usages": 2, "raw_literals": 0, "conformance": None},
|
||||
],
|
||||
"totals": {"name": "TOTAL", "files": 2, "token_usages": 5, "raw_literals": 1, "conformance": 83.3},
|
||||
}
|
||||
|
||||
|
||||
def test_delta_rows() -> None:
|
||||
rows = {r["name"]: r for r in conformance_diff.delta(current_report(), previous_report())["rows"]}
|
||||
assert rows["."] == {"name": ".", "token_usages": 2.0, "raw_literals": 0.0, "conformance": 25.0}
|
||||
assert rows["new"]["token_usages"] == 2.0
|
||||
assert rows["new"]["conformance"] is None
|
||||
assert rows["TOTAL"]["conformance"] == 33.3
|
||||
|
||||
|
||||
def test_delta_missing_previous_conformance() -> None:
|
||||
rows = conformance_diff.delta(current_report(), previous_report(None))["rows"]
|
||||
assert rows[0]["conformance"] is None
|
||||
|
||||
|
||||
def test_num_and_fmt() -> None:
|
||||
assert conformance_diff._num("x") == 0.0
|
||||
assert conformance_diff._num(2) == 2.0
|
||||
assert conformance_diff._fmt(None) == "-"
|
||||
assert conformance_diff._fmt(2.5) == "2.5"
|
||||
assert conformance_diff._fmt(2, signed=True) == "+2"
|
||||
|
||||
|
||||
def test_table_without_and_with_delta() -> None:
|
||||
plain = conformance_diff.table(current_report(), None)
|
||||
assert plain.splitlines()[0].split() == ["surface", "files", "tokens", "raw", "conf%"]
|
||||
assert "TOTAL" in plain
|
||||
diff = conformance_diff.delta(current_report(), previous_report())
|
||||
text = conformance_diff.table(current_report(), diff)
|
||||
assert "d.conf" in text
|
||||
assert "+25" in text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
["[]", "{}", '{"surfaces": 1, "totals": {}}', '{"surfaces": [1], "totals": {}}',
|
||||
'{"surfaces": [], "totals": 1}'],
|
||||
)
|
||||
def test_load_previous_rejects_bad_shape(tmp_path: Path, payload: str) -> None:
|
||||
path = tmp_path / "p.json"
|
||||
path.write_text(payload, encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="not a conformance report"):
|
||||
conformance_diff.load_previous(path)
|
||||
Loading…
Reference in New Issue
Block a user