@pilllesss/yorn 1.0.182 → 1.0.183
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.
Potentially problematic release.
This version of @pilllesss/yorn might be problematic. Click here for more details.
- package/README.md +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/skills/code-review/LICENSE +21 -0
- package/dist/skills/code-review/SKILL.md +233 -0
- package/dist/skills/code-review/assets/pr-review-template.md +137 -0
- package/dist/skills/code-review/assets/review-checklist.md +123 -0
- package/dist/skills/code-review/reference/angular.md +768 -0
- package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/dist/skills/code-review/reference/c.md +890 -0
- package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
- package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/dist/skills/code-review/reference/cpp.md +893 -0
- package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/dist/skills/code-review/reference/csharp.md +525 -0
- package/dist/skills/code-review/reference/css-less-sass.md +661 -0
- package/dist/skills/code-review/reference/dart.md +670 -0
- package/dist/skills/code-review/reference/django.md +985 -0
- package/dist/skills/code-review/reference/fastapi.md +580 -0
- package/dist/skills/code-review/reference/go.md +993 -0
- package/dist/skills/code-review/reference/java.md +409 -0
- package/dist/skills/code-review/reference/java8.md +586 -0
- package/dist/skills/code-review/reference/kotlin.md +1018 -0
- package/dist/skills/code-review/reference/nestjs.md +593 -0
- package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
- package/dist/skills/code-review/reference/php.md +684 -0
- package/dist/skills/code-review/reference/python.md +1073 -0
- package/dist/skills/code-review/reference/qt.md +757 -0
- package/dist/skills/code-review/reference/react.md +871 -0
- package/dist/skills/code-review/reference/ruby.md +964 -0
- package/dist/skills/code-review/reference/rust.md +846 -0
- package/dist/skills/code-review/reference/security-review-guide.md +494 -0
- package/dist/skills/code-review/reference/svelte.md +1064 -0
- package/dist/skills/code-review/reference/swift.md +936 -0
- package/dist/skills/code-review/reference/typescript.md +1016 -0
- package/dist/skills/code-review/reference/vue.md +924 -0
- package/dist/skills/code-review/reference/zig.md +440 -0
- package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
- package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
- package/dist/yorn.cjs +628 -628
- package/package.json +2 -2
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for pr-analyzer.py (stdlib unittest, no extra deps)."""
|
|
3
|
+
|
|
4
|
+
import importlib.util
|
|
5
|
+
import os
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
# The script has a hyphen in its name, so load it by path.
|
|
9
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
_spec = importlib.util.spec_from_file_location(
|
|
11
|
+
'pr_analyzer', os.path.join(_HERE, 'pr-analyzer.py')
|
|
12
|
+
)
|
|
13
|
+
pr_analyzer = importlib.util.module_from_spec(_spec)
|
|
14
|
+
_spec.loader.exec_module(pr_analyzer)
|
|
15
|
+
|
|
16
|
+
# Convenient aliases
|
|
17
|
+
FileStats = pr_analyzer.FileStats
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ═══════════════════════════════════════════════════════════════
|
|
21
|
+
# parse_diff — filename extraction (existing tests)
|
|
22
|
+
# ═══════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
class ParseDiffFilenameTest(unittest.TestCase):
|
|
25
|
+
def test_lib_prefixed_path(self):
|
|
26
|
+
# "lib/" embeds a literal "b/" that the old regex swallowed.
|
|
27
|
+
diff = (
|
|
28
|
+
"diff --git a/lib/foo.py b/lib/foo.py\n"
|
|
29
|
+
"index 1234567..89abcde 100644\n"
|
|
30
|
+
"--- a/lib/foo.py\n"
|
|
31
|
+
"+++ b/lib/foo.py\n"
|
|
32
|
+
"@@ -1,2 +1,3 @@\n"
|
|
33
|
+
" unchanged\n"
|
|
34
|
+
"+added line\n"
|
|
35
|
+
"-removed line\n"
|
|
36
|
+
)
|
|
37
|
+
files = pr_analyzer.parse_diff(diff)
|
|
38
|
+
self.assertEqual(len(files), 1)
|
|
39
|
+
self.assertEqual(files[0].filename, 'lib/foo.py')
|
|
40
|
+
self.assertEqual(files[0].additions, 1)
|
|
41
|
+
self.assertEqual(files[0].deletions, 1)
|
|
42
|
+
|
|
43
|
+
def test_normal_path(self):
|
|
44
|
+
diff = (
|
|
45
|
+
"diff --git a/src/main.py b/src/main.py\n"
|
|
46
|
+
"index 1111111..2222222 100644\n"
|
|
47
|
+
"--- a/src/main.py\n"
|
|
48
|
+
"+++ b/src/main.py\n"
|
|
49
|
+
"@@ -0,0 +1 @@\n"
|
|
50
|
+
"+print('hi')\n"
|
|
51
|
+
)
|
|
52
|
+
files = pr_analyzer.parse_diff(diff)
|
|
53
|
+
self.assertEqual(len(files), 1)
|
|
54
|
+
self.assertEqual(files[0].filename, 'src/main.py')
|
|
55
|
+
|
|
56
|
+
def test_other_embedded_b_slash_prefixes(self):
|
|
57
|
+
# web/ and db/ also contain a literal "b/".
|
|
58
|
+
diff = (
|
|
59
|
+
"diff --git a/web/x.js b/web/x.js\n"
|
|
60
|
+
"+++ b/web/x.js\n"
|
|
61
|
+
"+console.log(1)\n"
|
|
62
|
+
"diff --git a/db/y.sql b/db/y.sql\n"
|
|
63
|
+
"+++ b/db/y.sql\n"
|
|
64
|
+
"+SELECT 1;\n"
|
|
65
|
+
)
|
|
66
|
+
files = pr_analyzer.parse_diff(diff)
|
|
67
|
+
self.assertEqual([f.filename for f in files], ['web/x.js', 'db/y.sql'])
|
|
68
|
+
|
|
69
|
+
def test_rename_falls_back_to_b_side(self):
|
|
70
|
+
diff = (
|
|
71
|
+
"diff --git a/old/name.py b/new/name.py\n"
|
|
72
|
+
"similarity index 100%\n"
|
|
73
|
+
"rename from old/name.py\n"
|
|
74
|
+
"rename to new/name.py\n"
|
|
75
|
+
)
|
|
76
|
+
files = pr_analyzer.parse_diff(diff)
|
|
77
|
+
self.assertEqual(len(files), 1)
|
|
78
|
+
self.assertEqual(files[0].filename, 'new/name.py')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ═══════════════════════════════════════════════════════════════
|
|
82
|
+
# detect_language
|
|
83
|
+
# ═══════════════════════════════════════════════════════════════
|
|
84
|
+
|
|
85
|
+
class DetectLanguageTest(unittest.TestCase):
|
|
86
|
+
def test_common_extensions(self):
|
|
87
|
+
cases = {
|
|
88
|
+
'app.py': 'Python',
|
|
89
|
+
'index.ts': 'TypeScript',
|
|
90
|
+
'main.rs': 'Rust',
|
|
91
|
+
'handler.go': 'Go',
|
|
92
|
+
'App.java': 'Java',
|
|
93
|
+
'Activity.kt': 'Kotlin',
|
|
94
|
+
'ViewController.swift': 'Swift',
|
|
95
|
+
'app.tsx': 'TypeScript/React',
|
|
96
|
+
'style.css': 'CSS',
|
|
97
|
+
'query.sql': 'SQL',
|
|
98
|
+
}
|
|
99
|
+
for filename, expected in cases.items():
|
|
100
|
+
with self.subTest(filename=filename):
|
|
101
|
+
self.assertEqual(pr_analyzer.detect_language(filename), expected)
|
|
102
|
+
|
|
103
|
+
def test_unknown_extension(self):
|
|
104
|
+
self.assertEqual(pr_analyzer.detect_language('data.xyz'), 'unknown')
|
|
105
|
+
self.assertEqual(pr_analyzer.detect_language('Makefile'), 'unknown')
|
|
106
|
+
|
|
107
|
+
def test_cpp_variants(self):
|
|
108
|
+
for ext in ('.cpp', '.hpp', '.cc', '.cxx', '.hh', '.hxx'):
|
|
109
|
+
with self.subTest(ext=ext):
|
|
110
|
+
self.assertEqual(pr_analyzer.detect_language(f'file{ext}'), 'C++')
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ═══════════════════════════════════════════════════════════════
|
|
114
|
+
# is_test_file
|
|
115
|
+
# ═══════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
class IsTestFileTest(unittest.TestCase):
|
|
118
|
+
def test_python_test_prefix(self):
|
|
119
|
+
self.assertTrue(pr_analyzer.is_test_file('tests/test_handler.py'))
|
|
120
|
+
self.assertTrue(pr_analyzer.is_test_file('test_utils.py'))
|
|
121
|
+
|
|
122
|
+
def test_python_test_suffix(self):
|
|
123
|
+
self.assertTrue(pr_analyzer.is_test_file('handler_test.py'))
|
|
124
|
+
|
|
125
|
+
def test_rust_test_suffix(self):
|
|
126
|
+
self.assertTrue(pr_analyzer.is_test_file('src/my_module_test.rs'))
|
|
127
|
+
self.assertTrue(pr_analyzer.is_test_file('parser_test.rs'))
|
|
128
|
+
|
|
129
|
+
def test_go_test_suffix(self):
|
|
130
|
+
self.assertTrue(pr_analyzer.is_test_file('handler_test.go'))
|
|
131
|
+
self.assertTrue(pr_analyzer.is_test_file('pkg/auth_test.go'))
|
|
132
|
+
|
|
133
|
+
def test_js_ts_test_and_spec(self):
|
|
134
|
+
self.assertTrue(pr_analyzer.is_test_file('handler.test.ts'))
|
|
135
|
+
self.assertTrue(pr_analyzer.is_test_file('utils.spec.js'))
|
|
136
|
+
self.assertTrue(pr_analyzer.is_test_file('App.test.tsx'))
|
|
137
|
+
|
|
138
|
+
def test_tests_directory(self):
|
|
139
|
+
self.assertTrue(pr_analyzer.is_test_file('tests/conftest.py'))
|
|
140
|
+
self.assertTrue(pr_analyzer.is_test_file('test/helpers.js'))
|
|
141
|
+
|
|
142
|
+
def test_dunder_tests_directory(self):
|
|
143
|
+
self.assertTrue(pr_analyzer.is_test_file('__tests__/Button.test.tsx'))
|
|
144
|
+
|
|
145
|
+
def test_non_test_files_rejected(self):
|
|
146
|
+
self.assertFalse(pr_analyzer.is_test_file('handler.go'))
|
|
147
|
+
self.assertFalse(pr_analyzer.is_test_file('module.rs'))
|
|
148
|
+
self.assertFalse(pr_analyzer.is_test_file('src/utils.py'))
|
|
149
|
+
self.assertFalse(pr_analyzer.is_test_file('lib/parser.js'))
|
|
150
|
+
self.assertFalse(pr_analyzer.is_test_file('contest.py'))
|
|
151
|
+
|
|
152
|
+
def test_test_substring_not_matched(self):
|
|
153
|
+
"""Files containing 'test_' as substring must NOT be flagged."""
|
|
154
|
+
self.assertFalse(pr_analyzer.is_test_file('latest_report.py'))
|
|
155
|
+
self.assertFalse(pr_analyzer.is_test_file('contest_utils.py'))
|
|
156
|
+
self.assertFalse(pr_analyzer.is_test_file('src/latest_handler.py'))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ═══════════════════════════════════════════════════════════════
|
|
160
|
+
# is_config_file
|
|
161
|
+
# ═══════════════════════════════════════════════════════════════
|
|
162
|
+
|
|
163
|
+
class IsConfigFileTest(unittest.TestCase):
|
|
164
|
+
def test_known_json_configs(self):
|
|
165
|
+
self.assertTrue(pr_analyzer.is_config_file('package.json'))
|
|
166
|
+
self.assertTrue(pr_analyzer.is_config_file('tsconfig.json'))
|
|
167
|
+
self.assertTrue(pr_analyzer.is_config_file('.eslintrc.json'))
|
|
168
|
+
|
|
169
|
+
def test_known_yaml_configs(self):
|
|
170
|
+
self.assertTrue(pr_analyzer.is_config_file('docker-compose.yml'))
|
|
171
|
+
self.assertTrue(pr_analyzer.is_config_file('.github/workflows/ci.yml'))
|
|
172
|
+
self.assertTrue(pr_analyzer.is_config_file('.prettierrc.yml'))
|
|
173
|
+
|
|
174
|
+
def test_known_toml_configs(self):
|
|
175
|
+
self.assertTrue(pr_analyzer.is_config_file('Cargo.toml'))
|
|
176
|
+
self.assertTrue(pr_analyzer.is_config_file('pyproject.toml'))
|
|
177
|
+
|
|
178
|
+
def test_env_files(self):
|
|
179
|
+
self.assertTrue(pr_analyzer.is_config_file('.env'))
|
|
180
|
+
self.assertTrue(pr_analyzer.is_config_file('.env.local'))
|
|
181
|
+
self.assertTrue(pr_analyzer.is_config_file('.env.production'))
|
|
182
|
+
|
|
183
|
+
def test_config_in_filename(self):
|
|
184
|
+
self.assertTrue(pr_analyzer.is_config_file('app.config.ts'))
|
|
185
|
+
self.assertTrue(pr_analyzer.is_config_file('database_config.yml'))
|
|
186
|
+
|
|
187
|
+
def test_data_files_rejected(self):
|
|
188
|
+
"""Data files must NOT be flagged as config."""
|
|
189
|
+
self.assertFalse(pr_analyzer.is_config_file('data.json'))
|
|
190
|
+
self.assertFalse(pr_analyzer.is_config_file('openapi.yaml'))
|
|
191
|
+
self.assertFalse(pr_analyzer.is_config_file('swagger.json'))
|
|
192
|
+
self.assertFalse(pr_analyzer.is_config_file('fixtures/sample.yml'))
|
|
193
|
+
self.assertFalse(pr_analyzer.is_config_file('translations.json'))
|
|
194
|
+
self.assertFalse(pr_analyzer.is_config_file('schema.toml'))
|
|
195
|
+
|
|
196
|
+
def test_config_directory(self):
|
|
197
|
+
self.assertTrue(pr_analyzer.is_config_file('config/settings.yaml'))
|
|
198
|
+
self.assertTrue(pr_analyzer.is_config_file('config/database.yml'))
|
|
199
|
+
self.assertTrue(pr_analyzer.is_config_file('src/config/settings.json'))
|
|
200
|
+
|
|
201
|
+
def test_source_files_rejected(self):
|
|
202
|
+
self.assertFalse(pr_analyzer.is_config_file('src/index.ts'))
|
|
203
|
+
self.assertFalse(pr_analyzer.is_config_file('lib/utils.py'))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ═══════════════════════════════════════════════════════════════
|
|
207
|
+
# calculate_complexity
|
|
208
|
+
# ═══════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
class CalculateComplexityTest(unittest.TestCase):
|
|
211
|
+
def test_empty_files(self):
|
|
212
|
+
self.assertEqual(pr_analyzer.calculate_complexity([]), 0.0)
|
|
213
|
+
|
|
214
|
+
def test_small_simple_change(self):
|
|
215
|
+
files = [FileStats(filename='app.py', additions=5, deletions=2,
|
|
216
|
+
language='Python')]
|
|
217
|
+
score = pr_analyzer.calculate_complexity(files)
|
|
218
|
+
self.assertLess(score, 0.3)
|
|
219
|
+
|
|
220
|
+
def test_large_multi_language_change(self):
|
|
221
|
+
files = [
|
|
222
|
+
FileStats(filename='app.py', additions=300, deletions=100, language='Python'),
|
|
223
|
+
FileStats(filename='main.rs', additions=200, deletions=50, language='Rust'),
|
|
224
|
+
FileStats(filename='index.ts', additions=150, deletions=80, language='TypeScript'),
|
|
225
|
+
FileStats(filename='handler.go', additions=100, deletions=30, language='Go'),
|
|
226
|
+
FileStats(filename='App.tsx', additions=50, deletions=20, language='TypeScript/React'),
|
|
227
|
+
]
|
|
228
|
+
score = pr_analyzer.calculate_complexity(files)
|
|
229
|
+
self.assertGreater(score, 0.5)
|
|
230
|
+
|
|
231
|
+
def test_test_heavy_change_is_lower(self):
|
|
232
|
+
"""Changes with high test ratio should have lower complexity."""
|
|
233
|
+
prod_files = [FileStats(filename='app.py', additions=100, deletions=50,
|
|
234
|
+
language='Python')]
|
|
235
|
+
test_files = [
|
|
236
|
+
FileStats(filename='app.py', additions=100, deletions=50,
|
|
237
|
+
language='Python'),
|
|
238
|
+
FileStats(filename='tests/test_app.py', additions=100, deletions=0,
|
|
239
|
+
language='Python', is_test=True),
|
|
240
|
+
]
|
|
241
|
+
score_prod = pr_analyzer.calculate_complexity(prod_files)
|
|
242
|
+
score_test = pr_analyzer.calculate_complexity(test_files)
|
|
243
|
+
self.assertLess(score_test, score_prod)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ═══════════════════════════════════════════════════════════════
|
|
247
|
+
# identify_risk_factors
|
|
248
|
+
# ═══════════════════════════════════════════════════════════════
|
|
249
|
+
|
|
250
|
+
class IdentifyRiskFactorsTest(unittest.TestCase):
|
|
251
|
+
def test_large_pr_flagged(self):
|
|
252
|
+
files = [FileStats(filename='big.py', additions=300, deletions=200,
|
|
253
|
+
language='Python')]
|
|
254
|
+
risks = pr_analyzer.identify_risk_factors(files)
|
|
255
|
+
self.assertTrue(any('Large PR' in r for r in risks))
|
|
256
|
+
|
|
257
|
+
def test_no_tests_flagged(self):
|
|
258
|
+
files = [FileStats(filename='app.py', additions=40, deletions=20,
|
|
259
|
+
language='Python')]
|
|
260
|
+
risks = pr_analyzer.identify_risk_factors(files)
|
|
261
|
+
self.assertTrue(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
|
|
262
|
+
|
|
263
|
+
def test_with_tests_not_flagged(self):
|
|
264
|
+
files = [
|
|
265
|
+
FileStats(filename='app.py', additions=40, deletions=20,
|
|
266
|
+
language='Python'),
|
|
267
|
+
FileStats(filename='tests/test_app.py', additions=30, deletions=0,
|
|
268
|
+
language='Python', is_test=True),
|
|
269
|
+
]
|
|
270
|
+
risks = pr_analyzer.identify_risk_factors(files)
|
|
271
|
+
self.assertFalse(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
|
|
272
|
+
|
|
273
|
+
def test_security_sensitive_file(self):
|
|
274
|
+
files = [FileStats(filename='src/auth/login.py', additions=10, deletions=5,
|
|
275
|
+
language='Python')]
|
|
276
|
+
risks = pr_analyzer.identify_risk_factors(files)
|
|
277
|
+
self.assertTrue(any('Security-sensitive' in r for r in risks))
|
|
278
|
+
|
|
279
|
+
def test_database_migration(self):
|
|
280
|
+
files = [FileStats(filename='migrations/001_init.sql', additions=20, deletions=0,
|
|
281
|
+
language='SQL')]
|
|
282
|
+
risks = pr_analyzer.identify_risk_factors(files)
|
|
283
|
+
self.assertTrue(any('Database' in r for r in risks))
|
|
284
|
+
|
|
285
|
+
def test_test_substring_files_still_flag_no_tests(self):
|
|
286
|
+
"""Files like latest_report.py must not suppress NO_TEST_CHANGES."""
|
|
287
|
+
files = [
|
|
288
|
+
FileStats(filename='latest_report.py', additions=40, deletions=20,
|
|
289
|
+
language='Python'),
|
|
290
|
+
FileStats(filename='contest_utils.py', additions=30, deletions=10,
|
|
291
|
+
language='Python'),
|
|
292
|
+
]
|
|
293
|
+
risks = pr_analyzer.identify_risk_factors(files)
|
|
294
|
+
self.assertTrue(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ═══════════════════════════════════════════════════════════════
|
|
298
|
+
# generate_suggestions
|
|
299
|
+
# ═══════════════════════════════════════════════════════════════
|
|
300
|
+
|
|
301
|
+
class GenerateSuggestionsTest(unittest.TestCase):
|
|
302
|
+
def test_returns_list(self):
|
|
303
|
+
files = [FileStats(filename='app.py', additions=10, deletions=5,
|
|
304
|
+
language='Python')]
|
|
305
|
+
result = pr_analyzer.generate_suggestions(files, 0.1, [])
|
|
306
|
+
self.assertIsInstance(result, list)
|
|
307
|
+
self.assertGreater(len(result), 0)
|
|
308
|
+
|
|
309
|
+
def test_large_pr_split_suggestion(self):
|
|
310
|
+
files = [FileStats(filename='big.py', additions=600, deletions=300,
|
|
311
|
+
language='Python')]
|
|
312
|
+
result = pr_analyzer.generate_suggestions(files, 0.3, [])
|
|
313
|
+
self.assertTrue(any('splitting' in s.lower() for s in result))
|
|
314
|
+
|
|
315
|
+
def test_no_tests_suggestion(self):
|
|
316
|
+
files = [FileStats(filename='app.py', additions=40, deletions=20,
|
|
317
|
+
language='Python')]
|
|
318
|
+
risks = [f"{pr_analyzer.RISK_NO_TESTS}: no tests"]
|
|
319
|
+
result = pr_analyzer.generate_suggestions(files, 0.2, risks)
|
|
320
|
+
self.assertTrue(any('test' in s.lower() for s in result))
|
|
321
|
+
|
|
322
|
+
def test_rust_suggestion(self):
|
|
323
|
+
files = [FileStats(filename='lib.rs', additions=10, deletions=5,
|
|
324
|
+
language='Rust')]
|
|
325
|
+
result = pr_analyzer.generate_suggestions(files, 0.1, [])
|
|
326
|
+
self.assertTrue(any('unwrap' in s.lower() for s in result))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ═══════════════════════════════════════════════════════════════
|
|
330
|
+
# analyze_pr — end-to-end integration
|
|
331
|
+
# ═══════════════════════════════════════════════════════════════
|
|
332
|
+
|
|
333
|
+
class AnalyzePRTest(unittest.TestCase):
|
|
334
|
+
def test_end_to_end(self):
|
|
335
|
+
diff = (
|
|
336
|
+
"diff --git a/src/app.py b/src/app.py\n"
|
|
337
|
+
"index 1111111..2222222 100644\n"
|
|
338
|
+
"--- a/src/app.py\n"
|
|
339
|
+
"+++ b/src/app.py\n"
|
|
340
|
+
"@@ -1,3 +1,5 @@\n"
|
|
341
|
+
" import os\n"
|
|
342
|
+
"+import sys\n"
|
|
343
|
+
"+import json\n"
|
|
344
|
+
" def main():\n"
|
|
345
|
+
"+ print('hello')\n"
|
|
346
|
+
"+ return 0\n"
|
|
347
|
+
"diff --git a/tests/test_app.py b/tests/test_app.py\n"
|
|
348
|
+
"new file mode 100644\n"
|
|
349
|
+
"--- /dev/null\n"
|
|
350
|
+
"+++ b/tests/test_app.py\n"
|
|
351
|
+
"@@ -0,0 +1,3 @@\n"
|
|
352
|
+
"+from app import main\n"
|
|
353
|
+
"+def test_main():\n"
|
|
354
|
+
"+ assert main() == 0\n"
|
|
355
|
+
)
|
|
356
|
+
analysis = pr_analyzer.analyze_pr(diff)
|
|
357
|
+
|
|
358
|
+
self.assertEqual(analysis.total_files, 2)
|
|
359
|
+
self.assertEqual(analysis.total_additions, 7) # 4 + 3
|
|
360
|
+
self.assertEqual(analysis.total_deletions, 0)
|
|
361
|
+
self.assertGreater(len(analysis.suggestions), 0)
|
|
362
|
+
self.assertIn('XS (Extra Small)', analysis.size_category)
|
|
363
|
+
|
|
364
|
+
# Verify test file was detected
|
|
365
|
+
test_file = [f for f in analysis.files if 'test' in f.filename][0]
|
|
366
|
+
self.assertTrue(test_file.is_test)
|
|
367
|
+
|
|
368
|
+
# Verify no "NO_TEST_CHANGES" risk since tests are present
|
|
369
|
+
self.assertFalse(
|
|
370
|
+
any(pr_analyzer.RISK_NO_TESTS in r for r in analysis.risk_factors)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def test_empty_diff(self):
|
|
374
|
+
analysis = pr_analyzer.analyze_pr("")
|
|
375
|
+
self.assertEqual(analysis.total_files, 0)
|
|
376
|
+
self.assertEqual(analysis.complexity_score, 0.0)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
if __name__ == '__main__':
|
|
380
|
+
unittest.main()
|