mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:27:16 +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
|
## Commands
|
||||||
|
|
||||||
- run: n/a: collection of gates and libraries, no single entry point
|
- 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`
|
- test-changed: `scripts/test_changed.sh`
|
||||||
- lint: `ruff check agent_ready coverage_gaps flutter_web_drive repo_contract`
|
- 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`
|
- 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