claude-dev-env 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/agents/agent-writer.md +157 -0
- package/agents/clasp-deployment-orchestrator.md +609 -0
- package/agents/clean-coder.md +295 -0
- package/agents/code-quality-agent.md +40 -0
- package/agents/code-standards-agent.md +93 -0
- package/agents/config-centralizer.md +686 -0
- package/agents/config-extraction-agent.md +225 -0
- package/agents/doc-orchestrator.md +47 -0
- package/agents/docs-agent.md +112 -0
- package/agents/docx-agent.md +211 -0
- package/agents/git-commit-crafter.md +100 -0
- package/agents/magic-value-eliminator-agent.md +72 -0
- package/agents/mandatory-agent-workflow-agent.md +88 -0
- package/agents/parallel-workflow-coordinator.md +779 -0
- package/agents/pdf-agent.md +302 -0
- package/agents/plan-executor.md +226 -0
- package/agents/pr-description-writer.md +87 -0
- package/agents/project-context-loader.md +238 -0
- package/agents/project-docs-analyzer.md +54 -0
- package/agents/project-structure-organizer-agent.md +72 -0
- package/agents/readability-review-agent.md +76 -0
- package/agents/refactoring-specialist.md +69 -0
- package/agents/right-sized-engineer.md +129 -0
- package/agents/session-continuity-manager.md +53 -0
- package/agents/skill-to-agent-converter.md +371 -0
- package/agents/skill-writer-agent.md +470 -0
- package/agents/stub-detector-agent.md +140 -0
- package/agents/tdd-test-writer.md +62 -0
- package/agents/test-data-builder.md +68 -0
- package/agents/tooling-builder.md +78 -0
- package/agents/user-docs-writer.md +67 -0
- package/agents/validation-expert.md +71 -0
- package/agents/workflow-visual-documenter.md +82 -0
- package/agents/xlsx-agent.md +169 -0
- package/bin/install.mjs +256 -0
- package/commands/commit.md +28 -0
- package/commands/docupdate.md +322 -0
- package/commands/implement.md +102 -0
- package/commands/initialize.md +91 -0
- package/commands/plan.md +63 -0
- package/commands/pr-comments.md +47 -0
- package/commands/readability-review.md +20 -0
- package/commands/review-plan.md +7 -0
- package/commands/right-size.md +15 -0
- package/commands/stubcheck.md +89 -0
- package/commands/sum.md +30 -0
- package/docs/CODE_RULES.md +186 -0
- package/docs/DJANGO_PATTERNS.md +80 -0
- package/docs/REACT_PATTERNS.md +185 -0
- package/docs/TEST_QUALITY.md +104 -0
- package/hooks/advisory/migration-safety-advisor.py +49 -0
- package/hooks/advisory/refactor-guard.py +205 -0
- package/hooks/blocking/block-main-commit.py +168 -0
- package/hooks/blocking/code-rules-enforcer.py +549 -0
- package/hooks/blocking/destructive-command-blocker.py +107 -0
- package/hooks/blocking/docker-settings-guard.py +44 -0
- package/hooks/blocking/hedging-language-blocker.py +130 -0
- package/hooks/blocking/parallel-task-blocker.py +69 -0
- package/hooks/blocking/pr-description-enforcer.py +87 -0
- package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
- package/hooks/blocking/sensitive-file-protector.py +70 -0
- package/hooks/blocking/tdd-enforcer.py +62 -0
- package/hooks/blocking/test-preflight-check.py +343 -0
- package/hooks/blocking/write-existing-file-blocker.py +63 -0
- package/hooks/git-hooks/post-commit.py +103 -0
- package/hooks/github-action/test_workflow.py +33 -0
- package/hooks/hooks.json +246 -0
- package/hooks/lifecycle/config-change-guard.py +84 -0
- package/hooks/lifecycle/session-end-cleanup.py +59 -0
- package/hooks/notification/attention-needed-notify.py +63 -0
- package/hooks/notification/claude-notification-handler.py +59 -0
- package/hooks/notification/notification_utils.py +206 -0
- package/hooks/rewrite-plugin-paths.py +116 -0
- package/hooks/session/bulk-edit-reminder.py +30 -0
- package/hooks/session/code-rules-reminder.py +97 -0
- package/hooks/session/compact-context-reinject.py +39 -0
- package/hooks/session/hook-structure-context.py +140 -0
- package/hooks/session/plugin-data-dir-cleanup.py +39 -0
- package/hooks/validation/code-style-validator.py +145 -0
- package/hooks/validation/e2e-test-validator.py +142 -0
- package/hooks/validation/hook-format-validator.py +66 -0
- package/hooks/validation/mypy_validator.py +180 -0
- package/hooks/validators/README.md +125 -0
- package/hooks/validators/VALIDATION_REPORT.md +287 -0
- package/hooks/validators/__init__.py +19 -0
- package/hooks/validators/abbreviation_checks.py +82 -0
- package/hooks/validators/code_quality_checks.py +133 -0
- package/hooks/validators/comment_checks.py +188 -0
- package/hooks/validators/file_structure_checks.py +182 -0
- package/hooks/validators/git_checks.py +107 -0
- package/hooks/validators/health_check.py +214 -0
- package/hooks/validators/magic_value_checks.py +81 -0
- package/hooks/validators/mypy_integration.py +52 -0
- package/hooks/validators/output_formatter.py +266 -0
- package/hooks/validators/pr_reference_checks.py +72 -0
- package/hooks/validators/python_antipattern_checks.py +110 -0
- package/hooks/validators/python_style_checks.py +364 -0
- package/hooks/validators/react_checks.py +90 -0
- package/hooks/validators/ruff_integration.py +80 -0
- package/hooks/validators/run_all_validators.py +772 -0
- package/hooks/validators/security_checks.py +135 -0
- package/hooks/validators/test_abbreviation_checks.py +76 -0
- package/hooks/validators/test_bad.tsx +7 -0
- package/hooks/validators/test_code_quality_checks.py +129 -0
- package/hooks/validators/test_file_structure_checks.py +307 -0
- package/hooks/validators/test_files/01_basic_component.tsx +10 -0
- package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
- package/hooks/validators/test_files/03_pure_component.tsx +10 -0
- package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
- package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
- package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
- package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
- package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
- package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
- package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
- package/hooks/validators/test_files/11_ts_file.ts +10 -0
- package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
- package/hooks/validators/test_files/13_functional_component.tsx +8 -0
- package/hooks/validators/test_files/14_indented_class.tsx +13 -0
- package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
- package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
- package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
- package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
- package/hooks/validators/test_files/async_views.py +23 -0
- package/hooks/validators/test_files/async_with_imports.py +14 -0
- package/hooks/validators/test_files/bad_inline_imports.py +37 -0
- package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
- package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
- package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
- package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
- package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
- package/hooks/validators/test_files/not_management_command.py +10 -0
- package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
- package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
- package/hooks/validators/test_files/test_async_functions.py +45 -0
- package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
- package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
- package/hooks/validators/test_git_checks.py +295 -0
- package/hooks/validators/test_good.tsx +5 -0
- package/hooks/validators/test_health_check.py +57 -0
- package/hooks/validators/test_magic_value_checks.py +63 -0
- package/hooks/validators/test_mypy_integration.py +27 -0
- package/hooks/validators/test_output_formatter.py +150 -0
- package/hooks/validators/test_pr_reference_checks.py +41 -0
- package/hooks/validators/test_python_antipattern_checks.py +113 -0
- package/hooks/validators/test_python_style_checks.py +439 -0
- package/hooks/validators/test_react_checks.py +213 -0
- package/hooks/validators/test_results.txt +25 -0
- package/hooks/validators/test_ruff_integration.py +27 -0
- package/hooks/validators/test_run_all_validators.py +228 -0
- package/hooks/validators/test_run_all_validators_integration.py +48 -0
- package/hooks/validators/test_safety_checks.py +243 -0
- package/hooks/validators/test_security_checks.py +105 -0
- package/hooks/validators/test_test_safety_checks.py +321 -0
- package/hooks/validators/test_todo_checks.py +39 -0
- package/hooks/validators/test_type_safety_checks.py +85 -0
- package/hooks/validators/test_useless_test_checks.py +55 -0
- package/hooks/validators/test_validator_base.py +26 -0
- package/hooks/validators/test_verify_paths.py +34 -0
- package/hooks/validators/todo_checks.py +59 -0
- package/hooks/validators/type_safety_checks.py +101 -0
- package/hooks/validators/useless_test_checks.py +92 -0
- package/hooks/validators/validator_base.py +19 -0
- package/hooks/validators/verify_paths.py +57 -0
- package/hooks/workflow/auto-formatter.py +114 -0
- package/hooks/workflow/investigation-tracker-reset.py +46 -0
- package/package.json +30 -0
- package/rules/agent-spawn-protocol.md +47 -0
- package/rules/cleanup-temp-files.md +27 -0
- package/rules/code-reviews.md +11 -0
- package/rules/code-standards.md +43 -0
- package/rules/conservative-action.md +20 -0
- package/rules/context7.md +12 -0
- package/rules/explore-thoroughly.md +27 -0
- package/rules/git-workflow.md +42 -0
- package/rules/parallel-tools.md +23 -0
- package/rules/research-mode.md +23 -0
- package/rules/right-sized-engineering.md +28 -0
- package/rules/tdd.md +7 -0
- package/rules/testing.md +12 -0
- package/skills/agent-prompt/SKILL.md +102 -0
- package/skills/anthropic-plan/SKILL.md +107 -0
- package/skills/everything-search/SKILL.md +144 -0
- package/skills/ingest/SKILL.md +40 -0
- package/skills/npm-creator/SKILL.md +183 -0
- package/skills/pr-review-responder/EXAMPLES.md +590 -0
- package/skills/pr-review-responder/PRINCIPLES.md +539 -0
- package/skills/pr-review-responder/README.md +209 -0
- package/skills/pr-review-responder/SKILL.md +202 -0
- package/skills/pr-review-responder/TESTING.md +407 -0
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
- package/skills/pr-review-responder/update_skill.py +297 -0
- package/skills/prompt-generator/REFERENCE.md +150 -0
- package/skills/prompt-generator/SKILL.md +154 -0
- package/skills/readability-review/SKILL.md +127 -0
- package/skills/recall/SKILL.md +27 -0
- package/skills/remember/SKILL.md +63 -0
- package/skills/rule-audit/SKILL.md +307 -0
- package/skills/rule-creator/SKILL.md +150 -0
- package/skills/skill-writer/REFERENCE.md +246 -0
- package/skills/skill-writer/SKILL.md +270 -0
- package/skills/tdd-team/SKILL.md +128 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Tests for Python style checks."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from python_style_checks import (
|
|
11
|
+
Violation,
|
|
12
|
+
check_imports_at_top,
|
|
13
|
+
check_no_empty_line_after_decorators,
|
|
14
|
+
check_single_empty_line_between_functions,
|
|
15
|
+
check_view_function_naming,
|
|
16
|
+
validate_file,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Test data: Code samples
|
|
21
|
+
GOOD_IMPORTS = '''import os
|
|
22
|
+
import sys
|
|
23
|
+
from typing import List
|
|
24
|
+
|
|
25
|
+
def foo() -> None:
|
|
26
|
+
pass
|
|
27
|
+
'''
|
|
28
|
+
|
|
29
|
+
BAD_IMPORTS_AFTER_CODE = '''def foo() -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
'''
|
|
34
|
+
|
|
35
|
+
BAD_IMPORTS_AFTER_CONSTANT = '''MY_CONSTANT = 42
|
|
36
|
+
|
|
37
|
+
import os
|
|
38
|
+
'''
|
|
39
|
+
|
|
40
|
+
GOOD_DECORATOR_NO_BLANK = '''@decorator
|
|
41
|
+
def foo() -> None:
|
|
42
|
+
pass
|
|
43
|
+
'''
|
|
44
|
+
|
|
45
|
+
BAD_DECORATOR_WITH_BLANK = '''@decorator
|
|
46
|
+
|
|
47
|
+
def foo() -> None:
|
|
48
|
+
pass
|
|
49
|
+
'''
|
|
50
|
+
|
|
51
|
+
GOOD_SINGLE_LINE_BETWEEN_FUNCTIONS = '''def foo() -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def bar() -> None:
|
|
55
|
+
pass
|
|
56
|
+
'''
|
|
57
|
+
|
|
58
|
+
BAD_NO_LINE_BETWEEN_FUNCTIONS = '''def foo() -> None:
|
|
59
|
+
pass
|
|
60
|
+
def bar() -> None:
|
|
61
|
+
pass
|
|
62
|
+
'''
|
|
63
|
+
|
|
64
|
+
BAD_MULTIPLE_LINES_BETWEEN_FUNCTIONS = '''def foo() -> None:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def bar() -> None:
|
|
69
|
+
pass
|
|
70
|
+
'''
|
|
71
|
+
|
|
72
|
+
GOOD_VIEW_NAMING = '''def user_profile_view(request):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def get_tasks_view(request, user_id):
|
|
76
|
+
pass
|
|
77
|
+
'''
|
|
78
|
+
|
|
79
|
+
BAD_VIEW_NAMING = '''def user_profile(request):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def getTasks(request, user_id):
|
|
83
|
+
pass
|
|
84
|
+
'''
|
|
85
|
+
|
|
86
|
+
GOOD_NON_VIEW_FUNCTION = '''def helper_function(data):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
def process_data(items):
|
|
90
|
+
pass
|
|
91
|
+
'''
|
|
92
|
+
|
|
93
|
+
# Async function test samples
|
|
94
|
+
ASYNC_GOOD_DECORATOR_NO_BLANK = '''import asyncio
|
|
95
|
+
|
|
96
|
+
@asyncio.coroutine
|
|
97
|
+
async def foo() -> None:
|
|
98
|
+
await asyncio.sleep(0)
|
|
99
|
+
'''
|
|
100
|
+
|
|
101
|
+
ASYNC_BAD_DECORATOR_WITH_BLANK = '''import asyncio
|
|
102
|
+
|
|
103
|
+
@asyncio.coroutine
|
|
104
|
+
|
|
105
|
+
async def foo() -> None:
|
|
106
|
+
await asyncio.sleep(0)
|
|
107
|
+
'''
|
|
108
|
+
|
|
109
|
+
ASYNC_GOOD_SINGLE_LINE_BETWEEN = '''import asyncio
|
|
110
|
+
|
|
111
|
+
async def foo() -> None:
|
|
112
|
+
await asyncio.sleep(0)
|
|
113
|
+
|
|
114
|
+
async def bar() -> None:
|
|
115
|
+
await asyncio.sleep(0)
|
|
116
|
+
'''
|
|
117
|
+
|
|
118
|
+
ASYNC_BAD_MULTIPLE_LINES_BETWEEN = '''import asyncio
|
|
119
|
+
|
|
120
|
+
async def foo() -> None:
|
|
121
|
+
await asyncio.sleep(0)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def bar() -> None:
|
|
125
|
+
await asyncio.sleep(0)
|
|
126
|
+
'''
|
|
127
|
+
|
|
128
|
+
ASYNC_GOOD_VIEW_NAMING = '''from django.http import HttpRequest, HttpResponse
|
|
129
|
+
|
|
130
|
+
async def user_profile_view(request: HttpRequest) -> HttpResponse:
|
|
131
|
+
return HttpResponse("profile")
|
|
132
|
+
|
|
133
|
+
async def get_tasks_view(request: HttpRequest, user_id: int) -> HttpResponse:
|
|
134
|
+
return HttpResponse("tasks")
|
|
135
|
+
'''
|
|
136
|
+
|
|
137
|
+
ASYNC_BAD_VIEW_NAMING = '''from django.http import HttpRequest, HttpResponse
|
|
138
|
+
|
|
139
|
+
async def user_profile(request: HttpRequest) -> HttpResponse:
|
|
140
|
+
return HttpResponse("profile")
|
|
141
|
+
|
|
142
|
+
async def getTasks(request: HttpRequest, user_id: int) -> HttpResponse:
|
|
143
|
+
return HttpResponse("tasks")
|
|
144
|
+
'''
|
|
145
|
+
|
|
146
|
+
ASYNC_BAD_INLINE_IMPORT = '''import asyncio
|
|
147
|
+
|
|
148
|
+
async def foo() -> None:
|
|
149
|
+
import json
|
|
150
|
+
await asyncio.sleep(0)
|
|
151
|
+
'''
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestImportsAtTop:
|
|
155
|
+
"""Test import positioning validation."""
|
|
156
|
+
|
|
157
|
+
def test_imports_at_top_valid(self) -> None:
|
|
158
|
+
"""All imports at top should pass."""
|
|
159
|
+
tree = ast.parse(GOOD_IMPORTS)
|
|
160
|
+
violations = check_imports_at_top(tree, "test.py")
|
|
161
|
+
assert violations == []
|
|
162
|
+
|
|
163
|
+
def test_import_after_function(self) -> None:
|
|
164
|
+
"""Import after function should fail."""
|
|
165
|
+
tree = ast.parse(BAD_IMPORTS_AFTER_CODE)
|
|
166
|
+
violations = check_imports_at_top(tree, "test.py")
|
|
167
|
+
assert len(violations) == 1
|
|
168
|
+
assert violations[0].line == 4
|
|
169
|
+
assert "import" in violations[0].message.lower()
|
|
170
|
+
|
|
171
|
+
def test_import_after_constant(self) -> None:
|
|
172
|
+
"""Import after constant should fail."""
|
|
173
|
+
tree = ast.parse(BAD_IMPORTS_AFTER_CONSTANT)
|
|
174
|
+
violations = check_imports_at_top(tree, "test.py")
|
|
175
|
+
assert len(violations) == 1
|
|
176
|
+
assert violations[0].line == 3
|
|
177
|
+
|
|
178
|
+
def test_async_inline_import_fails(self) -> None:
|
|
179
|
+
"""Import inside async function should fail."""
|
|
180
|
+
tree = ast.parse(ASYNC_BAD_INLINE_IMPORT)
|
|
181
|
+
violations = check_imports_at_top(tree, "test.py")
|
|
182
|
+
assert len(violations) == 1
|
|
183
|
+
assert "inside function" in violations[0].message.lower()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestNoEmptyLineAfterDecorators:
|
|
187
|
+
"""Test decorator spacing validation."""
|
|
188
|
+
|
|
189
|
+
def test_no_blank_line_valid(self) -> None:
|
|
190
|
+
"""Decorator directly above function should pass."""
|
|
191
|
+
violations = check_no_empty_line_after_decorators(
|
|
192
|
+
GOOD_DECORATOR_NO_BLANK, "test.py"
|
|
193
|
+
)
|
|
194
|
+
assert violations == []
|
|
195
|
+
|
|
196
|
+
def test_blank_line_after_decorator_fails(self) -> None:
|
|
197
|
+
"""Blank line after decorator should fail."""
|
|
198
|
+
violations = check_no_empty_line_after_decorators(
|
|
199
|
+
BAD_DECORATOR_WITH_BLANK, "test.py"
|
|
200
|
+
)
|
|
201
|
+
assert len(violations) == 1
|
|
202
|
+
assert violations[0].line == 1
|
|
203
|
+
assert "decorator" in violations[0].message.lower()
|
|
204
|
+
|
|
205
|
+
def test_async_no_blank_line_valid(self) -> None:
|
|
206
|
+
"""Decorator directly above async function should pass."""
|
|
207
|
+
violations = check_no_empty_line_after_decorators(
|
|
208
|
+
ASYNC_GOOD_DECORATOR_NO_BLANK, "test.py"
|
|
209
|
+
)
|
|
210
|
+
assert violations == []
|
|
211
|
+
|
|
212
|
+
def test_async_blank_line_after_decorator_fails(self) -> None:
|
|
213
|
+
"""Blank line after decorator on async function should fail."""
|
|
214
|
+
violations = check_no_empty_line_after_decorators(
|
|
215
|
+
ASYNC_BAD_DECORATOR_WITH_BLANK, "test.py"
|
|
216
|
+
)
|
|
217
|
+
assert len(violations) == 1
|
|
218
|
+
assert violations[0].line == 3
|
|
219
|
+
assert "decorator" in violations[0].message.lower()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestSingleEmptyLineBetweenFunctions:
|
|
223
|
+
"""Test function spacing validation."""
|
|
224
|
+
|
|
225
|
+
def test_single_line_valid(self) -> None:
|
|
226
|
+
"""Exactly one blank line should pass."""
|
|
227
|
+
violations = check_single_empty_line_between_functions(
|
|
228
|
+
GOOD_SINGLE_LINE_BETWEEN_FUNCTIONS, "test.py"
|
|
229
|
+
)
|
|
230
|
+
assert violations == []
|
|
231
|
+
|
|
232
|
+
def test_no_line_between_functions_fails(self) -> None:
|
|
233
|
+
"""No blank line should fail."""
|
|
234
|
+
violations = check_single_empty_line_between_functions(
|
|
235
|
+
BAD_NO_LINE_BETWEEN_FUNCTIONS, "test.py"
|
|
236
|
+
)
|
|
237
|
+
assert len(violations) == 1
|
|
238
|
+
assert "empty line" in violations[0].message.lower()
|
|
239
|
+
|
|
240
|
+
def test_multiple_lines_between_functions_fails(self) -> None:
|
|
241
|
+
"""Multiple blank lines should fail."""
|
|
242
|
+
violations = check_single_empty_line_between_functions(
|
|
243
|
+
BAD_MULTIPLE_LINES_BETWEEN_FUNCTIONS, "test.py"
|
|
244
|
+
)
|
|
245
|
+
assert len(violations) == 1
|
|
246
|
+
assert "empty line" in violations[0].message.lower()
|
|
247
|
+
|
|
248
|
+
def test_async_single_line_valid(self) -> None:
|
|
249
|
+
"""Exactly one blank line between async functions should pass."""
|
|
250
|
+
violations = check_single_empty_line_between_functions(
|
|
251
|
+
ASYNC_GOOD_SINGLE_LINE_BETWEEN, "test.py"
|
|
252
|
+
)
|
|
253
|
+
assert violations == []
|
|
254
|
+
|
|
255
|
+
def test_async_multiple_lines_fails(self) -> None:
|
|
256
|
+
"""Multiple blank lines between async functions should fail."""
|
|
257
|
+
violations = check_single_empty_line_between_functions(
|
|
258
|
+
ASYNC_BAD_MULTIPLE_LINES_BETWEEN, "test.py"
|
|
259
|
+
)
|
|
260
|
+
assert len(violations) == 1
|
|
261
|
+
assert "empty line" in violations[0].message.lower()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TestViewFunctionNaming:
|
|
265
|
+
"""Test view function naming validation."""
|
|
266
|
+
|
|
267
|
+
def test_view_functions_named_correctly(self) -> None:
|
|
268
|
+
"""View functions ending with _view should pass."""
|
|
269
|
+
tree = ast.parse(GOOD_VIEW_NAMING)
|
|
270
|
+
violations = check_view_function_naming(tree, "views.py")
|
|
271
|
+
assert violations == []
|
|
272
|
+
|
|
273
|
+
def test_view_functions_without_suffix_fail(self) -> None:
|
|
274
|
+
"""View functions not ending with _view should fail."""
|
|
275
|
+
tree = ast.parse(BAD_VIEW_NAMING)
|
|
276
|
+
violations = check_view_function_naming(tree, "views.py")
|
|
277
|
+
assert len(violations) == 2
|
|
278
|
+
assert all("_view" in v.message for v in violations)
|
|
279
|
+
|
|
280
|
+
def test_non_view_file_ignored(self) -> None:
|
|
281
|
+
"""Non-views.py files should be ignored."""
|
|
282
|
+
tree = ast.parse(BAD_VIEW_NAMING)
|
|
283
|
+
violations = check_view_function_naming(tree, "utils.py")
|
|
284
|
+
assert violations == []
|
|
285
|
+
|
|
286
|
+
def test_non_request_function_ignored(self) -> None:
|
|
287
|
+
"""Functions without request parameter should be ignored."""
|
|
288
|
+
tree = ast.parse(GOOD_NON_VIEW_FUNCTION)
|
|
289
|
+
violations = check_view_function_naming(tree, "views.py")
|
|
290
|
+
assert violations == []
|
|
291
|
+
|
|
292
|
+
def test_async_view_functions_named_correctly(self) -> None:
|
|
293
|
+
"""Async view functions ending with _view should pass."""
|
|
294
|
+
tree = ast.parse(ASYNC_GOOD_VIEW_NAMING)
|
|
295
|
+
violations = check_view_function_naming(tree, "views.py")
|
|
296
|
+
assert violations == []
|
|
297
|
+
|
|
298
|
+
def test_async_view_functions_without_suffix_fail(self) -> None:
|
|
299
|
+
"""Async view functions not ending with _view should fail."""
|
|
300
|
+
tree = ast.parse(ASYNC_BAD_VIEW_NAMING)
|
|
301
|
+
violations = check_view_function_naming(tree, "views.py")
|
|
302
|
+
assert len(violations) == 2
|
|
303
|
+
assert all("_view" in v.message for v in violations)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class TestValidateFile:
|
|
307
|
+
"""Test file validation integration."""
|
|
308
|
+
|
|
309
|
+
def test_valid_file_passes(self) -> None:
|
|
310
|
+
"""File with no violations should pass."""
|
|
311
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
312
|
+
f.write(GOOD_IMPORTS)
|
|
313
|
+
f.flush()
|
|
314
|
+
temp_path = Path(f.name)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
violations = validate_file(temp_path)
|
|
318
|
+
assert violations == []
|
|
319
|
+
finally:
|
|
320
|
+
temp_path.unlink()
|
|
321
|
+
|
|
322
|
+
def test_invalid_file_returns_violations(self) -> None:
|
|
323
|
+
"""File with violations should return them."""
|
|
324
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
325
|
+
f.write(BAD_IMPORTS_AFTER_CODE)
|
|
326
|
+
f.flush()
|
|
327
|
+
temp_path = Path(f.name)
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
violations = validate_file(temp_path)
|
|
331
|
+
assert len(violations) > 0
|
|
332
|
+
assert all(isinstance(v, Violation) for v in violations)
|
|
333
|
+
finally:
|
|
334
|
+
temp_path.unlink()
|
|
335
|
+
|
|
336
|
+
def test_syntax_error_returns_violation(self) -> None:
|
|
337
|
+
"""File with syntax error should return violation."""
|
|
338
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
339
|
+
f.write("def foo(\n") # Invalid syntax
|
|
340
|
+
f.flush()
|
|
341
|
+
temp_path = Path(f.name)
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
violations = validate_file(temp_path)
|
|
345
|
+
assert len(violations) == 1
|
|
346
|
+
assert "syntax error" in violations[0].message.lower()
|
|
347
|
+
finally:
|
|
348
|
+
temp_path.unlink()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class TestViolationClass:
|
|
352
|
+
"""Test Violation dataclass."""
|
|
353
|
+
|
|
354
|
+
def test_violation_creation(self) -> None:
|
|
355
|
+
"""Violation should store file, line, message."""
|
|
356
|
+
v = Violation("test.py", 42, "Test message")
|
|
357
|
+
assert v.file == "test.py"
|
|
358
|
+
assert v.line == 42
|
|
359
|
+
assert v.message == "Test message"
|
|
360
|
+
|
|
361
|
+
def test_violation_str_format(self) -> None:
|
|
362
|
+
"""Violation should format as file:line: message."""
|
|
363
|
+
v = Violation("test.py", 42, "Test message")
|
|
364
|
+
assert str(v) == "test.py:42: Test message"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class TestAutoFix:
|
|
368
|
+
"""Test auto-fix capabilities."""
|
|
369
|
+
|
|
370
|
+
def test_fix_empty_line_after_decorator(self) -> None:
|
|
371
|
+
"""Auto-fix should remove blank line between decorator and function."""
|
|
372
|
+
code = '''@decorator
|
|
373
|
+
|
|
374
|
+
def foo():
|
|
375
|
+
pass
|
|
376
|
+
'''
|
|
377
|
+
expected = '''@decorator
|
|
378
|
+
def foo():
|
|
379
|
+
pass
|
|
380
|
+
'''
|
|
381
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
|
382
|
+
temp_file.write(code)
|
|
383
|
+
temp_path = Path(temp_file.name)
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
from python_style_checks import fix_file
|
|
387
|
+
fixed = fix_file(temp_path)
|
|
388
|
+
assert fixed is True
|
|
389
|
+
result = temp_path.read_text()
|
|
390
|
+
assert result.strip() == expected.strip()
|
|
391
|
+
finally:
|
|
392
|
+
temp_path.unlink()
|
|
393
|
+
|
|
394
|
+
def test_fix_multiple_blank_lines_between_functions(self) -> None:
|
|
395
|
+
"""Auto-fix should collapse multiple blank lines to single."""
|
|
396
|
+
code = '''def foo():
|
|
397
|
+
pass
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def bar():
|
|
401
|
+
pass
|
|
402
|
+
'''
|
|
403
|
+
expected = '''def foo():
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
def bar():
|
|
407
|
+
pass
|
|
408
|
+
'''
|
|
409
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
|
410
|
+
temp_file.write(code)
|
|
411
|
+
temp_path = Path(temp_file.name)
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
from python_style_checks import fix_file
|
|
415
|
+
fixed = fix_file(temp_path)
|
|
416
|
+
assert fixed is True
|
|
417
|
+
result = temp_path.read_text()
|
|
418
|
+
assert result.strip() == expected.strip()
|
|
419
|
+
finally:
|
|
420
|
+
temp_path.unlink()
|
|
421
|
+
|
|
422
|
+
def test_no_fix_needed_returns_false(self) -> None:
|
|
423
|
+
"""Auto-fix should return False when no fixes needed."""
|
|
424
|
+
code = '''def foo():
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
def bar():
|
|
428
|
+
pass
|
|
429
|
+
'''
|
|
430
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
|
431
|
+
temp_file.write(code)
|
|
432
|
+
temp_path = Path(temp_file.name)
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
from python_style_checks import fix_file
|
|
436
|
+
fixed = fix_file(temp_path)
|
|
437
|
+
assert fixed is False
|
|
438
|
+
finally:
|
|
439
|
+
temp_path.unlink()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Tests for React class component validator."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from react_checks import check_no_class_components, Violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_class_component_extends_component_should_fail(tmp_path: Path) -> None:
|
|
9
|
+
"""Class component using Component should be detected."""
|
|
10
|
+
test_file = tmp_path / "BadComponent.tsx"
|
|
11
|
+
test_file.write_text("""import { Component } from 'react';
|
|
12
|
+
|
|
13
|
+
class UserProfile extends Component {
|
|
14
|
+
render() {
|
|
15
|
+
return <div>Profile</div>;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
""")
|
|
19
|
+
|
|
20
|
+
violations = check_no_class_components([str(test_file)])
|
|
21
|
+
|
|
22
|
+
assert len(violations) == 1
|
|
23
|
+
assert violations[0].file == str(test_file)
|
|
24
|
+
assert violations[0].line == 3
|
|
25
|
+
assert "class component" in violations[0].message.lower()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_class_component_extends_react_component_should_fail(tmp_path: Path) -> None:
|
|
29
|
+
"""Class component using React.Component should be detected."""
|
|
30
|
+
test_file = tmp_path / "BadComponent.tsx"
|
|
31
|
+
test_file.write_text("""import React from 'react';
|
|
32
|
+
|
|
33
|
+
class TaskList extends React.Component {
|
|
34
|
+
render() {
|
|
35
|
+
return <ul>Tasks</ul>;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
""")
|
|
39
|
+
|
|
40
|
+
violations = check_no_class_components([str(test_file)])
|
|
41
|
+
|
|
42
|
+
assert len(violations) == 1
|
|
43
|
+
assert violations[0].file == str(test_file)
|
|
44
|
+
assert violations[0].line == 3
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_class_component_extends_purecomponent_should_fail(tmp_path: Path) -> None:
|
|
48
|
+
"""Class component using PureComponent should be detected."""
|
|
49
|
+
test_file = tmp_path / "BadPureComponent.tsx"
|
|
50
|
+
test_file.write_text("""import { PureComponent } from 'react';
|
|
51
|
+
|
|
52
|
+
class OptimizedList extends PureComponent {
|
|
53
|
+
render() {
|
|
54
|
+
return <ul>Items</ul>;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
""")
|
|
58
|
+
|
|
59
|
+
violations = check_no_class_components([str(test_file)])
|
|
60
|
+
|
|
61
|
+
assert len(violations) == 1
|
|
62
|
+
assert violations[0].file == str(test_file)
|
|
63
|
+
assert violations[0].line == 3
|
|
64
|
+
assert "class component" in violations[0].message.lower()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_class_component_extends_react_purecomponent_should_fail(tmp_path: Path) -> None:
|
|
68
|
+
"""Class component using React.PureComponent should be detected."""
|
|
69
|
+
test_file = tmp_path / "BadReactPureComponent.tsx"
|
|
70
|
+
test_file.write_text("""import React from 'react';
|
|
71
|
+
|
|
72
|
+
class Dashboard extends React.PureComponent {
|
|
73
|
+
render() {
|
|
74
|
+
return <div>Dashboard</div>;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
""")
|
|
78
|
+
|
|
79
|
+
violations = check_no_class_components([str(test_file)])
|
|
80
|
+
|
|
81
|
+
assert len(violations) == 1
|
|
82
|
+
assert violations[0].file == str(test_file)
|
|
83
|
+
assert violations[0].line == 3
|
|
84
|
+
assert "class component" in violations[0].message.lower()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_functional_component_should_pass(tmp_path: Path) -> None:
|
|
88
|
+
"""Functional components should not trigger violations."""
|
|
89
|
+
test_file = tmp_path / "GoodComponent.tsx"
|
|
90
|
+
test_file.write_text("""import React from 'react';
|
|
91
|
+
|
|
92
|
+
function UserProfile() {
|
|
93
|
+
return <div>Profile</div>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const TaskList: React.FC = () => {
|
|
97
|
+
return <ul>Tasks</ul>;
|
|
98
|
+
};
|
|
99
|
+
""")
|
|
100
|
+
|
|
101
|
+
violations = check_no_class_components([str(test_file)])
|
|
102
|
+
|
|
103
|
+
assert len(violations) == 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_error_boundary_class_should_pass(tmp_path: Path) -> None:
|
|
107
|
+
"""Error boundary classes should be allowed (documented exception)."""
|
|
108
|
+
test_file = tmp_path / "ErrorBoundary.tsx"
|
|
109
|
+
test_file.write_text("""import { Component, ErrorInfo, ReactNode } from 'react';
|
|
110
|
+
|
|
111
|
+
interface Props {
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface State {
|
|
116
|
+
hasError: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class ErrorBoundary extends Component<Props, State> {
|
|
120
|
+
state = { hasError: false };
|
|
121
|
+
|
|
122
|
+
static getDerivedStateFromError(): State {
|
|
123
|
+
return { hasError: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
|
127
|
+
console.error('Error caught:', error, errorInfo);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
render() {
|
|
131
|
+
if (this.state.hasError) {
|
|
132
|
+
return <h1>Something went wrong.</h1>;
|
|
133
|
+
}
|
|
134
|
+
return this.props.children;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
""")
|
|
138
|
+
|
|
139
|
+
violations = check_no_class_components([str(test_file)])
|
|
140
|
+
|
|
141
|
+
assert len(violations) == 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_multiple_files_with_mixed_violations(tmp_path: Path) -> None:
|
|
145
|
+
"""Should detect violations across multiple files."""
|
|
146
|
+
good_file = tmp_path / "GoodComponent.tsx"
|
|
147
|
+
good_file.write_text("""function MyComponent() {
|
|
148
|
+
return <div>Good</div>;
|
|
149
|
+
}
|
|
150
|
+
""")
|
|
151
|
+
|
|
152
|
+
bad_file1 = tmp_path / "BadComponent1.tsx"
|
|
153
|
+
bad_file1.write_text("""import { Component } from 'react';
|
|
154
|
+
|
|
155
|
+
class Bad1 extends Component {
|
|
156
|
+
render() { return <div>Bad</div>; }
|
|
157
|
+
}
|
|
158
|
+
""")
|
|
159
|
+
|
|
160
|
+
bad_file2 = tmp_path / "BadComponent2.tsx"
|
|
161
|
+
bad_file2.write_text("""import React from 'react';
|
|
162
|
+
|
|
163
|
+
class Bad2 extends React.Component {
|
|
164
|
+
render() { return <div>Bad</div>; }
|
|
165
|
+
}
|
|
166
|
+
""")
|
|
167
|
+
|
|
168
|
+
violations = check_no_class_components([
|
|
169
|
+
str(good_file),
|
|
170
|
+
str(bad_file1),
|
|
171
|
+
str(bad_file2)
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
assert len(violations) == 2
|
|
175
|
+
assert any(str(bad_file1) in v.file for v in violations)
|
|
176
|
+
assert any(str(bad_file2) in v.file for v in violations)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_non_react_class_should_pass(tmp_path: Path) -> None:
|
|
180
|
+
"""Regular TypeScript classes should not trigger violations."""
|
|
181
|
+
test_file = tmp_path / "RegularClass.ts"
|
|
182
|
+
test_file.write_text("""class UserService {
|
|
183
|
+
fetchUser(id: string): Promise<User> {
|
|
184
|
+
return fetch(`/api/users/${id}`).then(r => r.json());
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class TaskManager extends EventEmitter {
|
|
189
|
+
private tasks: Task[] = [];
|
|
190
|
+
}
|
|
191
|
+
""")
|
|
192
|
+
|
|
193
|
+
violations = check_no_class_components([str(test_file)])
|
|
194
|
+
|
|
195
|
+
assert len(violations) == 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_jsx_file_with_class_component_should_fail(tmp_path: Path) -> None:
|
|
199
|
+
"""Should detect class components in .jsx files too."""
|
|
200
|
+
test_file = tmp_path / "OldComponent.jsx"
|
|
201
|
+
test_file.write_text("""const React = require('react');
|
|
202
|
+
|
|
203
|
+
class OldComponent extends React.Component {
|
|
204
|
+
render() {
|
|
205
|
+
return <div>Old style</div>;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
""")
|
|
209
|
+
|
|
210
|
+
violations = check_no_class_components([str(test_file)])
|
|
211
|
+
|
|
212
|
+
assert len(violations) == 1
|
|
213
|
+
assert violations[0].file == str(test_file)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
============================= test session starts =============================
|
|
2
|
+
platform -- Python X.Y.Z, pytest-X.Y.Z -- python
|
|
3
|
+
cachedir: .pytest_cache
|
|
4
|
+
benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
|
|
5
|
+
rootdir: hooks/validators
|
|
6
|
+
plugins: anyio-4.9.0, asyncio-1.1.0, benchmark-5.1.0, cov-6.2.1, mock-3.14.1
|
|
7
|
+
asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
|
|
8
|
+
collecting ... collected 88 items / 2 errors
|
|
9
|
+
|
|
10
|
+
=================================== ERRORS ====================================
|
|
11
|
+
___ ERROR collecting test_files/skip_decorators/test_04_skip_with_parens.py ___
|
|
12
|
+
test_files\skip_decorators\test_04_skip_with_parens.py:6: in <module>
|
|
13
|
+
@skip()
|
|
14
|
+
^^^^^^
|
|
15
|
+
E TypeError: skip() missing 1 required positional argument: 'reason'
|
|
16
|
+
_____________ ERROR collecting test_files/test_async_functions.py _____________
|
|
17
|
+
test_files\test_async_functions.py:11: in <module>
|
|
18
|
+
@asyncio.coroutine
|
|
19
|
+
^^^^^^^^^^^^^^^^^
|
|
20
|
+
E AttributeError: module 'asyncio' has no attribute 'coroutine'. Did you mean: 'coroutines'?
|
|
21
|
+
=========================== short test summary info ===========================
|
|
22
|
+
ERROR test_files/skip_decorators/test_04_skip_with_parens.py - TypeError: ski...
|
|
23
|
+
ERROR test_files/test_async_functions.py - AttributeError: module 'asyncio' h...
|
|
24
|
+
!!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!!
|
|
25
|
+
============================== 2 errors in 0.14s ==============================
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tests for ruff integration module."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from ruff_integration import RuffResult, check_ruff_available, run_ruff_check
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_ruff_result_dataclass() -> None:
|
|
10
|
+
"""Test RuffResult dataclass creation."""
|
|
11
|
+
result = RuffResult(passed=True, output="test", fixed_count=0)
|
|
12
|
+
assert result.passed is True
|
|
13
|
+
assert result.output == "test"
|
|
14
|
+
assert result.fixed_count == 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_check_ruff_available_returns_false_when_not_installed() -> None:
|
|
18
|
+
"""Test that check_ruff_available returns False when ruff not found."""
|
|
19
|
+
with patch("subprocess.run", side_effect=FileNotFoundError):
|
|
20
|
+
assert check_ruff_available() is False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_run_ruff_check_returns_passed_for_empty_files() -> None:
|
|
24
|
+
"""Test that run_ruff_check passes with no files."""
|
|
25
|
+
result = run_ruff_check([])
|
|
26
|
+
assert result.passed is True
|
|
27
|
+
assert "No files" in result.output
|