loopgate 0.1.0__py3-none-any.whl
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.
- harness/.githooks/_resolve +47 -0
- harness/.githooks/hoist +45 -0
- harness/.githooks/pre-commit +7 -0
- harness/.githooks/pre-push +10 -0
- harness/.githooks/prepare-commit-msg +7 -0
- harness/__init__.py +0 -0
- harness/cli.py +532 -0
- harness/config.py +203 -0
- harness/docs/PROJECT_STATUS.md +38 -0
- harness/docs/PROMPT.md +39 -0
- harness/docs/plan.md +68 -0
- harness/docs/specs/another_spec.md +56 -0
- harness/docs/specs/base.md +56 -0
- harness/gate.py +297 -0
- harness/js-scaffold/PROMPT.md +8 -0
- harness/js-scaffold/README.md +12 -0
- harness/js-scaffold/index.html +47 -0
- harness/js-scaffold/package-lock.json +176 -0
- harness/js-scaffold/package.json +16 -0
- harness/js-scaffold/quiz.js +21 -0
- harness/js-scaffold/specs/quiz.md +6 -0
- harness/js-scaffold/test.js +8 -0
- harness/ralph.ps1 +84 -0
- harness/ralph.sh +34 -0
- harness/temp.pyproject.toml +407 -0
- harness/tests/mutation/mutmut-cicd-stats.json +11 -0
- harness/tests/mutation/test_check_mutmut.py +135 -0
- harness/tests/preferences/test_preferences.py +498 -0
- harness/tests/preferences/test_preferences_properties.py +245 -0
- loopgate-0.1.0.dist-info/METADATA +478 -0
- loopgate-0.1.0.dist-info/RECORD +37 -0
- loopgate-0.1.0.dist-info/WHEEL +4 -0
- loopgate-0.1.0.dist-info/entry_points.txt +2 -0
- loopgate-0.1.0.dist-info/licenses/LICENSE +21 -0
- mutation/check_mutmut.py +163 -0
- preferences/__init__.py +0 -0
- preferences/preferences.py +306 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Property-based tests for preferences.preferences.py using Hypothesis.
|
|
2
|
+
|
|
3
|
+
"With Hypothesis, you write tests which should pass for all inputs in whatever range you describe, and let
|
|
4
|
+
Hypothesis randomly choose which of those inputs to check, including edge cases you might not have thought
|
|
5
|
+
about." TESTS THE CODE WITH A RANGE OF INPUTS.
|
|
6
|
+
Hypothesis docs: https://hypothesis.readthedocs.io/
|
|
7
|
+
|
|
8
|
+
Tests that preferences.py checks for names, classes, comprehensions, and continue
|
|
9
|
+
|
|
10
|
+
Hypothesis persistence: Do not set database=None by default. Local runs use Hypothesis's example
|
|
11
|
+
database under .hypothesis/examples, so past failures are replayed first and users can debug them
|
|
12
|
+
quickly. CI automatically uses Hypothesis's built-in `ci` profile, which is stateless and deterministic.
|
|
13
|
+
If a generated input is important, save it as @example(...) or a normal regression test instead of
|
|
14
|
+
relying on the local database. The generated .hypothesis/ directory is gitignored.
|
|
15
|
+
|
|
16
|
+
Test hygiene: keep strategies at module scope. Set max_examples only when a test needs a runtime cap. Do
|
|
17
|
+
not use function-scoped fixtures with @given; patch per-example state inside helper functions instead.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import keyword
|
|
24
|
+
import string
|
|
25
|
+
import sys
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from unittest import mock
|
|
28
|
+
|
|
29
|
+
from hypothesis import assume, given, strategies
|
|
30
|
+
|
|
31
|
+
# preferences.py are optional AST-style checks. Repo-owner can keep or delete.
|
|
32
|
+
preferences_violations: Callable[[str, str], str] | None
|
|
33
|
+
try:
|
|
34
|
+
from preferences.preferences import preferences_violations
|
|
35
|
+
except ImportError:
|
|
36
|
+
preferences_violations = None
|
|
37
|
+
|
|
38
|
+
# Patterns in a deterministic order so hypothesis shrinks toward the first entry predictably.
|
|
39
|
+
IDENTIFIER_START = string.ascii_letters + "_"
|
|
40
|
+
IDENTIFIER_REST = IDENTIFIER_START + string.digits
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@strategies.composite
|
|
44
|
+
def identifiers(draw: strategies.DrawFn) -> str:
|
|
45
|
+
"""Draw a valid ASCII Python identifier that is not a keyword."""
|
|
46
|
+
first = draw(strategies.sampled_from(IDENTIFIER_START))
|
|
47
|
+
rest = draw(strategies.text(alphabet=IDENTIFIER_REST, max_size=24))
|
|
48
|
+
name = first + rest
|
|
49
|
+
assume(not keyword.iskeyword(name))
|
|
50
|
+
return name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
IDENTIFIERS = identifiers()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def flags(source: str, needle: str) -> bool:
|
|
57
|
+
"""Whether preferences_violations reports a message containing needle for source."""
|
|
58
|
+
return needle in preferences_violations("m.py", source) if preferences_violations else False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------- preferences.py-absent fallback (optional module)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_module_tolerates_absent_preferences_on_import() -> None:
|
|
65
|
+
"""When preferences.preferences.py can't be imported (e.g. human deleted), this module still loads and its
|
|
66
|
+
preferences_violations is None, so its gate tests here keep running. mock.patch.dict maps the module
|
|
67
|
+
name to None (the standard way to make `import` raise ImportError) and auto-restores it; reloading
|
|
68
|
+
this module under that patch exercises the ImportError fallback, then a final reload restores it.
|
|
69
|
+
"""
|
|
70
|
+
module = sys.modules[__name__]
|
|
71
|
+
try:
|
|
72
|
+
with mock.patch.dict(sys.modules, {"preferences.preferences": None}):
|
|
73
|
+
reloaded = importlib.reload(module)
|
|
74
|
+
assert reloaded.preferences_violations is None
|
|
75
|
+
finally:
|
|
76
|
+
importlib.reload(module) # restore the real preferences_violations for the rest of the suite
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_flags_never_calls_preferences_when_it_is_absent() -> None:
|
|
80
|
+
"""Deleting preferences.py must not crash flags(). It reports no violation instead.
|
|
81
|
+
|
|
82
|
+
A variable named with a leading underscore breaks the underscore rule. The first assert confirms
|
|
83
|
+
the rule fires on it. The second feeds in that same variable with the module gone and gets no
|
|
84
|
+
violation back, which is only possible if preferences_violations was never called.
|
|
85
|
+
"""
|
|
86
|
+
source, rule = "_bad = 1\n", "starts with underscore"
|
|
87
|
+
assert flags(source, rule) is True
|
|
88
|
+
with mock.patch.object(sys.modules[__name__], "preferences_violations", None):
|
|
89
|
+
assert flags(source, rule) is False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --------------------------------------------------------------------- underscore-lead identifier rule
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@given(name=IDENTIFIERS)
|
|
96
|
+
def test_underscore_lead_flagged_iff_leading_underscore_not_dunder(name: str) -> None:
|
|
97
|
+
"""An assignment target trips the underscore rule IFF it has a prohibited leading underscore.
|
|
98
|
+
Covers the whole identifier domain, including dunders and the exempt lone '_', in one property.
|
|
99
|
+
"""
|
|
100
|
+
expected = name != "_" and name.startswith("_") and not name.startswith("__") and not name.endswith("__")
|
|
101
|
+
assert flags(f"{name} = 1\n", "starts with underscore") is expected
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@given(name=IDENTIFIERS)
|
|
105
|
+
def test_underscore_rule_holds_for_function_and_argument_names(name: str) -> None:
|
|
106
|
+
"""The same underscore rule applies to function names and argument names, not just assignments."""
|
|
107
|
+
assume(not name.endswith("__")) # keep dunder methods/args (__init__ etc.) out of this slice
|
|
108
|
+
expected = name != "_" and name.startswith("_") and not name.startswith("__")
|
|
109
|
+
assert flags(f"def {name}():\n return 1\n", "starts with underscore") is expected
|
|
110
|
+
assert flags(f"def f({name}):\n return {name}\n", "starts with underscore") is expected
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------------------- pointless-class rule
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@strategies.composite
|
|
117
|
+
def class_source(draw: strategies.DrawFn) -> tuple[str, bool]:
|
|
118
|
+
"""Draw a class definition varying base/decorator/keyword presence and method count.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
draw: Hypothesis draw callable.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
(source, should_flag) where should_flag is the documented intent: trip IFF the class has no
|
|
125
|
+
base, no decorator, no keyword, and at most one method.
|
|
126
|
+
"""
|
|
127
|
+
has_base = draw(strategies.booleans())
|
|
128
|
+
has_decorator = draw(strategies.booleans())
|
|
129
|
+
has_keyword = draw(strategies.booleans())
|
|
130
|
+
method_count = draw(strategies.integers(min_value=0, max_value=3))
|
|
131
|
+
|
|
132
|
+
decorator = "@deco\n" if has_decorator else ""
|
|
133
|
+
header_bits = (["Base"] if has_base else []) + (["metaclass=type"] if has_keyword else [])
|
|
134
|
+
header = f"({', '.join(header_bits)})" if header_bits else ""
|
|
135
|
+
body = "".join(f" def m{i}(self):\n return {i}\n" for i in range(method_count)) or " x = 1\n"
|
|
136
|
+
source = f"{decorator}class C{header}:\n{body}"
|
|
137
|
+
|
|
138
|
+
should_flag = not has_base and not has_decorator and not has_keyword and method_count <= 1
|
|
139
|
+
return source, should_flag
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@given(case=class_source())
|
|
143
|
+
def test_pointless_class_flagged_iff_plain_and_at_most_one_method(case: tuple[str, bool]) -> None:
|
|
144
|
+
"""A class trips the pointless-class rule IFF it is plain (no base/decorator/keyword) with <= 1
|
|
145
|
+
method. Any base, decorator, keyword, or a second method exempts it.
|
|
146
|
+
"""
|
|
147
|
+
source, should_flag = case
|
|
148
|
+
assert flags(source, "no base, decorator, or behavior") is should_flag
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------------------ complex-comprehension rule
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@strategies.composite
|
|
155
|
+
def comprehension_source(draw: strategies.DrawFn) -> tuple[str, bool]:
|
|
156
|
+
"""Draw a list comprehension with a chosen generator count and which generator (if any) filters.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
draw: Hypothesis draw callable.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
(source, should_flag) where should_flag is the documented intent: trip IFF there is more than
|
|
163
|
+
one generator AND at least one generator carries an `if`. Crucially the filtered generator may
|
|
164
|
+
be a LATER one, exercising the check's early-return-on-first-match loop.
|
|
165
|
+
"""
|
|
166
|
+
generator_count = draw(strategies.integers(min_value=1, max_value=3))
|
|
167
|
+
# -1 means "no if on any generator"; otherwise the index of the single generator that filters.
|
|
168
|
+
if_on = draw(strategies.integers(min_value=-1, max_value=generator_count - 1))
|
|
169
|
+
|
|
170
|
+
clauses: list[str] = []
|
|
171
|
+
for index in range(generator_count):
|
|
172
|
+
clause = f"for v{index} in xs{index}"
|
|
173
|
+
if index == if_on:
|
|
174
|
+
clause += f" if v{index}"
|
|
175
|
+
clauses.append(clause)
|
|
176
|
+
source = f"[v0 {' '.join(clauses)}]\n"
|
|
177
|
+
|
|
178
|
+
should_flag = generator_count > 1 and if_on != -1
|
|
179
|
+
return source, should_flag
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@given(case=comprehension_source())
|
|
183
|
+
def test_complex_comprehension_flagged_iff_multi_generator_with_filter(case: tuple[str, bool]) -> None:
|
|
184
|
+
"""A comprehension trips IFF it has multiple generators AND at least one has an `if` -- regardless of
|
|
185
|
+
WHICH generator carries the `if`. The later-generator case guards the check's early return, which
|
|
186
|
+
scans generators in order and returns on the first one that filters.
|
|
187
|
+
"""
|
|
188
|
+
source, should_flag = case
|
|
189
|
+
assert flags(source, "Overly complex comprehension") is should_flag
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# --------------------------------------------------------------------------- chaotic-continue rule
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@strategies.composite
|
|
196
|
+
def nested_continue_source(draw: strategies.DrawFn) -> str:
|
|
197
|
+
"""Draw a `continue` wrapped in an outer `for` plus TWO-to-four more if/for blocks.
|
|
198
|
+
|
|
199
|
+
The rule allows one `if` guard directly inside a loop (`for: if: continue`), so to always be
|
|
200
|
+
over-nested we stack at least two blocks below the outer loop.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
draw: Hypothesis draw callable.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Source whose `continue` sits at least two if/for blocks below its enclosing loop, so the
|
|
207
|
+
over-nesting rule always flags it.
|
|
208
|
+
"""
|
|
209
|
+
depth = draw(strategies.integers(min_value=2, max_value=4))
|
|
210
|
+
blocks = draw(strategies.lists(strategies.sampled_from(["if cond", "for i in xs"]), min_size=depth, max_size=depth))
|
|
211
|
+
|
|
212
|
+
lines = ["for outer in items:"] # an outer loop the continue always belongs to
|
|
213
|
+
indent = " "
|
|
214
|
+
for block in blocks:
|
|
215
|
+
lines.append(f"{indent}{block}:")
|
|
216
|
+
indent += " "
|
|
217
|
+
lines.append(f"{indent}continue")
|
|
218
|
+
return "\n".join(lines) + "\n"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@given(source=nested_continue_source())
|
|
222
|
+
def test_continue_nested_under_stacked_blocks_is_flagged(source: str) -> None:
|
|
223
|
+
"""A `continue` stacked two or more if/for blocks below its enclosing loop is flagged as overly
|
|
224
|
+
nested, whatever mix of if/for those blocks are.
|
|
225
|
+
"""
|
|
226
|
+
assert flags(source, "Overly-nested 'continue'")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_single_if_guard_in_a_loop_is_not_flagged() -> None:
|
|
230
|
+
"""The common, readable `for ...: if ...: continue` (one if guard in one loop) is NOT over-nested."""
|
|
231
|
+
assert not flags("for i in items:\n if skip:\n continue\n", "Overly-nested 'continue'")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_shallow_continue_in_single_loop_is_not_flagged() -> None:
|
|
235
|
+
"""Control (example, not property): a `continue` directly in one `for` -- parent For, grandparent
|
|
236
|
+
module -- is not overly nested, so it is not flagged.
|
|
237
|
+
"""
|
|
238
|
+
assert not flags("for x in items:\n continue\n", "Overly-nested 'continue'")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_continue_in_while_loop_is_flagged() -> None:
|
|
242
|
+
"""Control (example): a `continue` anywhere inside a while loop is flagged (freeze risk), a separate
|
|
243
|
+
branch from the nested-if detection.
|
|
244
|
+
"""
|
|
245
|
+
assert flags("while cond:\n continue\n", "while loop banned")
|