ratch 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {ratch-0.1.2/ratch.egg-info → ratch-0.1.3}/PKG-INFO +1 -1
- {ratch-0.1.2 → ratch-0.1.3}/pyproject.toml +1 -1
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/ai_signatures.py +39 -13
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/bdd_conventions.py +18 -10
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/catalog_size.py +13 -6
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/commit_heatmap.py +22 -3
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/font_cdn.py +1 -1
- {ratch-0.1.2 → ratch-0.1.3}/ratch/cli.py +4 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/registry.py +12 -12
- {ratch-0.1.2 → ratch-0.1.3}/ratch/runner.py +2 -0
- {ratch-0.1.2 → ratch-0.1.3/ratch.egg-info}/PKG-INFO +1 -1
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_ai_signatures.py +3 -4
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_bdd_conventions.py +22 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_catalog_size.py +1 -1
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_cli.py +14 -3
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_commit_heatmap.py +8 -1
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_font_cdn.py +2 -2
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_registry.py +3 -3
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_self_host.py +2 -2
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_self_host_blackbox.py +7 -1
- {ratch-0.1.2 → ratch-0.1.3}/LICENSE +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/README.md +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/__init__.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/__main__.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/check.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/__init__.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/autoclose.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/circular_import.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/conflict_markers.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/doc_cli.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/doc_counts.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/docs_render.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/first_person.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/forbidden_literal.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/hash_named_test.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/injected_clock.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/internal_refs.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/loc_cap.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/manifest_purity.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/plugin_registry.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/pytest_skip.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/reassurance.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/repo_root_ssot.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/todo_issue.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/vacuous_assert.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/pytest_plugin.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/reporter.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/result.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/testing.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch/workspace.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/SOURCES.txt +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/dependency_links.txt +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/entry_points.txt +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/requires.txt +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/top_level.txt +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/setup.cfg +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_autoclose.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_check.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_circular_import.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_conflict_markers.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_doc_cli.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_doc_counts.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_docs_render.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_first_person.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_forbidden_literal.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_hash_named_test.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_injected_clock.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_internal_refs.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_loc_cap.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_manifest_purity.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_plugin_registry.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_pytest_seam.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_pytest_skip.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_reassurance.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_repo_root_ssot.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_reporter.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_result.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_runner.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_testing.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_todo_issue.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_vacuous_assert.py +0 -0
- {ratch-0.1.2 → ratch-0.1.3}/tests/test_workspace.py +0 -0
|
@@ -53,11 +53,13 @@ class NoAiSignatures:
|
|
|
53
53
|
r"Assisted by",
|
|
54
54
|
"\N{ROBOT FACE}",
|
|
55
55
|
r"[\w.+-]+@users\.noreply\.[\w.]+",
|
|
56
|
-
), min_surface=1):
|
|
56
|
+
), min_surface=1, sources=("log",), files=()):
|
|
57
57
|
if min_surface < 1:
|
|
58
58
|
raise ValueError("min_surface must be >= 1")
|
|
59
59
|
self.patterns = tuple(patterns)
|
|
60
60
|
self.min_surface = min_surface
|
|
61
|
+
self.sources = tuple(sources)
|
|
62
|
+
self.files = tuple(files)
|
|
61
63
|
|
|
62
64
|
def _state(self, findings, examined_n):
|
|
63
65
|
if findings:
|
|
@@ -70,12 +72,29 @@ class NoAiSignatures:
|
|
|
70
72
|
proc = ws.run(["git", "rev-parse", "--is-shallow-repository"])
|
|
71
73
|
return proc.stdout.strip() == "true"
|
|
72
74
|
|
|
75
|
+
def _scan(self, haystack, path, prefix, findings):
|
|
76
|
+
for pattern in self.patterns:
|
|
77
|
+
if re.search(pattern, haystack, re.IGNORECASE):
|
|
78
|
+
anchor = f"{prefix}:{pattern}"
|
|
79
|
+
findings.append(
|
|
80
|
+
Finding(self.id, path, anchor,
|
|
81
|
+
message=f"provenance leak {anchor}")
|
|
82
|
+
)
|
|
83
|
+
|
|
73
84
|
def check(self, ws):
|
|
74
85
|
findings = []
|
|
75
86
|
examined_n = 0
|
|
76
|
-
if
|
|
87
|
+
if self._is_shallow(ws):
|
|
88
|
+
findings.append(
|
|
89
|
+
Finding(self.id, "<git>", "shallow",
|
|
90
|
+
message="shallow clone; history is incomplete")
|
|
91
|
+
)
|
|
92
|
+
return Result(
|
|
93
|
+
self.id, State.FAIL, examined_n=1,
|
|
94
|
+
skipped_n=ws.skipped_n, findings=findings,
|
|
95
|
+
)
|
|
96
|
+
if "log" in self.sources:
|
|
77
97
|
raw = ws.git_log(
|
|
78
|
-
rng=None,
|
|
79
98
|
fmt="%H%x00%an%x00%ae%x00%cn%x00%ce%x00%B%x00",
|
|
80
99
|
)
|
|
81
100
|
fields = raw.split("\x00")
|
|
@@ -84,16 +103,23 @@ class NoAiSignatures:
|
|
|
84
103
|
an, ae, cn, ce, body = fields[i + 1:i + 6]
|
|
85
104
|
examined_n += 1
|
|
86
105
|
haystack = "\n".join([an, ae, cn, ce, body])
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
self._scan(haystack, "<commit>", sha[:9], findings)
|
|
107
|
+
if "tags" in self.sources:
|
|
108
|
+
proc = ws.run([
|
|
109
|
+
"git", "for-each-ref", "refs/tags",
|
|
110
|
+
"--format=%(refname)%x00%(contents)",
|
|
111
|
+
])
|
|
112
|
+
blob = proc.stdout or ""
|
|
113
|
+
if blob.strip():
|
|
114
|
+
examined_n += 1
|
|
115
|
+
self._scan(blob, "<tag>", "tag", findings)
|
|
116
|
+
if "files" in self.sources:
|
|
117
|
+
tracked = set(ws.tracked_files())
|
|
118
|
+
for path in self.files:
|
|
119
|
+
if path not in tracked:
|
|
120
|
+
continue
|
|
121
|
+
examined_n += 1
|
|
122
|
+
self._scan(ws.read(path), path, path, findings)
|
|
97
123
|
return Result(
|
|
98
124
|
self.id, self._state(findings, examined_n),
|
|
99
125
|
examined_n=examined_n, skipped_n=ws.skipped_n,
|
|
@@ -9,8 +9,10 @@ from ratch.result import Finding, Result, State
|
|
|
9
9
|
from ratch.testing import FakeWorkspace
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def _is_bdd_name(name):
|
|
13
|
-
if name.startswith(
|
|
12
|
+
def _is_bdd_name(name, prefix):
|
|
13
|
+
if prefix and not name.startswith(prefix):
|
|
14
|
+
return None
|
|
15
|
+
if not prefix and name.startswith("test_"):
|
|
14
16
|
return False
|
|
15
17
|
if "_should_" not in name or "_when_" not in name:
|
|
16
18
|
return False
|
|
@@ -37,11 +39,12 @@ class BddTestConventions:
|
|
|
37
39
|
"""Require BDD test names in tracked test modules.
|
|
38
40
|
|
|
39
41
|
Rule:
|
|
40
|
-
In tracked tests/*.py,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
In tracked tests/*.py, selected top-level defs must contain
|
|
43
|
+
_should_ and _when_ and must not use or as a snake_case segment.
|
|
44
|
+
prefix="" (this repo) inspects every public def and forbids a
|
|
45
|
+
test_ prefix. prefix="test_" inspects only that prefix (helpers
|
|
46
|
+
are ignored). blank_blocks=3 requires two blank-line gaps;
|
|
47
|
+
blank_blocks=0 skips body shape. Labels are ignored.
|
|
45
48
|
|
|
46
49
|
Why:
|
|
47
50
|
A test name that reads as a sentence is the readable spec; a
|
|
@@ -67,10 +70,12 @@ class BddTestConventions:
|
|
|
67
70
|
confidence = "inferred"
|
|
68
71
|
tolerates_unparseable = False
|
|
69
72
|
|
|
70
|
-
def __init__(self, min_surface=1):
|
|
73
|
+
def __init__(self, min_surface=1, prefix="", blank_blocks=3):
|
|
71
74
|
if min_surface < 1:
|
|
72
75
|
raise ValueError("min_surface must be >= 1")
|
|
73
76
|
self.min_surface = min_surface
|
|
77
|
+
self.prefix = prefix
|
|
78
|
+
self.blank_blocks = blank_blocks
|
|
74
79
|
|
|
75
80
|
def _state(self, findings, examined_n):
|
|
76
81
|
if findings:
|
|
@@ -96,13 +101,16 @@ class BddTestConventions:
|
|
|
96
101
|
name = node.name
|
|
97
102
|
if name.startswith("_"):
|
|
98
103
|
continue
|
|
99
|
-
|
|
104
|
+
verdict = _is_bdd_name(name, self.prefix)
|
|
105
|
+
if verdict is None:
|
|
106
|
+
continue
|
|
107
|
+
if not verdict:
|
|
100
108
|
findings.append(
|
|
101
109
|
Finding(self.id, path, name,
|
|
102
110
|
message=f"bdd name: {name}")
|
|
103
111
|
)
|
|
104
112
|
continue
|
|
105
|
-
if _paragraph_n(node, lines) <
|
|
113
|
+
if self.blank_blocks and _paragraph_n(node, lines) < self.blank_blocks:
|
|
106
114
|
findings.append(
|
|
107
115
|
Finding(self.id, path, name,
|
|
108
116
|
message=f"bdd blank: {name}")
|
|
@@ -8,8 +8,8 @@ class CatalogSize:
|
|
|
8
8
|
"""Report how many checks are installed, without gating.
|
|
9
9
|
|
|
10
10
|
Rule:
|
|
11
|
-
|
|
12
|
-
measured value.
|
|
11
|
+
Reports enabled_n (manifest CHECKS) and catalog_n (entry
|
|
12
|
+
points) as a measured value.
|
|
13
13
|
|
|
14
14
|
Why:
|
|
15
15
|
A gate that failed the run because the catalog was empty would
|
|
@@ -38,17 +38,24 @@ class CatalogSize:
|
|
|
38
38
|
self.min_surface = min_surface
|
|
39
39
|
|
|
40
40
|
def check(self, ws):
|
|
41
|
-
|
|
41
|
+
catalog_n = len(ws.plugin_classes())
|
|
42
|
+
enabled_n = catalog_n
|
|
43
|
+
root = getattr(ws, "repo_root", None)
|
|
44
|
+
if root is not None:
|
|
45
|
+
from ratch.registry import load_manifest
|
|
46
|
+
enabled_n = len(load_manifest(root))
|
|
42
47
|
measured = MeasuredValue(
|
|
43
|
-
value=
|
|
48
|
+
value=f"enabled_n={enabled_n} catalog_n={catalog_n}",
|
|
49
|
+
state=MState.MEASURED,
|
|
44
50
|
source="plugin_classes", measured_at=ws.now(),
|
|
45
51
|
)
|
|
46
|
-
if
|
|
52
|
+
if catalog_n < self.min_surface:
|
|
47
53
|
state = State.VACUOUS
|
|
48
54
|
else:
|
|
49
55
|
state = State.PASS
|
|
50
56
|
return Result(
|
|
51
|
-
self.id, state, examined_n=
|
|
57
|
+
self.id, state, examined_n=catalog_n, findings=[],
|
|
58
|
+
measured=measured,
|
|
52
59
|
)
|
|
53
60
|
|
|
54
61
|
def plants(self, ws):
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""commit-heatmap eye: directory groups, file rows, time columns."""
|
|
2
|
+
import os
|
|
2
3
|
from collections import Counter, defaultdict
|
|
3
4
|
from datetime import datetime, timedelta
|
|
4
5
|
|
|
@@ -27,10 +28,14 @@ _GROWTH_STEPS = (
|
|
|
27
28
|
(316, "#", "@"),
|
|
28
29
|
)
|
|
29
30
|
_GROWTH_CAPTION = (
|
|
30
|
-
"growth (per-column ins-del, log ladder, not file size;
|
|
31
|
+
"growth (per-column ins-del, log ladder, not file size; tracked only)"
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def _want_compact():
|
|
36
|
+
return bool(os.environ.get("RATCH_COMPACT"))
|
|
37
|
+
|
|
38
|
+
|
|
34
39
|
def _top_dir(path):
|
|
35
40
|
if "/" not in path:
|
|
36
41
|
return "(root)"
|
|
@@ -242,11 +247,14 @@ class CommitHeatmap:
|
|
|
242
247
|
touch_dir_n = Counter()
|
|
243
248
|
touch_file_n = Counter()
|
|
244
249
|
grow_file_n = Counter()
|
|
250
|
+
tracked = set(ws.tracked_files())
|
|
245
251
|
for ts, files in commits:
|
|
246
252
|
col = _bucket_key(ts, grain)
|
|
247
253
|
if col not in retained:
|
|
248
254
|
continue
|
|
249
255
|
for path, ins, dele in files:
|
|
256
|
+
if path not in tracked:
|
|
257
|
+
continue
|
|
250
258
|
top = _top_dir(path)
|
|
251
259
|
touch_dir[(top, col)] += 1
|
|
252
260
|
touch_dir_n[top] += 1
|
|
@@ -273,8 +281,15 @@ class CommitHeatmap:
|
|
|
273
281
|
_GROWTH_CAPTION, grow_dir, grow_file, dirs,
|
|
274
282
|
_group(dirs, grow_files), cols, _growth_char,
|
|
275
283
|
))
|
|
284
|
+
if _want_compact():
|
|
285
|
+
value = (
|
|
286
|
+
f"tracked-only commits={n} dirs={len(dirs)} "
|
|
287
|
+
f"files={len(touch_files)}"
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
value = "\n".join(lines)
|
|
276
291
|
measured = MeasuredValue(
|
|
277
|
-
value=
|
|
292
|
+
value=value,
|
|
278
293
|
state=MState.MEASURED,
|
|
279
294
|
source="git_log:numstat",
|
|
280
295
|
measured_at=ws.now(),
|
|
@@ -292,7 +307,11 @@ class CommitHeatmap:
|
|
|
292
307
|
|
|
293
308
|
def fixture(self, kit):
|
|
294
309
|
return FakeWorkspace(
|
|
295
|
-
files={
|
|
310
|
+
files={
|
|
311
|
+
"a.py": "x = 1\n",
|
|
312
|
+
"ratch/checks/a.py": "x = 1\n",
|
|
313
|
+
"tests/t.py": "x = 1\n",
|
|
314
|
+
},
|
|
296
315
|
git_log_text=(
|
|
297
316
|
"\x1e1700000000\n"
|
|
298
317
|
"12\t0\tratch/checks/a.py\n"
|
|
@@ -5,6 +5,7 @@ Only check builds a Workspace and a Runner (and may request nicing).
|
|
|
5
5
|
"""
|
|
6
6
|
import argparse
|
|
7
7
|
import inspect
|
|
8
|
+
import os
|
|
8
9
|
import pathlib
|
|
9
10
|
import sys
|
|
10
11
|
|
|
@@ -21,6 +22,7 @@ def build_parser():
|
|
|
21
22
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
22
23
|
check = sub.add_parser("check")
|
|
23
24
|
check.add_argument("--staged", action="store_true")
|
|
25
|
+
check.add_argument("--compact", action="store_true")
|
|
24
26
|
sub.add_parser("list")
|
|
25
27
|
describe = sub.add_parser("describe")
|
|
26
28
|
describe.add_argument("id")
|
|
@@ -57,6 +59,8 @@ def main(argv=None):
|
|
|
57
59
|
if args.command == "describe":
|
|
58
60
|
return cmd_describe(args.id)
|
|
59
61
|
|
|
62
|
+
if args.compact:
|
|
63
|
+
os.environ["RATCH_COMPACT"] = "1"
|
|
60
64
|
view = "index" if args.staged else "worktree"
|
|
61
65
|
ws = Workspace(cwd, view)
|
|
62
66
|
checks = load_manifest(cwd)
|
|
@@ -9,8 +9,6 @@ import importlib.metadata
|
|
|
9
9
|
import importlib.util
|
|
10
10
|
import pathlib
|
|
11
11
|
|
|
12
|
-
from ratch.checks.forbidden_literal import NoForbiddenLiteral
|
|
13
|
-
|
|
14
12
|
__all__ = ["discover", "load_manifest"]
|
|
15
13
|
|
|
16
14
|
|
|
@@ -21,16 +19,18 @@ def discover():
|
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
def load_manifest(repo_root):
|
|
24
|
-
"""Return the check instances a repo declares, or
|
|
22
|
+
"""Return the check instances a repo declares, or an empty list.
|
|
25
23
|
|
|
26
|
-
A repo opts into
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
A repo opts into checks by exposing CHECKS from ratch_checks.py at
|
|
25
|
+
its root. Absent that file, nothing runs: a silent default gate
|
|
26
|
+
would scan product names in consumer READMEs.
|
|
29
27
|
"""
|
|
30
28
|
manifest = pathlib.Path(repo_root) / "ratch_checks.py"
|
|
31
|
-
if manifest.is_file():
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
-
|
|
29
|
+
if not manifest.is_file():
|
|
30
|
+
return []
|
|
31
|
+
spec = importlib.util.spec_from_file_location("ratch_checks", manifest)
|
|
32
|
+
if spec is None or spec.loader is None:
|
|
33
|
+
return []
|
|
34
|
+
module = importlib.util.module_from_spec(spec)
|
|
35
|
+
spec.loader.exec_module(module)
|
|
36
|
+
return list(module.CHECKS)
|
|
@@ -39,6 +39,8 @@ def derive_state(raw, examined_n, unparseable_n, tolerates_unparseable,
|
|
|
39
39
|
"""
|
|
40
40
|
if raw is State.ERROR:
|
|
41
41
|
return State.ERROR
|
|
42
|
+
if raw is State.NOT_APPLICABLE:
|
|
43
|
+
return State.NOT_APPLICABLE
|
|
42
44
|
if unparseable_n > 0 and not tolerates_unparseable:
|
|
43
45
|
return State.FAIL
|
|
44
46
|
if findings:
|
|
@@ -68,7 +68,7 @@ def no_ai_signatures_should_pass_when_history_carries_no_attribution_shape():
|
|
|
68
68
|
assert result.findings == []
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def
|
|
71
|
+
def no_ai_signatures_should_fail_when_the_repo_is_shallow():
|
|
72
72
|
sha = "deadbeef0badf00d"
|
|
73
73
|
trailer = "feat: x\n\nCo-Authored-By: Bot <bot@example.test>\n"
|
|
74
74
|
ws = FakeWorkspace(
|
|
@@ -80,9 +80,8 @@ def no_ai_signatures_should_be_vacuous_when_the_repo_is_shallow():
|
|
|
80
80
|
|
|
81
81
|
result = NoAiSignatures().check(ws)
|
|
82
82
|
|
|
83
|
-
assert result.state is State.
|
|
84
|
-
assert
|
|
85
|
-
assert result.findings == []
|
|
83
|
+
assert result.state is State.FAIL
|
|
84
|
+
assert any(f.anchor == "shallow" for f in result.findings)
|
|
86
85
|
|
|
87
86
|
|
|
88
87
|
def no_ai_signatures_should_fail_when_trailer_uses_github_lowercase_casing():
|
|
@@ -114,3 +114,25 @@ def bdd_test_conventions_should_be_discoverable_when_registered_as_an_entry_poin
|
|
|
114
114
|
expected = BddTestConventions
|
|
115
115
|
|
|
116
116
|
assert found is expected
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def bdd_test_conventions_should_ignore_helpers_when_prefix_is_test_():
|
|
120
|
+
src = (
|
|
121
|
+
"def make_toml():\n return 1\n"
|
|
122
|
+
"def test_foo_should_bar_when_baz():\n"
|
|
123
|
+
" x = 1\n\n y = 2\n\n assert x\n"
|
|
124
|
+
)
|
|
125
|
+
ws = FakeWorkspace(files={"tests/t.py": src})
|
|
126
|
+
|
|
127
|
+
result = BddTestConventions(prefix="test_").check(ws)
|
|
128
|
+
|
|
129
|
+
assert result.state is State.PASS
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def bdd_test_conventions_should_skip_body_shape_when_blank_blocks_is_zero():
|
|
133
|
+
src = "def foo_should_bar_when_baz():\n x = 1\n assert x\n"
|
|
134
|
+
ws = FakeWorkspace(files={"tests/t.py": src})
|
|
135
|
+
|
|
136
|
+
result = BddTestConventions(blank_blocks=0).check(ws)
|
|
137
|
+
|
|
138
|
+
assert result.state is State.PASS
|
|
@@ -18,7 +18,7 @@ def catalog_size_should_pass_when_the_workspace_has_plugins():
|
|
|
18
18
|
|
|
19
19
|
assert result.state is State.PASS
|
|
20
20
|
assert result.measured is not None
|
|
21
|
-
assert result.measured.value
|
|
21
|
+
assert "catalog_n=1" in str(result.measured.value)
|
|
22
22
|
assert result.measured.state is MState.MEASURED
|
|
23
23
|
|
|
24
24
|
|
|
@@ -5,10 +5,18 @@ from ratch.cli import main
|
|
|
5
5
|
from ratch.registry import discover
|
|
6
6
|
from ratch.testing import make_tmp_repo
|
|
7
7
|
|
|
8
|
+
_MANIFEST = (
|
|
9
|
+
"from ratch.checks.forbidden_literal import NoForbiddenLiteral\n"
|
|
10
|
+
"CHECKS = [NoForbiddenLiteral()]\n"
|
|
11
|
+
)
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
def main_should_return_one_when_the_repo_has_a_violation(tmp_path, monkeypatch):
|
|
10
15
|
banned = "cl" + "aude"
|
|
11
|
-
repo = make_tmp_repo(tmp_path, {
|
|
16
|
+
repo = make_tmp_repo(tmp_path, {
|
|
17
|
+
"pkg/mod.py": f'MARK = "{banned}"\n',
|
|
18
|
+
"ratch_checks.py": _MANIFEST,
|
|
19
|
+
})
|
|
12
20
|
monkeypatch.chdir(repo)
|
|
13
21
|
monkeypatch.setattr(os, "nice", lambda n: None)
|
|
14
22
|
|
|
@@ -18,7 +26,10 @@ def main_should_return_one_when_the_repo_has_a_violation(tmp_path, monkeypatch):
|
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
def main_should_return_zero_when_the_repo_is_clean(tmp_path, monkeypatch):
|
|
21
|
-
repo = make_tmp_repo(tmp_path, {
|
|
29
|
+
repo = make_tmp_repo(tmp_path, {
|
|
30
|
+
"pkg/mod.py": "MARK = 'clean'\n",
|
|
31
|
+
"ratch_checks.py": _MANIFEST,
|
|
32
|
+
})
|
|
22
33
|
monkeypatch.chdir(repo)
|
|
23
34
|
monkeypatch.setattr(os, "nice", lambda n: None)
|
|
24
35
|
|
|
@@ -51,7 +62,7 @@ def main_should_list_discovered_plugins_when_invoked_with_list(
|
|
|
51
62
|
assert line == f"{name:<{width}}{label}"
|
|
52
63
|
labels[name] = label
|
|
53
64
|
assert set(labels.values()) <= {"enabled", "catalog"}
|
|
54
|
-
assert labels["no-forbidden-literal"] == "
|
|
65
|
+
assert labels["no-forbidden-literal"] == "catalog"
|
|
55
66
|
assert labels["no-external-font-cdn"] == "catalog"
|
|
56
67
|
|
|
57
68
|
|
|
@@ -18,7 +18,14 @@ _LOG = (
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _ws(log=_LOG):
|
|
21
|
-
return FakeWorkspace(
|
|
21
|
+
return FakeWorkspace(
|
|
22
|
+
files={
|
|
23
|
+
"a.py": "x = 1\n",
|
|
24
|
+
"ratch/checks/a.py": "x = 1\n",
|
|
25
|
+
"tests/t.py": "x = 1\n",
|
|
26
|
+
},
|
|
27
|
+
git_log_text=log,
|
|
28
|
+
)
|
|
22
29
|
|
|
23
30
|
|
|
24
31
|
def parse_log_should_read_insert_and_delete_counts_when_numstat_line_is_plain():
|
|
@@ -27,12 +27,12 @@ def no_external_font_cdn_should_pass_when_css_uses_local_fonts():
|
|
|
27
27
|
assert result.state is State.PASS
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def
|
|
30
|
+
def no_external_font_cdn_should_be_not_applicable_when_no_web_file_is_present():
|
|
31
31
|
ws = FakeWorkspace(files={"a.py": "x = 1\n"})
|
|
32
32
|
|
|
33
33
|
result = NoExternalFontCdn().check(ws)
|
|
34
34
|
|
|
35
|
-
assert result.state is State.
|
|
35
|
+
assert result.state is State.NOT_APPLICABLE
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def no_external_font_cdn_should_bite_its_plants_when_checked_against_its_own_fixture(tmp_path):
|
|
@@ -14,12 +14,12 @@ def discover_should_resolve_no_forbidden_literal_when_the_package_is_installed()
|
|
|
14
14
|
assert found is expected
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def load_manifest_should_return_no_checks_when_no_manifest_exists(tmp_path):
|
|
18
18
|
checks = load_manifest(tmp_path)
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
found = len(checks)
|
|
21
21
|
|
|
22
|
-
assert
|
|
22
|
+
assert found == 0
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def no_forbidden_literal_should_raise_when_min_surface_is_zero():
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""The ratch repo dogfoods every tooth it ships.
|
|
2
2
|
|
|
3
3
|
load_manifest read at this repo's own root must return the twenty-three ids in
|
|
4
|
-
ratch_checks.py CHECKS, not the
|
|
5
|
-
|
|
4
|
+
ratch_checks.py CHECKS, not the empty default a fresh repo falls back to.
|
|
5
|
+
no-external-font-cdn is shipped as an entry point only.
|
|
6
6
|
"""
|
|
7
7
|
import importlib
|
|
8
8
|
|
|
@@ -25,7 +25,13 @@ def _hand_rolled_repo():
|
|
|
25
25
|
cwd=root, check=True,
|
|
26
26
|
)
|
|
27
27
|
(root / "seed.py").write_text("x = 1\n")
|
|
28
|
-
|
|
28
|
+
(root / "ratch_checks.py").write_text(
|
|
29
|
+
"from ratch.checks.forbidden_literal import NoForbiddenLiteral\n"
|
|
30
|
+
"CHECKS = [NoForbiddenLiteral()]\n"
|
|
31
|
+
)
|
|
32
|
+
subprocess.run(
|
|
33
|
+
["git", "add", "seed.py", "ratch_checks.py"], cwd=root, check=True,
|
|
34
|
+
)
|
|
29
35
|
subprocess.run(["git", "commit", "-q", "-m", "seed"], cwd=root, check=True)
|
|
30
36
|
return root
|
|
31
37
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|