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.
Files changed (82) hide show
  1. {ratch-0.1.2/ratch.egg-info → ratch-0.1.3}/PKG-INFO +1 -1
  2. {ratch-0.1.2 → ratch-0.1.3}/pyproject.toml +1 -1
  3. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/ai_signatures.py +39 -13
  4. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/bdd_conventions.py +18 -10
  5. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/catalog_size.py +13 -6
  6. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/commit_heatmap.py +22 -3
  7. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/font_cdn.py +1 -1
  8. {ratch-0.1.2 → ratch-0.1.3}/ratch/cli.py +4 -0
  9. {ratch-0.1.2 → ratch-0.1.3}/ratch/registry.py +12 -12
  10. {ratch-0.1.2 → ratch-0.1.3}/ratch/runner.py +2 -0
  11. {ratch-0.1.2 → ratch-0.1.3/ratch.egg-info}/PKG-INFO +1 -1
  12. {ratch-0.1.2 → ratch-0.1.3}/tests/test_ai_signatures.py +3 -4
  13. {ratch-0.1.2 → ratch-0.1.3}/tests/test_bdd_conventions.py +22 -0
  14. {ratch-0.1.2 → ratch-0.1.3}/tests/test_catalog_size.py +1 -1
  15. {ratch-0.1.2 → ratch-0.1.3}/tests/test_cli.py +14 -3
  16. {ratch-0.1.2 → ratch-0.1.3}/tests/test_commit_heatmap.py +8 -1
  17. {ratch-0.1.2 → ratch-0.1.3}/tests/test_font_cdn.py +2 -2
  18. {ratch-0.1.2 → ratch-0.1.3}/tests/test_registry.py +3 -3
  19. {ratch-0.1.2 → ratch-0.1.3}/tests/test_self_host.py +2 -2
  20. {ratch-0.1.2 → ratch-0.1.3}/tests/test_self_host_blackbox.py +7 -1
  21. {ratch-0.1.2 → ratch-0.1.3}/LICENSE +0 -0
  22. {ratch-0.1.2 → ratch-0.1.3}/README.md +0 -0
  23. {ratch-0.1.2 → ratch-0.1.3}/ratch/__init__.py +0 -0
  24. {ratch-0.1.2 → ratch-0.1.3}/ratch/__main__.py +0 -0
  25. {ratch-0.1.2 → ratch-0.1.3}/ratch/check.py +0 -0
  26. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/__init__.py +0 -0
  27. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/autoclose.py +0 -0
  28. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/circular_import.py +0 -0
  29. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/conflict_markers.py +0 -0
  30. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/doc_cli.py +0 -0
  31. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/doc_counts.py +0 -0
  32. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/docs_render.py +0 -0
  33. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/first_person.py +0 -0
  34. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/forbidden_literal.py +0 -0
  35. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/hash_named_test.py +0 -0
  36. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/injected_clock.py +0 -0
  37. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/internal_refs.py +0 -0
  38. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/loc_cap.py +0 -0
  39. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/manifest_purity.py +0 -0
  40. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/plugin_registry.py +0 -0
  41. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/pytest_skip.py +0 -0
  42. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/reassurance.py +0 -0
  43. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/repo_root_ssot.py +0 -0
  44. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/todo_issue.py +0 -0
  45. {ratch-0.1.2 → ratch-0.1.3}/ratch/checks/vacuous_assert.py +0 -0
  46. {ratch-0.1.2 → ratch-0.1.3}/ratch/pytest_plugin.py +0 -0
  47. {ratch-0.1.2 → ratch-0.1.3}/ratch/reporter.py +0 -0
  48. {ratch-0.1.2 → ratch-0.1.3}/ratch/result.py +0 -0
  49. {ratch-0.1.2 → ratch-0.1.3}/ratch/testing.py +0 -0
  50. {ratch-0.1.2 → ratch-0.1.3}/ratch/workspace.py +0 -0
  51. {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/SOURCES.txt +0 -0
  52. {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/dependency_links.txt +0 -0
  53. {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/entry_points.txt +0 -0
  54. {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/requires.txt +0 -0
  55. {ratch-0.1.2 → ratch-0.1.3}/ratch.egg-info/top_level.txt +0 -0
  56. {ratch-0.1.2 → ratch-0.1.3}/setup.cfg +0 -0
  57. {ratch-0.1.2 → ratch-0.1.3}/tests/test_autoclose.py +0 -0
  58. {ratch-0.1.2 → ratch-0.1.3}/tests/test_check.py +0 -0
  59. {ratch-0.1.2 → ratch-0.1.3}/tests/test_circular_import.py +0 -0
  60. {ratch-0.1.2 → ratch-0.1.3}/tests/test_conflict_markers.py +0 -0
  61. {ratch-0.1.2 → ratch-0.1.3}/tests/test_doc_cli.py +0 -0
  62. {ratch-0.1.2 → ratch-0.1.3}/tests/test_doc_counts.py +0 -0
  63. {ratch-0.1.2 → ratch-0.1.3}/tests/test_docs_render.py +0 -0
  64. {ratch-0.1.2 → ratch-0.1.3}/tests/test_first_person.py +0 -0
  65. {ratch-0.1.2 → ratch-0.1.3}/tests/test_forbidden_literal.py +0 -0
  66. {ratch-0.1.2 → ratch-0.1.3}/tests/test_hash_named_test.py +0 -0
  67. {ratch-0.1.2 → ratch-0.1.3}/tests/test_injected_clock.py +0 -0
  68. {ratch-0.1.2 → ratch-0.1.3}/tests/test_internal_refs.py +0 -0
  69. {ratch-0.1.2 → ratch-0.1.3}/tests/test_loc_cap.py +0 -0
  70. {ratch-0.1.2 → ratch-0.1.3}/tests/test_manifest_purity.py +0 -0
  71. {ratch-0.1.2 → ratch-0.1.3}/tests/test_plugin_registry.py +0 -0
  72. {ratch-0.1.2 → ratch-0.1.3}/tests/test_pytest_seam.py +0 -0
  73. {ratch-0.1.2 → ratch-0.1.3}/tests/test_pytest_skip.py +0 -0
  74. {ratch-0.1.2 → ratch-0.1.3}/tests/test_reassurance.py +0 -0
  75. {ratch-0.1.2 → ratch-0.1.3}/tests/test_repo_root_ssot.py +0 -0
  76. {ratch-0.1.2 → ratch-0.1.3}/tests/test_reporter.py +0 -0
  77. {ratch-0.1.2 → ratch-0.1.3}/tests/test_result.py +0 -0
  78. {ratch-0.1.2 → ratch-0.1.3}/tests/test_runner.py +0 -0
  79. {ratch-0.1.2 → ratch-0.1.3}/tests/test_testing.py +0 -0
  80. {ratch-0.1.2 → ratch-0.1.3}/tests/test_todo_issue.py +0 -0
  81. {ratch-0.1.2 → ratch-0.1.3}/tests/test_vacuous_assert.py +0 -0
  82. {ratch-0.1.2 → ratch-0.1.3}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ratch
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: The ratchet only turns one way: code is the single source of truth.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ratch"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "The ratchet only turns one way: code is the single source of truth."
9
9
  license = {text = "Apache-2.0"}
10
10
  requires-python = ">=3.11"
@@ -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 not self._is_shallow(ws):
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
- for pattern in self.patterns:
88
- # IGNORECASE so GitHub's canonical `Co-authored-by:` casing
89
- # (and `generated with` / `assisted by`) is caught too
90
- # (resolves Fable finding: default missed lowercase trailers).
91
- if re.search(pattern, haystack, re.IGNORECASE):
92
- anchor = f"{sha[:9]}:{pattern}"
93
- findings.append(
94
- Finding(self.id, "<commit>", anchor,
95
- message=f"provenance leak {anchor}")
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("test_"):
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, every top-level def whose name does not
41
- start with underscore must contain _should_ and _when_, must not
42
- start with test_, and must not use or as a snake_case segment.
43
- The body must be three blocks separated by two blank lines.
44
- Labels such as given/when/then are allowed and ignored.
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
- if not _is_bdd_name(name):
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) < 3:
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
- The installed ratch.checks catalog size is reported as a
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
- n = len(ws.plugin_classes())
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=n, state=MState.MEASURED,
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 n < self.min_surface:
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=n, findings=[], measured=measured,
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; full history)"
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="\n".join(lines),
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={"a.py": "x = 1\n"},
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"
@@ -49,7 +49,7 @@ class NoExternalFontCdn:
49
49
  if findings:
50
50
  return State.FAIL
51
51
  if examined_n < self.min_surface:
52
- return State.VACUOUS
52
+ return State.NOT_APPLICABLE
53
53
  return State.PASS
54
54
 
55
55
  def check(self, ws):
@@ -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 the default gate.
22
+ """Return the check instances a repo declares, or an empty list.
25
23
 
26
- A repo opts into a custom check set by exposing a CHECKS list from a
27
- ratch_checks.py module at its root; absent that file, the default
28
- surface is the single forbidden-literal gate.
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
- spec = importlib.util.spec_from_file_location("ratch_checks", manifest)
33
- module = importlib.util.module_from_spec(spec)
34
- spec.loader.exec_module(module)
35
- return list(module.CHECKS)
36
- return [NoForbiddenLiteral()]
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ratch
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: The ratchet only turns one way: code is the single source of truth.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -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 no_ai_signatures_should_be_vacuous_when_the_repo_is_shallow():
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.VACUOUS
84
- assert result.examined_n == 0
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 == 1
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, {"pkg/mod.py": f'MARK = "{banned}"\n'})
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, {"pkg/mod.py": "MARK = 'clean'\n"})
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"] == "enabled"
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(files={"a.py": "x = 1\n"}, git_log_text=log)
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 no_external_font_cdn_should_be_vacuous_when_no_web_file_is_present():
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.VACUOUS
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 load_manifest_should_return_the_default_check_when_no_manifest_exists(tmp_path):
17
+ def load_manifest_should_return_no_checks_when_no_manifest_exists(tmp_path):
18
18
  checks = load_manifest(tmp_path)
19
19
 
20
- assert len(checks) == 1
20
+ found = len(checks)
21
21
 
22
- assert isinstance(checks[0], NoForbiddenLiteral)
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 single-check default a fresh repo falls
5
- back to. no-external-font-cdn is shipped as an entry point only.
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
- subprocess.run(["git", "add", "seed.py"], cwd=root, check=True)
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