Commit 4adb893704

4adb893704fe2f443ff48d6983da69e4295eaace

parent: 3b24b70d69

Unsigned

cmc <hello@cleberg.net> · 2026-08-09 01:32 UTC

Pin CI actions to SHA, simplify main(), tidy exceptions and tests

Layout: unified · split

.github/workflows/release.yml +2 −2
@@ -11,7 +11,7 @@ jobs:
11 steps: 11 steps:
12 - uses: actions/checkout@v5 12 - uses: actions/checkout@v5
13 - name: Install uv 13 - name: Install uv
14 uses: astral-sh/setup-uv@v6 14 uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
15 - name: Build 15 - name: Build
16 run: uv build 16 run: uv build
17 - name: Check 17 - name: Check
@@ -33,4 +33,4 @@ jobs:
33 name: dist 33 name: dist
34 path: dist/ 34 path: dist/
35 - name: Publish to PyPI 35 - name: Publish to PyPI
36 uses: pypa/gh-action-pypi-publish@release/v1 36 uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
control_coverage/cli.py +8 −4
@@ -117,11 +117,11 @@ def _now() -> str:
117 return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") 117 return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
118 118
119 119
120def _build_report(paths, args, scp, names, subject): 120def _build_report(paths, scp, names, subject):
121 """Load a corpus from *paths* and evaluate it into a CoverageReport.""" 121 """Load a corpus from *paths* and evaluate it into a CoverageReport."""
122 try: 122 try:
123 observations = corpus.load_corpus(paths) 123 observations = corpus.load_corpus(paths)
124 except (ValueError, FileNotFoundError, OSError) as exc: 124 except (ValueError, OSError) as exc:
125 raise SystemExit(f"error: {exc}") from None 125 raise SystemExit(f"error: {exc}") from None
126 catalogs = catalog.load_frameworks(names) 126 catalogs = catalog.load_frameworks(names)
127 return evaluate(catalogs, observations, scope=scp, subject=subject, generated_at=_now()) 127 return evaluate(catalogs, observations, scope=scp, subject=subject, generated_at=_now())
@@ -135,7 +135,7 @@ def main(argv: list[str] | None = None) -> int:
135 135
136 try: 136 try:
137 observations = corpus.load_corpus(args.reports) 137 observations = corpus.load_corpus(args.reports)
138 except (ValueError, FileNotFoundError, OSError) as exc: 138 except (ValueError, OSError) as exc:
139 raise SystemExit(f"error: {exc}") from None 139 raise SystemExit(f"error: {exc}") from None
140 140
141 scp = scope.load(args.scope) if args.scope else scope.empty() 141 scp = scope.load(args.scope) if args.scope else scope.empty()
@@ -159,6 +159,10 @@ def main(argv: list[str] | None = None) -> int:
159 _print_blind_spots(report) 159 _print_blind_spots(report)
160 return _exit_code(report, args.fail_under) 160 return _exit_code(report, args.fail_under)
161 161
162 return _default_mode(args, report, subject)
163
164
165def _default_mode(args, report, subject) -> int:
162 formats = [f.strip() for f in args.format.split(",") if f.strip()] 166 formats = [f.strip() for f in args.format.split(",") if f.strip()]
163 if args.out: 167 if args.out:
164 out_dir = Path(args.out) 168 out_dir = Path(args.out)
@@ -180,7 +184,7 @@ def main(argv: list[str] | None = None) -> int:
180def _trend_mode(args, scp, names, subject, current) -> int: 184def _trend_mode(args, scp, names, subject, current) -> int:
181 from . import trend 185 from . import trend
182 186
183 baseline = _build_report([args.baseline], args, scp, names, subject) 187 baseline = _build_report([args.baseline], scp, names, subject)
184 tr = trend.compare(baseline, current) 188 tr = trend.compare(baseline, current)
185 189
186 formats = [f.strip() for f in args.format.split(",") if f.strip()] 190 formats = [f.strip() for f in args.format.split(",") if f.strip()]
control_coverage/coverage.py +1 −1
@@ -74,7 +74,7 @@ class FrameworkCoverage:
74 74
75 @property 75 @property
76 def counts(self) -> dict[str, int]: 76 def counts(self) -> dict[str, int]:
77 counts = {s: 0 for s in STATE_ORDER} 77 counts = dict.fromkeys(STATE_ORDER, 0)
78 for r in self.results: 78 for r in self.results:
79 counts[r.state] += 1 79 counts[r.state] += 1
80 return counts 80 return counts
control_coverage/trend.py +1 −1
@@ -81,7 +81,7 @@ class FrameworkTrend:
81 81
82 @property 82 @property
83 def counts(self) -> dict[str, int]: 83 def counts(self) -> dict[str, int]:
84 counts = {c: 0 for c in CATEGORY_ORDER} 84 counts = dict.fromkeys(CATEGORY_ORDER, 0)
85 for d in self.deltas: 85 for d in self.deltas:
86 counts[d.category] += 1 86 counts[d.category] += 1
87 return counts 87 return counts
tests/test_cli.py +2 −1
@@ -85,7 +85,8 @@ def test_trend_html_output(tmp_path):
85 cli.main([GITHUB, AWS, "--framework", "SOC2", "--baseline", BASELINE, 85 cli.main([GITHUB, AWS, "--framework", "SOC2", "--baseline", BASELINE,
86 "--format", "html,json", "--out", str(tmp_path)]) 86 "--format", "html,json", "--out", str(tmp_path)])
87 names = {p.name for p in tmp_path.iterdir()} 87 names = {p.name for p in tmp_path.iterdir()}
88 assert "trend.html" in names and "trend.json" in names 88 assert "trend.html" in names
89 assert "trend.json" in names
89 90
90 91
91def test_crosswalk_mode(capsys): 92def test_crosswalk_mode(capsys):
tests/test_crosswalk.py +2 −1
@@ -63,5 +63,6 @@ def test_html_is_self_contained():
63 html = crosswalk.render_html(_crosswalk(["SOC2", "ISO", "NIST"])) 63 html = crosswalk.render_html(_crosswalk(["SOC2", "ISO", "NIST"]))
64 assert html.startswith("<!doctype html>") 64 assert html.startswith("<!doctype html>")
65 assert "<style>" in html 65 assert "<style>" in html
66 assert "http://" not in html and "https://" not in html 66 assert "http://" not in html
67 assert "https://" not in html
67 assert "github.org.require-2fa" in html 68 assert "github.org.require-2fa" in html
tests/test_reporters.py +4 −2
@@ -47,7 +47,8 @@ def test_html_is_self_contained():
47 html = reporters.render(_report(), "html") 47 html = reporters.render(_report(), "html")
48 assert html.startswith("<!doctype html>") 48 assert html.startswith("<!doctype html>")
49 assert "<style>" in html 49 assert "<style>" in html
50 assert "http://" not in html and "https://" not in html # no external assets 50 assert "http://" not in html # no external assets
51 assert "https://" not in html
51 52
52 53
53def test_soa_lists_applicability_and_status(): 54def test_soa_lists_applicability_and_status():
@@ -60,5 +61,6 @@ def test_soa_lists_applicability_and_status():
60def test_unknown_format_raises(): 61def test_unknown_format_raises():
61 import pytest 62 import pytest
62 63
64 report = _report()
63 with pytest.raises(ValueError, match="unknown format"): 65 with pytest.raises(ValueError, match="unknown format"):
64 reporters.render(_report(), "pdf") 66 reporters.render(report, "pdf")
tests/test_trend.py +2 −1
@@ -71,5 +71,6 @@ def test_html_is_self_contained():
71 html = trend.render_html(_compare()) 71 html = trend.render_html(_compare())
72 assert html.startswith("<!doctype html>") 72 assert html.startswith("<!doctype html>")
73 assert "<style>" in html 73 assert "<style>" in html
74 assert "http://" not in html and "https://" not in html 74 assert "http://" not in html
75 assert "https://" not in html
75 assert "CC9.2" in html # a changed control shows up 76 assert "CC9.2" in html # a changed control shows up