skylos 1.1.12__py3-none-any.whl → 1.2.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.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/analyzer.py +93 -133
- skylos/cli.py +20 -2
- skylos/constants.py +34 -27
- skylos/framework_aware.py +158 -0
- skylos/test_aware.py +66 -0
- skylos/visitor.py +28 -3
- {skylos-1.1.12.dist-info → skylos-1.2.0.dist-info}/METADATA +1 -1
- skylos-1.2.0.dist-info/RECORD +32 -0
- test/test_analyzer.py +223 -195
- test/test_changes_analyzer.py +149 -0
- test/test_constants.py +348 -0
- test/test_framework_aware.py +372 -0
- test/test_test_aware.py +328 -0
- test/test_visitor.py +0 -10
- skylos-1.1.12.dist-info/RECORD +0 -26
- {skylos-1.1.12.dist-info → skylos-1.2.0.dist-info}/WHEEL +0 -0
- {skylos-1.1.12.dist-info → skylos-1.2.0.dist-info}/entry_points.txt +0 -0
- {skylos-1.1.12.dist-info → skylos-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import pytest
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import Mock, patch
|
|
6
|
+
import fnmatch
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from skylos.framework_aware import (
|
|
10
|
+
FrameworkAwareVisitor,
|
|
11
|
+
detect_framework_usage,
|
|
12
|
+
FRAMEWORK_DECORATORS,
|
|
13
|
+
FRAMEWORK_FUNCTIONS,
|
|
14
|
+
FRAMEWORK_IMPORTS
|
|
15
|
+
)
|
|
16
|
+
except ImportError:
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
20
|
+
from skylos.framework_aware import (
|
|
21
|
+
FrameworkAwareVisitor,
|
|
22
|
+
detect_framework_usage,
|
|
23
|
+
FRAMEWORK_DECORATORS,
|
|
24
|
+
FRAMEWORK_FUNCTIONS,
|
|
25
|
+
FRAMEWORK_IMPORTS
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
class TestFrameworkAwareVisitor:
|
|
29
|
+
|
|
30
|
+
def test_init_default(self):
|
|
31
|
+
"""Test default initialization"""
|
|
32
|
+
visitor = FrameworkAwareVisitor()
|
|
33
|
+
assert visitor.is_framework_file == False
|
|
34
|
+
assert visitor.framework_decorated_lines == set()
|
|
35
|
+
assert visitor.detected_frameworks == set()
|
|
36
|
+
|
|
37
|
+
def test_flask_import_detection(self):
|
|
38
|
+
"""Test Flask import detection"""
|
|
39
|
+
code = """
|
|
40
|
+
import flask
|
|
41
|
+
from flask import Flask, request
|
|
42
|
+
"""
|
|
43
|
+
tree = ast.parse(code)
|
|
44
|
+
visitor = FrameworkAwareVisitor()
|
|
45
|
+
visitor.visit(tree)
|
|
46
|
+
|
|
47
|
+
assert visitor.is_framework_file == True
|
|
48
|
+
assert 'flask' in visitor.detected_frameworks
|
|
49
|
+
|
|
50
|
+
def test_fastapi_import_detection(self):
|
|
51
|
+
"""Test FastAPI import detection"""
|
|
52
|
+
code = """
|
|
53
|
+
from fastapi import FastAPI
|
|
54
|
+
import fastapi
|
|
55
|
+
"""
|
|
56
|
+
tree = ast.parse(code)
|
|
57
|
+
visitor = FrameworkAwareVisitor()
|
|
58
|
+
visitor.visit(tree)
|
|
59
|
+
|
|
60
|
+
assert visitor.is_framework_file == True
|
|
61
|
+
assert 'fastapi' in visitor.detected_frameworks
|
|
62
|
+
|
|
63
|
+
def test_django_import_detection(self):
|
|
64
|
+
"""Test Django import detection"""
|
|
65
|
+
code = """
|
|
66
|
+
from django.http import HttpResponse
|
|
67
|
+
from django.views import View
|
|
68
|
+
"""
|
|
69
|
+
tree = ast.parse(code)
|
|
70
|
+
visitor = FrameworkAwareVisitor()
|
|
71
|
+
visitor.visit(tree)
|
|
72
|
+
|
|
73
|
+
assert visitor.is_framework_file == True
|
|
74
|
+
assert 'django' in visitor.detected_frameworks
|
|
75
|
+
|
|
76
|
+
def test_flask_route_decorator_detection(self):
|
|
77
|
+
"""Test Flask route decorator detection"""
|
|
78
|
+
code = """
|
|
79
|
+
@app.route('/api/users')
|
|
80
|
+
def get_users():
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
@app.post('/api/users')
|
|
84
|
+
def create_user():
|
|
85
|
+
return {}
|
|
86
|
+
"""
|
|
87
|
+
tree = ast.parse(code)
|
|
88
|
+
visitor = FrameworkAwareVisitor()
|
|
89
|
+
visitor.visit(tree)
|
|
90
|
+
|
|
91
|
+
assert visitor.is_framework_file == True
|
|
92
|
+
assert 3 in visitor.framework_decorated_lines # get_users function
|
|
93
|
+
assert 7 in visitor.framework_decorated_lines # create_user function
|
|
94
|
+
|
|
95
|
+
def test_fastapi_router_decorator_detection(self):
|
|
96
|
+
"""Test FastAPI router decorator detection"""
|
|
97
|
+
code = """
|
|
98
|
+
@router.get('/items')
|
|
99
|
+
async def read_items():
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
@router.post('/items')
|
|
103
|
+
async def create_item():
|
|
104
|
+
return {}
|
|
105
|
+
"""
|
|
106
|
+
tree = ast.parse(code)
|
|
107
|
+
visitor = FrameworkAwareVisitor()
|
|
108
|
+
visitor.visit(tree)
|
|
109
|
+
|
|
110
|
+
assert visitor.is_framework_file == True
|
|
111
|
+
assert 3 in visitor.framework_decorated_lines
|
|
112
|
+
assert 7 in visitor.framework_decorated_lines
|
|
113
|
+
|
|
114
|
+
def test_django_decorator_detection(self):
|
|
115
|
+
"""Test Django decorator detection"""
|
|
116
|
+
code = """
|
|
117
|
+
@login_required
|
|
118
|
+
def protected_view(request):
|
|
119
|
+
return HttpResponse("Protected")
|
|
120
|
+
|
|
121
|
+
@permission_required('auth.add_user')
|
|
122
|
+
def admin_view(request):
|
|
123
|
+
return HttpResponse("Admin")
|
|
124
|
+
"""
|
|
125
|
+
tree = ast.parse(code)
|
|
126
|
+
visitor = FrameworkAwareVisitor()
|
|
127
|
+
visitor.visit(tree)
|
|
128
|
+
|
|
129
|
+
assert visitor.is_framework_file == True
|
|
130
|
+
assert 3 in visitor.framework_decorated_lines
|
|
131
|
+
assert 7 in visitor.framework_decorated_lines
|
|
132
|
+
|
|
133
|
+
def test_celery_task_decorator_detection(self):
|
|
134
|
+
"""Test Celery task decorator detection"""
|
|
135
|
+
code = """
|
|
136
|
+
@task
|
|
137
|
+
def background_task():
|
|
138
|
+
return "done"
|
|
139
|
+
|
|
140
|
+
@shared_task
|
|
141
|
+
def shared_background_task():
|
|
142
|
+
return "shared done"
|
|
143
|
+
"""
|
|
144
|
+
tree = ast.parse(code)
|
|
145
|
+
visitor = FrameworkAwareVisitor()
|
|
146
|
+
visitor.visit(tree)
|
|
147
|
+
|
|
148
|
+
assert visitor.is_framework_file == True
|
|
149
|
+
assert 3 in visitor.framework_decorated_lines
|
|
150
|
+
assert 7 in visitor.framework_decorated_lines
|
|
151
|
+
|
|
152
|
+
def test_django_view_class_detection(self):
|
|
153
|
+
"""Test Django view class detection"""
|
|
154
|
+
code = """
|
|
155
|
+
from django import views
|
|
156
|
+
|
|
157
|
+
class UserView(View):
|
|
158
|
+
def get(self, request):
|
|
159
|
+
return HttpResponse("GET")
|
|
160
|
+
|
|
161
|
+
class UserViewSet(ViewSet):
|
|
162
|
+
def list(self, request):
|
|
163
|
+
return Response([])
|
|
164
|
+
"""
|
|
165
|
+
tree = ast.parse(code)
|
|
166
|
+
visitor = FrameworkAwareVisitor()
|
|
167
|
+
visitor.visit(tree)
|
|
168
|
+
|
|
169
|
+
assert visitor.is_framework_file == True
|
|
170
|
+
assert 4 in visitor.framework_decorated_lines
|
|
171
|
+
assert 8 in visitor.framework_decorated_lines
|
|
172
|
+
|
|
173
|
+
def test_django_model_methods_in_framework_file(self):
|
|
174
|
+
code = """
|
|
175
|
+
from django.db import models
|
|
176
|
+
|
|
177
|
+
class User(models.Model):
|
|
178
|
+
name = models.CharField(max_length=100)
|
|
179
|
+
|
|
180
|
+
def save(self, *args, **kwargs):
|
|
181
|
+
# custom save logic
|
|
182
|
+
super().save(*args, **kwargs)
|
|
183
|
+
|
|
184
|
+
def delete(self, *args, **kwargs):
|
|
185
|
+
# custom delete logic
|
|
186
|
+
super().delete(*args, **kwargs)
|
|
187
|
+
"""
|
|
188
|
+
tree = ast.parse(code)
|
|
189
|
+
visitor = FrameworkAwareVisitor()
|
|
190
|
+
visitor.visit(tree)
|
|
191
|
+
|
|
192
|
+
assert visitor.is_framework_file == True
|
|
193
|
+
assert 7 in visitor.framework_decorated_lines # save method
|
|
194
|
+
assert 11 in visitor.framework_decorated_lines # delete method
|
|
195
|
+
|
|
196
|
+
def test_framework_functions_not_detected_in_non_framework_file(self):
|
|
197
|
+
code = """
|
|
198
|
+
def save(self):
|
|
199
|
+
# regular save method, not Django
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
def get(self):
|
|
203
|
+
# regular get method, not Django view
|
|
204
|
+
pass
|
|
205
|
+
"""
|
|
206
|
+
tree = ast.parse(code)
|
|
207
|
+
visitor = FrameworkAwareVisitor()
|
|
208
|
+
visitor.visit(tree)
|
|
209
|
+
|
|
210
|
+
assert visitor.is_framework_file == False
|
|
211
|
+
assert visitor.framework_decorated_lines == set()
|
|
212
|
+
|
|
213
|
+
def test_multiple_decorators(self):
|
|
214
|
+
"""Test function with multiple decorators"""
|
|
215
|
+
code = """
|
|
216
|
+
@app.route('/users')
|
|
217
|
+
@login_required
|
|
218
|
+
@cache.cached(timeout=60)
|
|
219
|
+
def get_users():
|
|
220
|
+
return []
|
|
221
|
+
"""
|
|
222
|
+
tree = ast.parse(code)
|
|
223
|
+
visitor = FrameworkAwareVisitor()
|
|
224
|
+
visitor.visit(tree)
|
|
225
|
+
|
|
226
|
+
assert visitor.is_framework_file == True
|
|
227
|
+
assert 5 in visitor.framework_decorated_lines
|
|
228
|
+
|
|
229
|
+
def test_complex_decorator_patterns(self):
|
|
230
|
+
"""Test complex decorator patterns"""
|
|
231
|
+
code = """
|
|
232
|
+
@app.route('/api/v1/users/<int:user_id>', methods=['GET', 'POST'])
|
|
233
|
+
def user_endpoint(user_id):
|
|
234
|
+
return {}
|
|
235
|
+
|
|
236
|
+
@router.get('/items/{item_id}')
|
|
237
|
+
async def get_item(item_id: int):
|
|
238
|
+
return {}
|
|
239
|
+
"""
|
|
240
|
+
tree = ast.parse(code)
|
|
241
|
+
visitor = FrameworkAwareVisitor()
|
|
242
|
+
visitor.visit(tree)
|
|
243
|
+
|
|
244
|
+
assert visitor.is_framework_file == True
|
|
245
|
+
assert 3 in visitor.framework_decorated_lines
|
|
246
|
+
assert 7 in visitor.framework_decorated_lines
|
|
247
|
+
|
|
248
|
+
@patch('skylos.framework_aware.Path')
|
|
249
|
+
def test_file_content_framework_detection(self, mock_path):
|
|
250
|
+
"""Test framework detection from file content"""
|
|
251
|
+
mock_file = Mock()
|
|
252
|
+
mock_file.read_text.return_value = "from flask import Flask\napp = Flask(__name__)"
|
|
253
|
+
mock_path.return_value = mock_file
|
|
254
|
+
|
|
255
|
+
visitor = FrameworkAwareVisitor(filename="test.py")
|
|
256
|
+
|
|
257
|
+
assert visitor.is_framework_file == True
|
|
258
|
+
assert 'flask' in visitor.detected_frameworks
|
|
259
|
+
|
|
260
|
+
def test_normalize_decorator_name(self):
|
|
261
|
+
visitor = FrameworkAwareVisitor()
|
|
262
|
+
|
|
263
|
+
node = ast.parse("@decorator\ndef func(): pass").body[0].decorator_list[0]
|
|
264
|
+
result = visitor._normalize_decorator(node)
|
|
265
|
+
assert result == "@decorator"
|
|
266
|
+
|
|
267
|
+
node = ast.parse("@app.route\ndef func(): pass").body[0].decorator_list[0]
|
|
268
|
+
result = visitor._normalize_decorator(node)
|
|
269
|
+
assert result == "@app.route"
|
|
270
|
+
|
|
271
|
+
class TestDetectFrameworkUsage:
|
|
272
|
+
|
|
273
|
+
def test_framework_decorated_function_low_confidence(self):
|
|
274
|
+
mock_def = Mock()
|
|
275
|
+
mock_def.line = 10
|
|
276
|
+
mock_def.simple_name = "get_users"
|
|
277
|
+
mock_def.type = "function"
|
|
278
|
+
|
|
279
|
+
mock_visitor = Mock()
|
|
280
|
+
mock_visitor.framework_decorated_lines = {10}
|
|
281
|
+
mock_visitor.is_framework_file = True
|
|
282
|
+
|
|
283
|
+
confidence = detect_framework_usage(mock_def, visitor=mock_visitor)
|
|
284
|
+
assert confidence == 20
|
|
285
|
+
|
|
286
|
+
def test_framework_file_function_medium_confidence(self):
|
|
287
|
+
mock_def = Mock()
|
|
288
|
+
mock_def.line = 15
|
|
289
|
+
mock_def.simple_name = "helper_function"
|
|
290
|
+
mock_def.type = "function"
|
|
291
|
+
|
|
292
|
+
mock_visitor = Mock()
|
|
293
|
+
mock_visitor.framework_decorated_lines = set()
|
|
294
|
+
mock_visitor.is_framework_file = True
|
|
295
|
+
|
|
296
|
+
confidence = detect_framework_usage(mock_def, visitor=mock_visitor)
|
|
297
|
+
assert confidence == 40
|
|
298
|
+
|
|
299
|
+
def test_private_function_in_framework_file_no_confidence(self):
|
|
300
|
+
mock_def = Mock()
|
|
301
|
+
mock_def.line = 20
|
|
302
|
+
mock_def.simple_name = "_private_function"
|
|
303
|
+
mock_def.type = "function"
|
|
304
|
+
|
|
305
|
+
mock_visitor = Mock()
|
|
306
|
+
mock_visitor.framework_decorated_lines = set()
|
|
307
|
+
mock_visitor.is_framework_file = True
|
|
308
|
+
|
|
309
|
+
confidence = detect_framework_usage(mock_def, visitor=mock_visitor)
|
|
310
|
+
assert confidence is None
|
|
311
|
+
|
|
312
|
+
def test_non_framework_file_no_confidence(self):
|
|
313
|
+
mock_def = Mock()
|
|
314
|
+
mock_def.line = 25
|
|
315
|
+
mock_def.simple_name = "regular_function"
|
|
316
|
+
mock_def.type = "function"
|
|
317
|
+
|
|
318
|
+
mock_visitor = Mock()
|
|
319
|
+
mock_visitor.framework_decorated_lines = set()
|
|
320
|
+
mock_visitor.is_framework_file = False
|
|
321
|
+
|
|
322
|
+
confidence = detect_framework_usage(mock_def, visitor=mock_visitor)
|
|
323
|
+
assert confidence is None
|
|
324
|
+
|
|
325
|
+
def test_no_visitor_returns_none(self):
|
|
326
|
+
mock_def = Mock()
|
|
327
|
+
confidence = detect_framework_usage(mock_def, visitor=None)
|
|
328
|
+
assert confidence is None
|
|
329
|
+
|
|
330
|
+
def test_non_function_in_framework_file_no_confidence(self):
|
|
331
|
+
mock_def = Mock()
|
|
332
|
+
mock_def.line = 30
|
|
333
|
+
mock_def.simple_name = "my_variable"
|
|
334
|
+
mock_def.type = "variable"
|
|
335
|
+
|
|
336
|
+
mock_visitor = Mock()
|
|
337
|
+
mock_visitor.framework_decorated_lines = set()
|
|
338
|
+
mock_visitor.is_framework_file = True
|
|
339
|
+
|
|
340
|
+
confidence = detect_framework_usage(mock_def, visitor=mock_visitor)
|
|
341
|
+
assert confidence is None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class TestFrameworkPatterns:
|
|
345
|
+
|
|
346
|
+
def test_framework_decorators_list(self):
|
|
347
|
+
"""Test that FRAMEWORK_DECORATORS contains expected patterns"""
|
|
348
|
+
assert "@app.route" in FRAMEWORK_DECORATORS
|
|
349
|
+
assert "@router.get" in FRAMEWORK_DECORATORS
|
|
350
|
+
assert "@login_required" in FRAMEWORK_DECORATORS
|
|
351
|
+
assert "@task" in FRAMEWORK_DECORATORS
|
|
352
|
+
assert "@validator" in FRAMEWORK_DECORATORS
|
|
353
|
+
|
|
354
|
+
def test_framework_functions_list(self):
|
|
355
|
+
"""Test that FRAMEWORK_FUNCTIONS contains expected patterns"""
|
|
356
|
+
assert "get" in FRAMEWORK_FUNCTIONS
|
|
357
|
+
assert "post" in FRAMEWORK_FUNCTIONS
|
|
358
|
+
assert "save" in FRAMEWORK_FUNCTIONS
|
|
359
|
+
assert "*_queryset" in FRAMEWORK_FUNCTIONS
|
|
360
|
+
assert "get_context_data" in FRAMEWORK_FUNCTIONS
|
|
361
|
+
|
|
362
|
+
def test_framework_imports_set(self):
|
|
363
|
+
"""Test that FRAMEWORK_IMPORTS contains expected frameworks"""
|
|
364
|
+
assert 'flask' in FRAMEWORK_IMPORTS
|
|
365
|
+
assert 'django' in FRAMEWORK_IMPORTS
|
|
366
|
+
assert 'fastapi' in FRAMEWORK_IMPORTS
|
|
367
|
+
assert 'celery' in FRAMEWORK_IMPORTS
|
|
368
|
+
assert 'pydantic' in FRAMEWORK_IMPORTS
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
if __name__ == "__main__":
|
|
372
|
+
pytest.main([__file__, "-v"])
|
test/test_test_aware.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import ast
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
from skylos.test_aware import TestAwareVisitor
|
|
5
|
+
|
|
6
|
+
class TestTestAwareVisitor:
|
|
7
|
+
|
|
8
|
+
def test_init_without_filename(self):
|
|
9
|
+
visitor = TestAwareVisitor()
|
|
10
|
+
assert visitor.is_test_file == False
|
|
11
|
+
assert visitor.test_decorated_lines == set()
|
|
12
|
+
|
|
13
|
+
def test_init_with_non_test_filename(self):
|
|
14
|
+
visitor = TestAwareVisitor(filename="mymodule.py")
|
|
15
|
+
assert visitor.is_test_file == False
|
|
16
|
+
assert visitor.test_decorated_lines == set()
|
|
17
|
+
|
|
18
|
+
@patch('skylos.test_aware.TEST_FILE_RE')
|
|
19
|
+
def test_init_with_test_filename(self, mock_test_file_re):
|
|
20
|
+
mock_test_file_re.search.return_value = True
|
|
21
|
+
|
|
22
|
+
visitor = TestAwareVisitor(filename="test_module.py")
|
|
23
|
+
assert visitor.is_test_file == True
|
|
24
|
+
assert visitor.test_decorated_lines == set()
|
|
25
|
+
mock_test_file_re.search.assert_called_once_with("test_module.py")
|
|
26
|
+
|
|
27
|
+
def test_test_function_name_patterns(self):
|
|
28
|
+
code = """
|
|
29
|
+
def test_something():
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def another_test():
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def setup_method():
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def teardown_function():
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def setUp(self):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def tearDown(self):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def setUpClass(cls):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def tearDownClass(cls):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def setUpModule():
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def tearDownModule():
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def regular_function():
|
|
60
|
+
pass
|
|
61
|
+
"""
|
|
62
|
+
tree = ast.parse(code)
|
|
63
|
+
visitor = TestAwareVisitor()
|
|
64
|
+
visitor.visit(tree)
|
|
65
|
+
|
|
66
|
+
assert 2 in visitor.test_decorated_lines
|
|
67
|
+
assert 5 in visitor.test_decorated_lines
|
|
68
|
+
assert 8 in visitor.test_decorated_lines
|
|
69
|
+
assert 11 in visitor.test_decorated_lines
|
|
70
|
+
assert 14 in visitor.test_decorated_lines
|
|
71
|
+
assert 17 in visitor.test_decorated_lines # tearDown
|
|
72
|
+
assert 20 in visitor.test_decorated_lines # setUpClass
|
|
73
|
+
assert 23 in visitor.test_decorated_lines # tearDownClass
|
|
74
|
+
assert 26 in visitor.test_decorated_lines # setUpModule
|
|
75
|
+
assert 29 in visitor.test_decorated_lines # tearDownModule
|
|
76
|
+
assert 32 not in visitor.test_decorated_lines # gd ol regular_function
|
|
77
|
+
|
|
78
|
+
@patch('skylos.test_aware.TEST_DECOR_RE')
|
|
79
|
+
def test_test_decorator_detection(self, mock_test_decor_re):
|
|
80
|
+
mock_test_decor_re.match.return_value = True
|
|
81
|
+
|
|
82
|
+
code = """
|
|
83
|
+
@pytest.mark.parametrize('a,b', [(1,2), (3,4)])
|
|
84
|
+
def test_with_parametrize():
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
@mock.patch('module.function')
|
|
88
|
+
def test_with_mock():
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@unittest.skip("reason")
|
|
92
|
+
def test_with_skip():
|
|
93
|
+
pass
|
|
94
|
+
"""
|
|
95
|
+
tree = ast.parse(code)
|
|
96
|
+
visitor = TestAwareVisitor()
|
|
97
|
+
visitor.visit(tree)
|
|
98
|
+
|
|
99
|
+
assert 3 in visitor.test_decorated_lines
|
|
100
|
+
assert 7 in visitor.test_decorated_lines
|
|
101
|
+
assert 11 in visitor.test_decorated_lines
|
|
102
|
+
|
|
103
|
+
def test_pytest_fixture_detection(self):
|
|
104
|
+
code = """
|
|
105
|
+
@pytest.fixture
|
|
106
|
+
def sample_data():
|
|
107
|
+
return {"key": "value"}
|
|
108
|
+
|
|
109
|
+
@pytest.fixture(scope="session")
|
|
110
|
+
def database():
|
|
111
|
+
return create_test_db()
|
|
112
|
+
|
|
113
|
+
@fixture
|
|
114
|
+
def simple_fixture():
|
|
115
|
+
return "test"
|
|
116
|
+
"""
|
|
117
|
+
tree = ast.parse(code)
|
|
118
|
+
visitor = TestAwareVisitor()
|
|
119
|
+
visitor.visit(tree)
|
|
120
|
+
|
|
121
|
+
assert 3 in visitor.test_decorated_lines
|
|
122
|
+
assert 11 in visitor.test_decorated_lines
|
|
123
|
+
|
|
124
|
+
def test_async_test_function_detection(self):
|
|
125
|
+
code = """
|
|
126
|
+
async def test_async_function():
|
|
127
|
+
await some_async_operation()
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_with_asyncio_decorator():
|
|
131
|
+
await another_operation()
|
|
132
|
+
"""
|
|
133
|
+
tree = ast.parse(code)
|
|
134
|
+
visitor = TestAwareVisitor()
|
|
135
|
+
visitor.visit(tree)
|
|
136
|
+
|
|
137
|
+
assert 2 in visitor.test_decorated_lines
|
|
138
|
+
assert 6 in visitor.test_decorated_lines
|
|
139
|
+
|
|
140
|
+
def test_decorator_name_extraction(self):
|
|
141
|
+
visitor = TestAwareVisitor()
|
|
142
|
+
|
|
143
|
+
node = ast.parse("@decorator\ndef func(): pass").body[0].decorator_list[0]
|
|
144
|
+
result = visitor._decorator_name(node)
|
|
145
|
+
assert result == "decorator"
|
|
146
|
+
|
|
147
|
+
node = ast.parse("@pytest.mark.skip\ndef func(): pass").body[0].decorator_list[0]
|
|
148
|
+
result = visitor._decorator_name(node)
|
|
149
|
+
assert result == "pytest.mark.skip"
|
|
150
|
+
|
|
151
|
+
node = ast.parse("@fixture\ndef func(): pass").body[0].decorator_list[0]
|
|
152
|
+
result = visitor._decorator_name(node)
|
|
153
|
+
assert result == "fixture"
|
|
154
|
+
|
|
155
|
+
def test_multiple_decorators_on_function(self):
|
|
156
|
+
"""testing function with couple of decorators"""
|
|
157
|
+
code = """
|
|
158
|
+
@pytest.mark.parametrize('x', [1, 2, 3])
|
|
159
|
+
@pytest.mark.slow
|
|
160
|
+
@mock.patch('module.function')
|
|
161
|
+
def test_multiple_decorators():
|
|
162
|
+
pass
|
|
163
|
+
"""
|
|
164
|
+
tree = ast.parse(code)
|
|
165
|
+
visitor = TestAwareVisitor()
|
|
166
|
+
visitor.visit(tree)
|
|
167
|
+
|
|
168
|
+
# should be detected as test
|
|
169
|
+
assert 5 in visitor.test_decorated_lines
|
|
170
|
+
|
|
171
|
+
@patch('skylos.test_aware.TEST_IMPORT_RE')
|
|
172
|
+
def test_test_import_detection_when_test_file(self, mock_test_import_re):
|
|
173
|
+
mock_test_import_re.match.return_value = True
|
|
174
|
+
|
|
175
|
+
code = """
|
|
176
|
+
import pytest
|
|
177
|
+
import unittest
|
|
178
|
+
from mock import Mock
|
|
179
|
+
"""
|
|
180
|
+
tree = ast.parse(code)
|
|
181
|
+
visitor = TestAwareVisitor()
|
|
182
|
+
visitor.is_test_file = True
|
|
183
|
+
visitor.visit(tree)
|
|
184
|
+
|
|
185
|
+
assert mock_test_import_re.match.call_count >= 2
|
|
186
|
+
|
|
187
|
+
@patch('skylos.test_aware.TEST_IMPORT_RE')
|
|
188
|
+
def test_test_import_detection_when_not_test_file(self, mock_test_import_re):
|
|
189
|
+
code = """
|
|
190
|
+
import pytest
|
|
191
|
+
import unittest
|
|
192
|
+
"""
|
|
193
|
+
tree = ast.parse(code)
|
|
194
|
+
visitor = TestAwareVisitor()
|
|
195
|
+
visitor.is_test_file = False
|
|
196
|
+
visitor.visit(tree)
|
|
197
|
+
|
|
198
|
+
# no call regex match for imports
|
|
199
|
+
assert mock_test_import_re.match.call_count == 0
|
|
200
|
+
|
|
201
|
+
@patch('skylos.test_aware.TEST_IMPORT_RE')
|
|
202
|
+
def test_import_from_detection_when_test_file(self, mock_test_import_re):
|
|
203
|
+
mock_test_import_re.match.return_value = True
|
|
204
|
+
|
|
205
|
+
code = """
|
|
206
|
+
from pytest import fixture
|
|
207
|
+
from unittest.mock import Mock
|
|
208
|
+
"""
|
|
209
|
+
tree = ast.parse(code)
|
|
210
|
+
visitor = TestAwareVisitor()
|
|
211
|
+
visitor.is_test_file = True
|
|
212
|
+
visitor.visit(tree)
|
|
213
|
+
|
|
214
|
+
# should call regex match from-imports
|
|
215
|
+
assert mock_test_import_re.match.call_count >= 2
|
|
216
|
+
|
|
217
|
+
def test_complex_test_class(self):
|
|
218
|
+
code = """
|
|
219
|
+
class TestUserModel:
|
|
220
|
+
def setUp(self):
|
|
221
|
+
self.user = User()
|
|
222
|
+
|
|
223
|
+
def test_user_creation(self):
|
|
224
|
+
assert self.user.name == "test"
|
|
225
|
+
|
|
226
|
+
def test_user_validation(self):
|
|
227
|
+
assert self.user.is_valid()
|
|
228
|
+
|
|
229
|
+
def tearDown(self):
|
|
230
|
+
self.user.delete()
|
|
231
|
+
|
|
232
|
+
def helper_method(self):
|
|
233
|
+
return "not a test"
|
|
234
|
+
"""
|
|
235
|
+
tree = ast.parse(code)
|
|
236
|
+
visitor = TestAwareVisitor()
|
|
237
|
+
visitor.visit(tree)
|
|
238
|
+
|
|
239
|
+
assert 3 in visitor.test_decorated_lines # setUp
|
|
240
|
+
assert 6 in visitor.test_decorated_lines # test_user_creation
|
|
241
|
+
assert 9 in visitor.test_decorated_lines # test_user_validation
|
|
242
|
+
assert 12 in visitor.test_decorated_lines # tearDown
|
|
243
|
+
assert 15 not in visitor.test_decorated_lines # helper_method
|
|
244
|
+
|
|
245
|
+
def test_edge_case_function_names(self):
|
|
246
|
+
code = """
|
|
247
|
+
def test(): # 'test', no underscore
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
def testing_function(): # 'test' but not test_
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
def function_test(): # ending with _test
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
def setuptools_install(): # 'setup' but starts with setup
|
|
257
|
+
pass
|
|
258
|
+
"""
|
|
259
|
+
tree = ast.parse(code)
|
|
260
|
+
visitor = TestAwareVisitor()
|
|
261
|
+
visitor.visit(tree)
|
|
262
|
+
|
|
263
|
+
assert 2 not in visitor.test_decorated_lines
|
|
264
|
+
assert 5 not in visitor.test_decorated_lines
|
|
265
|
+
assert 8 in visitor.test_decorated_lines
|
|
266
|
+
assert 11 in visitor.test_decorated_lines
|
|
267
|
+
|
|
268
|
+
def test_nested_functions(self):
|
|
269
|
+
code = """
|
|
270
|
+
def outer_function():
|
|
271
|
+
def test_nested():
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
def setup_nested():
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
return test_nested
|
|
278
|
+
"""
|
|
279
|
+
tree = ast.parse(code)
|
|
280
|
+
visitor = TestAwareVisitor()
|
|
281
|
+
visitor.visit(tree)
|
|
282
|
+
|
|
283
|
+
assert 3 in visitor.test_decorated_lines
|
|
284
|
+
assert 6 in visitor.test_decorated_lines
|
|
285
|
+
assert 2 not in visitor.test_decorated_lines
|
|
286
|
+
|
|
287
|
+
class TestTestAwareVisitorIntegration:
|
|
288
|
+
|
|
289
|
+
@patch('skylos.test_aware.TEST_FILE_RE')
|
|
290
|
+
def test_full_test_file_analysis(self, mock_test_file_re):
|
|
291
|
+
mock_test_file_re.search.return_value = True
|
|
292
|
+
|
|
293
|
+
code = """
|
|
294
|
+
import pytest
|
|
295
|
+
from unittest.mock import Mock
|
|
296
|
+
|
|
297
|
+
class TestCalculator:
|
|
298
|
+
@pytest.fixture
|
|
299
|
+
def calculator(self):
|
|
300
|
+
return Calculator()
|
|
301
|
+
|
|
302
|
+
def setUp(self):
|
|
303
|
+
self.mock_logger = Mock()
|
|
304
|
+
|
|
305
|
+
def test_addition(self, calculator):
|
|
306
|
+
result = calculator.add(2, 3)
|
|
307
|
+
assert result == 5
|
|
308
|
+
|
|
309
|
+
@pytest.mark.parametrize('a,b,expected', [(1,2,3), (4,5,9)])
|
|
310
|
+
def test_addition_parametrized(self, calculator, a, b, expected):
|
|
311
|
+
assert calculator.add(a, b) == expected
|
|
312
|
+
|
|
313
|
+
def helper_method(self):
|
|
314
|
+
return "utility function"
|
|
315
|
+
"""
|
|
316
|
+
visitor = TestAwareVisitor(filename="test_calculator.py")
|
|
317
|
+
tree = ast.parse(code)
|
|
318
|
+
visitor.visit(tree)
|
|
319
|
+
|
|
320
|
+
assert visitor.is_test_file == True
|
|
321
|
+
assert 7 in visitor.test_decorated_lines
|
|
322
|
+
assert 10 in visitor.test_decorated_lines
|
|
323
|
+
assert 13 in visitor.test_decorated_lines
|
|
324
|
+
assert 18 in visitor.test_decorated_lines
|
|
325
|
+
assert 21 not in visitor.test_decorated_lines
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
pytest.main([__file__, "-v"])
|