pytest-api-cov 1.0.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/PKG-INFO +140 -21
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/README.md +139 -20
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/pyproject.toml +2 -3
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/src/pytest_api_cov/__init__.py +1 -0
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/src/pytest_api_cov/cli.py +29 -12
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/src/pytest_api_cov/config.py +2 -0
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/src/pytest_api_cov/plugin.py +363 -270
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/src/pytest_api_cov/pytest_flags.py +8 -0
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/.gitignore +0 -0
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/LICENSE +0 -0
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/src/pytest_api_cov/frameworks.py +0 -0
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/src/pytest_api_cov/models.py +0 -0
- {pytest_api_cov-1.0.0 → pytest_api_cov-1.0.1}/src/pytest_api_cov/report.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pytest-api-cov
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.1
|
4
4
|
Summary: Api Coverage Report Pytest Plugin
|
5
5
|
Author-email: Barnaby Gill <barnabasgill@gmail.com>
|
6
6
|
License: Apache-2.0
|
@@ -44,11 +44,28 @@ For most projects, no configuration is needed:
|
|
44
44
|
pytest --api-cov-report
|
45
45
|
```
|
46
46
|
|
47
|
+
### App Location Flexibility
|
48
|
+
|
49
|
+
**Zero Config**: Works automatically if your app is in `app.py`, `main.py`, or `server.py`
|
50
|
+
|
51
|
+
**Any Location**: Place your app anywhere in your project - just create a `conftest.py`:
|
52
|
+
|
53
|
+
```python
|
54
|
+
import pytest
|
55
|
+
from my_project.backend.api import my_app # Any import path!
|
56
|
+
|
57
|
+
@pytest.fixture
|
58
|
+
def app():
|
59
|
+
return my_app
|
60
|
+
```
|
61
|
+
|
47
62
|
The plugin will automatically discover your Flask/FastAPI app if it's in common locations:
|
48
63
|
- `app.py` (with variable `app`, `application`, or `main`)
|
49
64
|
- `main.py` (with variable `app`, `application`, or `main`)
|
50
65
|
- `server.py` (with variable `app`, `application`, or `server`)
|
51
66
|
|
67
|
+
**Your app can be located anywhere!** If it's not in a standard location, just create a `conftest.py` file to tell the plugin where to find it.
|
68
|
+
|
52
69
|
### Example
|
53
70
|
|
54
71
|
Given this FastAPI app in `app.py`:
|
@@ -74,12 +91,12 @@ def health_check():
|
|
74
91
|
And this test file:
|
75
92
|
|
76
93
|
```python
|
77
|
-
def test_root_endpoint(
|
78
|
-
response =
|
94
|
+
def test_root_endpoint(coverage_client):
|
95
|
+
response = coverage_client.get("/")
|
79
96
|
assert response.status_code == 200
|
80
97
|
|
81
|
-
def test_get_user(
|
82
|
-
response =
|
98
|
+
def test_get_user(coverage_client):
|
99
|
+
response = coverage_client.get("/users/123")
|
83
100
|
assert response.status_code == 200
|
84
101
|
```
|
85
102
|
|
@@ -93,6 +110,22 @@ Uncovered Endpoints:
|
|
93
110
|
Total API Coverage: 66.67%
|
94
111
|
```
|
95
112
|
|
113
|
+
Or running with advanced options `pytest --api-cov-report --api-cov-show-covered-endpoints --api-cov-exclusion-patterns="/users/*" --api-cov-show-excluded-endpoints --api-cov-report-path=api_coverage.json --api-cov-fail-under=49` produces:
|
114
|
+
|
115
|
+
```
|
116
|
+
API Coverage Report
|
117
|
+
Uncovered Endpoints:
|
118
|
+
[X] /health
|
119
|
+
Covered Endpoints:
|
120
|
+
[.] /
|
121
|
+
Excluded Endpoints:
|
122
|
+
[-] /users/{user_id}
|
123
|
+
|
124
|
+
SUCCESS: Coverage of 50.0% meets requirement of 49.0%
|
125
|
+
|
126
|
+
JSON report saved to api_coverage.json
|
127
|
+
```
|
128
|
+
|
96
129
|
## Advanced Configuration
|
97
130
|
|
98
131
|
### Setup Wizard
|
@@ -110,15 +143,56 @@ This will:
|
|
110
143
|
|
111
144
|
### Manual Configuration
|
112
145
|
|
113
|
-
Create a `conftest.py` file:
|
146
|
+
Create a `conftest.py` file to specify your app location (works with **any** file path or structure):
|
114
147
|
|
115
148
|
```python
|
116
149
|
import pytest
|
117
|
-
|
150
|
+
|
151
|
+
# Import from anywhere in your project
|
152
|
+
from my_project.backend.api import flask_app
|
153
|
+
# or from src.services.web_server import fastapi_instance
|
154
|
+
# or from deeply.nested.modules import my_app
|
118
155
|
|
119
156
|
@pytest.fixture
|
120
157
|
def app():
|
121
|
-
return app
|
158
|
+
return flask_app # Return your app instance
|
159
|
+
```
|
160
|
+
|
161
|
+
This approach works with any project structure - the plugin doesn't care where your app is located as long as you can import it.
|
162
|
+
|
163
|
+
### Custom Test Client Fixtures
|
164
|
+
|
165
|
+
If you have an existing test client fixture with custom setup (authentication, headers, etc.), you can wrap it with coverage tracking:
|
166
|
+
|
167
|
+
```python
|
168
|
+
import pytest
|
169
|
+
from fastapi.testclient import TestClient
|
170
|
+
from your_app import app
|
171
|
+
|
172
|
+
@pytest.fixture
|
173
|
+
def my_custom_client():
|
174
|
+
"""Custom test client with authentication."""
|
175
|
+
client = TestClient(app)
|
176
|
+
client.headers.update({"Authorization": "Bearer test-token"})
|
177
|
+
return client
|
178
|
+
|
179
|
+
def test_endpoint(coverage_client):
|
180
|
+
# coverage_client will be your custom client with coverage tracking
|
181
|
+
response = coverage_client.get("/protected-endpoint")
|
182
|
+
assert response.status_code == 200
|
183
|
+
```
|
184
|
+
|
185
|
+
Configure it in `pyproject.toml`:
|
186
|
+
|
187
|
+
```toml
|
188
|
+
[tool.pytest_api_cov]
|
189
|
+
client_fixture_name = "my_custom_client"
|
190
|
+
```
|
191
|
+
|
192
|
+
Or via command line:
|
193
|
+
|
194
|
+
```bash
|
195
|
+
pytest --api-cov-report --api-cov-client-fixture-name=my_custom_client
|
122
196
|
```
|
123
197
|
|
124
198
|
### Configuration Options
|
@@ -153,6 +227,9 @@ force_sugar = true
|
|
153
227
|
|
154
228
|
# Force no Unicode symbols in output
|
155
229
|
force_sugar_disabled = true
|
230
|
+
|
231
|
+
# Wrap an existing custom test client fixture with coverage tracking
|
232
|
+
client_fixture_name = "my_custom_client"
|
156
233
|
```
|
157
234
|
|
158
235
|
### Command Line Options
|
@@ -194,7 +271,6 @@ Works automatically with FastAPI and Flask applications.
|
|
194
271
|
|
195
272
|
```python
|
196
273
|
from fastapi import FastAPI
|
197
|
-
from fastapi.testclient import TestClient
|
198
274
|
|
199
275
|
app = FastAPI()
|
200
276
|
|
@@ -202,9 +278,9 @@ app = FastAPI()
|
|
202
278
|
def read_item(item_id: int):
|
203
279
|
return {"item_id": item_id}
|
204
280
|
|
205
|
-
# Tests automatically get a '
|
206
|
-
def test_read_item(
|
207
|
-
response =
|
281
|
+
# Tests automatically get a 'coverage_client' fixture
|
282
|
+
def test_read_item(coverage_client):
|
283
|
+
response = coverage_client.get("/items/42")
|
208
284
|
assert response.status_code == 200
|
209
285
|
```
|
210
286
|
|
@@ -219,9 +295,9 @@ app = Flask(__name__)
|
|
219
295
|
def get_user(user_id):
|
220
296
|
return {"user_id": user_id}
|
221
297
|
|
222
|
-
# Tests automatically get a '
|
223
|
-
def test_get_user(
|
224
|
-
response =
|
298
|
+
# Tests automatically get a 'coverage_client' fixture
|
299
|
+
def test_get_user(coverage_client):
|
300
|
+
response = coverage_client.get("/users/123")
|
225
301
|
assert response.status_code == 200
|
226
302
|
```
|
227
303
|
|
@@ -301,13 +377,56 @@ jobs:
|
|
301
377
|
|
302
378
|
### No App Found
|
303
379
|
|
304
|
-
If you see "No API app found",
|
380
|
+
If you see "No API app found", you have several options:
|
381
|
+
|
382
|
+
**Option 1 - Auto-discovery (Zero Config)**
|
383
|
+
Place your app in a standard location with a standard name:
|
384
|
+
- Files: `app.py`, `main.py`, `server.py`, `wsgi.py`, `asgi.py`
|
385
|
+
- Variable names: `app`, `application`, `main`, `server`
|
386
|
+
|
387
|
+
**Option 2 - Custom Location (Any File/Path)**
|
388
|
+
Create a `conftest.py` file to specify your app location:
|
389
|
+
|
390
|
+
```python
|
391
|
+
import pytest
|
392
|
+
from my_project.api.server import my_flask_app # Any import path
|
393
|
+
# or from src.backend.main import fastapi_instance
|
394
|
+
# or from anywhere import your_app
|
395
|
+
|
396
|
+
@pytest.fixture
|
397
|
+
def app():
|
398
|
+
return my_flask_app # Return your app instance
|
399
|
+
```
|
400
|
+
|
401
|
+
**Option 3 - Override Auto-discovery**
|
402
|
+
If you have multiple auto-discoverable files or want to use a different app:
|
403
|
+
|
404
|
+
```python
|
405
|
+
# Even if you have app.py, you can override it
|
406
|
+
import pytest
|
407
|
+
from main import my_real_app # Use this instead of app.py
|
408
|
+
|
409
|
+
@pytest.fixture
|
410
|
+
def app():
|
411
|
+
return my_real_app
|
412
|
+
```
|
413
|
+
|
414
|
+
**Option 4 - Setup Wizard**
|
415
|
+
Run the interactive setup: `pytest-api-cov init`
|
416
|
+
|
417
|
+
The plugin will automatically find your app using the `app` fixture first, then fall back to auto-discovery in common locations. This means you can place your app **anywhere** as long as you create the fixture.
|
418
|
+
|
419
|
+
### Multiple App Files
|
420
|
+
|
421
|
+
If you have multiple files that could be auto-discovered (e.g., both `app.py` and `main.py`), the plugin will use the **first valid app it finds** in this priority order:
|
305
422
|
|
306
|
-
1.
|
307
|
-
2.
|
308
|
-
3.
|
423
|
+
1. `app.py`
|
424
|
+
2. `main.py`
|
425
|
+
3. `server.py`
|
426
|
+
4. `wsgi.py`
|
427
|
+
5. `asgi.py`
|
309
428
|
|
310
|
-
|
429
|
+
To use a specific app when multiple exist, create a `conftest.py` with an `app` fixture pointing to your preferred app.
|
311
430
|
|
312
431
|
### No Endpoints Discovered
|
313
432
|
|
@@ -315,7 +434,7 @@ If you see "No endpoints discovered":
|
|
315
434
|
|
316
435
|
1. Check that your app is properly instantiated
|
317
436
|
2. Verify your routes/endpoints are defined
|
318
|
-
3. Ensure the `
|
437
|
+
3. Ensure the `coverage_client` fixture is working in your tests
|
319
438
|
4. Use `-v` or `-vv` for debug information
|
320
439
|
|
321
440
|
### Framework Not Detected
|
@@ -26,11 +26,28 @@ For most projects, no configuration is needed:
|
|
26
26
|
pytest --api-cov-report
|
27
27
|
```
|
28
28
|
|
29
|
+
### App Location Flexibility
|
30
|
+
|
31
|
+
**Zero Config**: Works automatically if your app is in `app.py`, `main.py`, or `server.py`
|
32
|
+
|
33
|
+
**Any Location**: Place your app anywhere in your project - just create a `conftest.py`:
|
34
|
+
|
35
|
+
```python
|
36
|
+
import pytest
|
37
|
+
from my_project.backend.api import my_app # Any import path!
|
38
|
+
|
39
|
+
@pytest.fixture
|
40
|
+
def app():
|
41
|
+
return my_app
|
42
|
+
```
|
43
|
+
|
29
44
|
The plugin will automatically discover your Flask/FastAPI app if it's in common locations:
|
30
45
|
- `app.py` (with variable `app`, `application`, or `main`)
|
31
46
|
- `main.py` (with variable `app`, `application`, or `main`)
|
32
47
|
- `server.py` (with variable `app`, `application`, or `server`)
|
33
48
|
|
49
|
+
**Your app can be located anywhere!** If it's not in a standard location, just create a `conftest.py` file to tell the plugin where to find it.
|
50
|
+
|
34
51
|
### Example
|
35
52
|
|
36
53
|
Given this FastAPI app in `app.py`:
|
@@ -56,12 +73,12 @@ def health_check():
|
|
56
73
|
And this test file:
|
57
74
|
|
58
75
|
```python
|
59
|
-
def test_root_endpoint(
|
60
|
-
response =
|
76
|
+
def test_root_endpoint(coverage_client):
|
77
|
+
response = coverage_client.get("/")
|
61
78
|
assert response.status_code == 200
|
62
79
|
|
63
|
-
def test_get_user(
|
64
|
-
response =
|
80
|
+
def test_get_user(coverage_client):
|
81
|
+
response = coverage_client.get("/users/123")
|
65
82
|
assert response.status_code == 200
|
66
83
|
```
|
67
84
|
|
@@ -75,6 +92,22 @@ Uncovered Endpoints:
|
|
75
92
|
Total API Coverage: 66.67%
|
76
93
|
```
|
77
94
|
|
95
|
+
Or running with advanced options `pytest --api-cov-report --api-cov-show-covered-endpoints --api-cov-exclusion-patterns="/users/*" --api-cov-show-excluded-endpoints --api-cov-report-path=api_coverage.json --api-cov-fail-under=49` produces:
|
96
|
+
|
97
|
+
```
|
98
|
+
API Coverage Report
|
99
|
+
Uncovered Endpoints:
|
100
|
+
[X] /health
|
101
|
+
Covered Endpoints:
|
102
|
+
[.] /
|
103
|
+
Excluded Endpoints:
|
104
|
+
[-] /users/{user_id}
|
105
|
+
|
106
|
+
SUCCESS: Coverage of 50.0% meets requirement of 49.0%
|
107
|
+
|
108
|
+
JSON report saved to api_coverage.json
|
109
|
+
```
|
110
|
+
|
78
111
|
## Advanced Configuration
|
79
112
|
|
80
113
|
### Setup Wizard
|
@@ -92,15 +125,56 @@ This will:
|
|
92
125
|
|
93
126
|
### Manual Configuration
|
94
127
|
|
95
|
-
Create a `conftest.py` file:
|
128
|
+
Create a `conftest.py` file to specify your app location (works with **any** file path or structure):
|
96
129
|
|
97
130
|
```python
|
98
131
|
import pytest
|
99
|
-
|
132
|
+
|
133
|
+
# Import from anywhere in your project
|
134
|
+
from my_project.backend.api import flask_app
|
135
|
+
# or from src.services.web_server import fastapi_instance
|
136
|
+
# or from deeply.nested.modules import my_app
|
100
137
|
|
101
138
|
@pytest.fixture
|
102
139
|
def app():
|
103
|
-
return app
|
140
|
+
return flask_app # Return your app instance
|
141
|
+
```
|
142
|
+
|
143
|
+
This approach works with any project structure - the plugin doesn't care where your app is located as long as you can import it.
|
144
|
+
|
145
|
+
### Custom Test Client Fixtures
|
146
|
+
|
147
|
+
If you have an existing test client fixture with custom setup (authentication, headers, etc.), you can wrap it with coverage tracking:
|
148
|
+
|
149
|
+
```python
|
150
|
+
import pytest
|
151
|
+
from fastapi.testclient import TestClient
|
152
|
+
from your_app import app
|
153
|
+
|
154
|
+
@pytest.fixture
|
155
|
+
def my_custom_client():
|
156
|
+
"""Custom test client with authentication."""
|
157
|
+
client = TestClient(app)
|
158
|
+
client.headers.update({"Authorization": "Bearer test-token"})
|
159
|
+
return client
|
160
|
+
|
161
|
+
def test_endpoint(coverage_client):
|
162
|
+
# coverage_client will be your custom client with coverage tracking
|
163
|
+
response = coverage_client.get("/protected-endpoint")
|
164
|
+
assert response.status_code == 200
|
165
|
+
```
|
166
|
+
|
167
|
+
Configure it in `pyproject.toml`:
|
168
|
+
|
169
|
+
```toml
|
170
|
+
[tool.pytest_api_cov]
|
171
|
+
client_fixture_name = "my_custom_client"
|
172
|
+
```
|
173
|
+
|
174
|
+
Or via command line:
|
175
|
+
|
176
|
+
```bash
|
177
|
+
pytest --api-cov-report --api-cov-client-fixture-name=my_custom_client
|
104
178
|
```
|
105
179
|
|
106
180
|
### Configuration Options
|
@@ -135,6 +209,9 @@ force_sugar = true
|
|
135
209
|
|
136
210
|
# Force no Unicode symbols in output
|
137
211
|
force_sugar_disabled = true
|
212
|
+
|
213
|
+
# Wrap an existing custom test client fixture with coverage tracking
|
214
|
+
client_fixture_name = "my_custom_client"
|
138
215
|
```
|
139
216
|
|
140
217
|
### Command Line Options
|
@@ -176,7 +253,6 @@ Works automatically with FastAPI and Flask applications.
|
|
176
253
|
|
177
254
|
```python
|
178
255
|
from fastapi import FastAPI
|
179
|
-
from fastapi.testclient import TestClient
|
180
256
|
|
181
257
|
app = FastAPI()
|
182
258
|
|
@@ -184,9 +260,9 @@ app = FastAPI()
|
|
184
260
|
def read_item(item_id: int):
|
185
261
|
return {"item_id": item_id}
|
186
262
|
|
187
|
-
# Tests automatically get a '
|
188
|
-
def test_read_item(
|
189
|
-
response =
|
263
|
+
# Tests automatically get a 'coverage_client' fixture
|
264
|
+
def test_read_item(coverage_client):
|
265
|
+
response = coverage_client.get("/items/42")
|
190
266
|
assert response.status_code == 200
|
191
267
|
```
|
192
268
|
|
@@ -201,9 +277,9 @@ app = Flask(__name__)
|
|
201
277
|
def get_user(user_id):
|
202
278
|
return {"user_id": user_id}
|
203
279
|
|
204
|
-
# Tests automatically get a '
|
205
|
-
def test_get_user(
|
206
|
-
response =
|
280
|
+
# Tests automatically get a 'coverage_client' fixture
|
281
|
+
def test_get_user(coverage_client):
|
282
|
+
response = coverage_client.get("/users/123")
|
207
283
|
assert response.status_code == 200
|
208
284
|
```
|
209
285
|
|
@@ -283,13 +359,56 @@ jobs:
|
|
283
359
|
|
284
360
|
### No App Found
|
285
361
|
|
286
|
-
If you see "No API app found",
|
362
|
+
If you see "No API app found", you have several options:
|
363
|
+
|
364
|
+
**Option 1 - Auto-discovery (Zero Config)**
|
365
|
+
Place your app in a standard location with a standard name:
|
366
|
+
- Files: `app.py`, `main.py`, `server.py`, `wsgi.py`, `asgi.py`
|
367
|
+
- Variable names: `app`, `application`, `main`, `server`
|
368
|
+
|
369
|
+
**Option 2 - Custom Location (Any File/Path)**
|
370
|
+
Create a `conftest.py` file to specify your app location:
|
371
|
+
|
372
|
+
```python
|
373
|
+
import pytest
|
374
|
+
from my_project.api.server import my_flask_app # Any import path
|
375
|
+
# or from src.backend.main import fastapi_instance
|
376
|
+
# or from anywhere import your_app
|
377
|
+
|
378
|
+
@pytest.fixture
|
379
|
+
def app():
|
380
|
+
return my_flask_app # Return your app instance
|
381
|
+
```
|
382
|
+
|
383
|
+
**Option 3 - Override Auto-discovery**
|
384
|
+
If you have multiple auto-discoverable files or want to use a different app:
|
385
|
+
|
386
|
+
```python
|
387
|
+
# Even if you have app.py, you can override it
|
388
|
+
import pytest
|
389
|
+
from main import my_real_app # Use this instead of app.py
|
390
|
+
|
391
|
+
@pytest.fixture
|
392
|
+
def app():
|
393
|
+
return my_real_app
|
394
|
+
```
|
395
|
+
|
396
|
+
**Option 4 - Setup Wizard**
|
397
|
+
Run the interactive setup: `pytest-api-cov init`
|
398
|
+
|
399
|
+
The plugin will automatically find your app using the `app` fixture first, then fall back to auto-discovery in common locations. This means you can place your app **anywhere** as long as you create the fixture.
|
400
|
+
|
401
|
+
### Multiple App Files
|
402
|
+
|
403
|
+
If you have multiple files that could be auto-discovered (e.g., both `app.py` and `main.py`), the plugin will use the **first valid app it finds** in this priority order:
|
287
404
|
|
288
|
-
1.
|
289
|
-
2.
|
290
|
-
3.
|
405
|
+
1. `app.py`
|
406
|
+
2. `main.py`
|
407
|
+
3. `server.py`
|
408
|
+
4. `wsgi.py`
|
409
|
+
5. `asgi.py`
|
291
410
|
|
292
|
-
|
411
|
+
To use a specific app when multiple exist, create a `conftest.py` with an `app` fixture pointing to your preferred app.
|
293
412
|
|
294
413
|
### No Endpoints Discovered
|
295
414
|
|
@@ -297,7 +416,7 @@ If you see "No endpoints discovered":
|
|
297
416
|
|
298
417
|
1. Check that your app is properly instantiated
|
299
418
|
2. Verify your routes/endpoints are defined
|
300
|
-
3. Ensure the `
|
419
|
+
3. Ensure the `coverage_client` fixture is working in your tests
|
301
420
|
4. Use `-v` or `-vv` for debug information
|
302
421
|
|
303
422
|
### Framework Not Detected
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "pytest-api-cov"
|
3
|
-
version = "1.0.
|
3
|
+
version = "1.0.1"
|
4
4
|
description = "Api Coverage Report Pytest Plugin"
|
5
5
|
readme = "README.md"
|
6
6
|
authors = [
|
@@ -32,7 +32,7 @@ dev = [
|
|
32
32
|
|
33
33
|
# API COVERAGE
|
34
34
|
[tool.pytest_api_cov]
|
35
|
-
|
35
|
+
fail_under = 70
|
36
36
|
show_covered_endpoints = true
|
37
37
|
show_uncovered_endpoints = true
|
38
38
|
show_excluded_endpoints = true
|
@@ -51,7 +51,6 @@ exclude_lines = [
|
|
51
51
|
"if TYPE_CHECKING:",
|
52
52
|
]
|
53
53
|
show_missing = true
|
54
|
-
fail_under = 70.0
|
55
54
|
|
56
55
|
|
57
56
|
[build-system]
|
@@ -56,22 +56,36 @@ def generate_conftest_content(framework: str, file_path: str, app_variable: str)
|
|
56
56
|
|
57
57
|
import pytest
|
58
58
|
|
59
|
-
# Import your {framework} app
|
59
|
+
# Import your {framework} app from anywhere in your project
|
60
60
|
from {module_path} import {app_variable}
|
61
61
|
|
62
62
|
|
63
63
|
@pytest.fixture
|
64
|
-
def
|
65
|
-
"""Provide the {framework}
|
64
|
+
def app():
|
65
|
+
"""Provide the {framework} app for API coverage testing.
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
def test_root_endpoint(client):
|
70
|
-
response = client.get("/")
|
71
|
-
assert response.status_code == 200
|
72
|
-
```
|
67
|
+
You can import from any location - just change the import path above
|
68
|
+
to match your project structure.
|
73
69
|
"""
|
74
70
|
return {app_variable}
|
71
|
+
|
72
|
+
|
73
|
+
# The plugin will automatically create a 'coverage_client' fixture that uses your 'app' fixture
|
74
|
+
# You can use either:
|
75
|
+
# - def test_endpoint(app): ... # Direct app access
|
76
|
+
# - def test_endpoint(coverage_client): ... # Test client with API coverage tracking
|
77
|
+
#
|
78
|
+
# To wrap an existing custom fixture instead, specify the fixture name in pyproject.toml:
|
79
|
+
# [tool.pytest_api_cov]
|
80
|
+
# client_fixture_name = "my_custom_client"
|
81
|
+
#
|
82
|
+
# Example custom fixture:
|
83
|
+
# @pytest.fixture
|
84
|
+
# def my_custom_client(app):
|
85
|
+
# client = app.test_client() # Flask
|
86
|
+
# # or client = TestClient(app) # FastAPI
|
87
|
+
# # Add custom setup here (auth headers, etc.)
|
88
|
+
# return client
|
75
89
|
'''
|
76
90
|
|
77
91
|
|
@@ -100,6 +114,9 @@ show_excluded_endpoints = false
|
|
100
114
|
|
101
115
|
# Force Unicode symbols in terminal output (optional)
|
102
116
|
# force_sugar = true
|
117
|
+
|
118
|
+
# Wrap an existing custom test client fixture with coverage tracking (optional)
|
119
|
+
# client_fixture_name = "my_custom_client"
|
103
120
|
"""
|
104
121
|
|
105
122
|
|
@@ -152,13 +169,13 @@ testpaths = ["tests"]
|
|
152
169
|
print("🎉 Setup complete!")
|
153
170
|
print()
|
154
171
|
print("Next steps:")
|
155
|
-
print("1. Write your tests using the '
|
172
|
+
print("1. Write your tests using the 'coverage_client' fixture")
|
156
173
|
print("2. Run: pytest --api-cov-report")
|
157
174
|
print()
|
158
175
|
print("Example test:")
|
159
176
|
print("""
|
160
|
-
def test_root_endpoint(
|
161
|
-
response =
|
177
|
+
def test_root_endpoint(coverage_client):
|
178
|
+
response = coverage_client.get("/")
|
162
179
|
assert response.status_code == 200
|
163
180
|
""")
|
164
181
|
|
@@ -20,6 +20,7 @@ class ApiCoverageReportConfig(BaseModel):
|
|
20
20
|
report_path: Optional[str] = Field(None, alias="api-cov-report-path")
|
21
21
|
force_sugar: bool = Field(False, alias="api-cov-force-sugar")
|
22
22
|
force_sugar_disabled: bool = Field(False, alias="api-cov-force-sugar-disabled")
|
23
|
+
client_fixture_name: str = Field("coverage_client", alias="api-cov-client-fixture-name")
|
23
24
|
|
24
25
|
|
25
26
|
def read_toml_config() -> Dict[str, Any]:
|
@@ -43,6 +44,7 @@ def read_session_config(session_config: Any) -> Dict[str, Any]:
|
|
43
44
|
"api-cov-report-path": "report_path",
|
44
45
|
"api-cov-force-sugar": "force_sugar",
|
45
46
|
"api-cov-force-sugar-disabled": "force_sugar_disabled",
|
47
|
+
"api-cov-client-fixture-name": "client_fixture_name",
|
46
48
|
}
|
47
49
|
config = {}
|
48
50
|
for opt, key in cli_options.items():
|
@@ -1,270 +1,363 @@
|
|
1
|
-
"""pytest plugin for API coverage tracking."""
|
2
|
-
|
3
|
-
import importlib
|
4
|
-
import importlib.util
|
5
|
-
import logging
|
6
|
-
import os
|
7
|
-
from typing import Any, Optional
|
8
|
-
|
9
|
-
import pytest
|
10
|
-
|
11
|
-
from .config import get_pytest_api_cov_report_config
|
12
|
-
from .frameworks import get_framework_adapter
|
13
|
-
from .models import SessionData
|
14
|
-
from .pytest_flags import add_pytest_api_cov_flags
|
15
|
-
from .report import generate_pytest_api_cov_report
|
16
|
-
|
17
|
-
logger = logging.getLogger(__name__)
|
18
|
-
|
19
|
-
|
20
|
-
def is_supported_framework(app: Any) -> bool:
|
21
|
-
"""Check if the app is a supported framework (Flask or FastAPI)."""
|
22
|
-
if app is None:
|
23
|
-
return False
|
24
|
-
|
25
|
-
app_type = type(app).__name__
|
26
|
-
module_name = getattr(type(app), "__module__", "").split(".")[0]
|
27
|
-
|
28
|
-
return (module_name == "flask" and app_type == "Flask") or (module_name == "fastapi" and app_type == "FastAPI")
|
29
|
-
|
30
|
-
|
31
|
-
def auto_discover_app() -> Optional[Any]:
|
32
|
-
"""Automatically discover Flask/FastAPI apps in common locations."""
|
33
|
-
logger.debug("> Auto-discovering app in common locations...")
|
34
|
-
|
35
|
-
common_patterns = [
|
36
|
-
("app.py", ["app", "application", "main"]),
|
37
|
-
("main.py", ["app", "application", "main"]),
|
38
|
-
("server.py", ["app", "application", "server"]),
|
39
|
-
("wsgi.py", ["app", "application"]),
|
40
|
-
("asgi.py", ["app", "application"]),
|
41
|
-
]
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
1
|
+
"""pytest plugin for API coverage tracking."""
|
2
|
+
|
3
|
+
import importlib
|
4
|
+
import importlib.util
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
from typing import Any, Optional
|
8
|
+
|
9
|
+
import pytest
|
10
|
+
|
11
|
+
from .config import get_pytest_api_cov_report_config
|
12
|
+
from .frameworks import get_framework_adapter
|
13
|
+
from .models import SessionData
|
14
|
+
from .pytest_flags import add_pytest_api_cov_flags
|
15
|
+
from .report import generate_pytest_api_cov_report
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
def is_supported_framework(app: Any) -> bool:
|
21
|
+
"""Check if the app is a supported framework (Flask or FastAPI)."""
|
22
|
+
if app is None:
|
23
|
+
return False
|
24
|
+
|
25
|
+
app_type = type(app).__name__
|
26
|
+
module_name = getattr(type(app), "__module__", "").split(".")[0]
|
27
|
+
|
28
|
+
return (module_name == "flask" and app_type == "Flask") or (module_name == "fastapi" and app_type == "FastAPI")
|
29
|
+
|
30
|
+
|
31
|
+
def auto_discover_app() -> Optional[Any]:
|
32
|
+
"""Automatically discover Flask/FastAPI apps in common locations."""
|
33
|
+
logger.debug("> Auto-discovering app in common locations...")
|
34
|
+
|
35
|
+
common_patterns = [
|
36
|
+
("app.py", ["app", "application", "main"]),
|
37
|
+
("main.py", ["app", "application", "main"]),
|
38
|
+
("server.py", ["app", "application", "server"]),
|
39
|
+
("wsgi.py", ["app", "application"]),
|
40
|
+
("asgi.py", ["app", "application"]),
|
41
|
+
]
|
42
|
+
|
43
|
+
found_apps = [] # Track all discovered apps
|
44
|
+
found_files = [] # Track all files that exist
|
45
|
+
|
46
|
+
for filename, attr_names in common_patterns:
|
47
|
+
if os.path.exists(filename):
|
48
|
+
found_files.append(filename)
|
49
|
+
logger.debug(f"> Found {filename}, checking for app variables...")
|
50
|
+
try:
|
51
|
+
module_name = filename[:-3] # .py extension
|
52
|
+
spec = importlib.util.spec_from_file_location(module_name, filename)
|
53
|
+
if spec and spec.loader:
|
54
|
+
module = importlib.util.module_from_spec(spec)
|
55
|
+
spec.loader.exec_module(module)
|
56
|
+
|
57
|
+
for attr_name in attr_names:
|
58
|
+
if hasattr(module, attr_name):
|
59
|
+
app = getattr(module, attr_name)
|
60
|
+
if is_supported_framework(app):
|
61
|
+
found_apps.append((filename, attr_name, type(app).__name__))
|
62
|
+
# Return the first valid app found, but log what we're doing
|
63
|
+
if len(found_apps) == 1:
|
64
|
+
logger.info(
|
65
|
+
f"✅ Auto-discovered {type(app).__name__} app in {filename} as '{attr_name}'"
|
66
|
+
)
|
67
|
+
# Check if there are more files to scan
|
68
|
+
remaining_files = [f for f in [p[0] for p in common_patterns[common_patterns.index((filename, attr_names)):]] if os.path.exists(f) and f != filename]
|
69
|
+
if remaining_files:
|
70
|
+
logger.debug(f"> Note: Also found files {remaining_files} but using first discovered app")
|
71
|
+
logger.debug(f"> To use a different app, create a conftest.py with an 'app' fixture")
|
72
|
+
return app
|
73
|
+
else:
|
74
|
+
logger.debug(f"> Found '{attr_name}' in {filename} but it's not a supported framework")
|
75
|
+
|
76
|
+
except Exception as e:
|
77
|
+
logger.debug(f"> Could not import {filename}: {e}")
|
78
|
+
continue
|
79
|
+
|
80
|
+
# If we get here, no apps were found
|
81
|
+
if found_files:
|
82
|
+
logger.debug(f"> Found files {found_files} but no supported Flask/FastAPI apps in them")
|
83
|
+
logger.debug(f"> If your app is in one of these files with a different variable name,")
|
84
|
+
logger.debug(f"> create a conftest.py with an 'app' fixture to specify it")
|
85
|
+
|
86
|
+
logger.debug("> No app auto-discovered")
|
87
|
+
return None
|
88
|
+
|
89
|
+
|
90
|
+
def get_helpful_error_message() -> str:
|
91
|
+
"""Generate a helpful error message for setup guidance."""
|
92
|
+
return """
|
93
|
+
🚫 No API app found!
|
94
|
+
|
95
|
+
Quick Setup Options:
|
96
|
+
|
97
|
+
Option 1 - Auto-discovery (Zero Config):
|
98
|
+
Place your FastAPI/Flask app in one of these files:
|
99
|
+
• app.py (with variable named 'app', 'application', or 'main')
|
100
|
+
• main.py (with variable named 'app', 'application', or 'main')
|
101
|
+
• server.py (with variable named 'app', 'application', or 'server')
|
102
|
+
|
103
|
+
Example app.py:
|
104
|
+
from fastapi import FastAPI
|
105
|
+
app = FastAPI() # <- Plugin will auto-discover this
|
106
|
+
|
107
|
+
Option 2 - Custom Location or Override Auto-discovery:
|
108
|
+
Create conftest.py to specify exactly which app to use:
|
109
|
+
|
110
|
+
import pytest
|
111
|
+
from my_project.api.server import my_app # Any import path!
|
112
|
+
# or from app import my_real_app # Override auto-discovery
|
113
|
+
|
114
|
+
@pytest.fixture
|
115
|
+
def app():
|
116
|
+
return my_app
|
117
|
+
|
118
|
+
This works for:
|
119
|
+
• Apps in custom locations
|
120
|
+
• Multiple app files (specify which one to use)
|
121
|
+
• Different variable names in standard files
|
122
|
+
|
123
|
+
Option 3 - Setup Wizard:
|
124
|
+
Run: pytest-api-cov init
|
125
|
+
|
126
|
+
Then run: pytest --api-cov-report
|
127
|
+
"""
|
128
|
+
|
129
|
+
|
130
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
131
|
+
"""Add API coverage flags to the pytest parser."""
|
132
|
+
add_pytest_api_cov_flags(parser)
|
133
|
+
|
134
|
+
|
135
|
+
def pytest_configure(config: pytest.Config) -> None:
|
136
|
+
"""Configure the pytest session and logging."""
|
137
|
+
if config.getoption("--api-cov-report"):
|
138
|
+
verbosity = config.option.verbose
|
139
|
+
|
140
|
+
if verbosity >= 2: # -vv or more
|
141
|
+
log_level = logging.DEBUG
|
142
|
+
elif verbosity >= 1: # -v
|
143
|
+
log_level = logging.INFO
|
144
|
+
else:
|
145
|
+
log_level = logging.WARNING
|
146
|
+
|
147
|
+
logger.setLevel(log_level)
|
148
|
+
|
149
|
+
if not logger.handlers:
|
150
|
+
handler = logging.StreamHandler()
|
151
|
+
handler.setLevel(log_level)
|
152
|
+
formatter = logging.Formatter("%(message)s")
|
153
|
+
handler.setFormatter(formatter)
|
154
|
+
logger.addHandler(handler)
|
155
|
+
|
156
|
+
logger.info("Initializing API coverage plugin...")
|
157
|
+
|
158
|
+
if config.pluginmanager.hasplugin("xdist"):
|
159
|
+
config.pluginmanager.register(DeferXdistPlugin(), "defer_xdist_plugin")
|
160
|
+
|
161
|
+
|
162
|
+
def pytest_sessionstart(session: pytest.Session) -> None:
|
163
|
+
"""Initialize the call recorder at the start of the session."""
|
164
|
+
if session.config.getoption("--api-cov-report"):
|
165
|
+
session.api_coverage_data = SessionData() # type: ignore[attr-defined]
|
166
|
+
|
167
|
+
|
168
|
+
def wrap_client_with_coverage(client: Any, recorder: Any, test_name: str) -> Any:
|
169
|
+
"""Wrap an existing test client with coverage tracking."""
|
170
|
+
|
171
|
+
class CoverageWrapper:
|
172
|
+
def __init__(self, wrapped_client: Any):
|
173
|
+
self._wrapped = wrapped_client
|
174
|
+
|
175
|
+
def __getattr__(self, name: str) -> Any:
|
176
|
+
attr = getattr(self._wrapped, name)
|
177
|
+
if name in ["get", "post", "put", "delete", "patch", "head", "options"]:
|
178
|
+
|
179
|
+
def tracked_method(*args: Any, **kwargs: Any) -> Any:
|
180
|
+
response = attr(*args, **kwargs)
|
181
|
+
# Extract path from args[0]
|
182
|
+
if args and recorder is not None:
|
183
|
+
path = args[0]
|
184
|
+
# Clean up the path to match endpoint format
|
185
|
+
if isinstance(path, str):
|
186
|
+
# Remove query parameters
|
187
|
+
path = path.partition("?")[0]
|
188
|
+
recorder.record_call(path, test_name)
|
189
|
+
return response
|
190
|
+
|
191
|
+
return tracked_method
|
192
|
+
return attr
|
193
|
+
|
194
|
+
return CoverageWrapper(client)
|
195
|
+
|
196
|
+
|
197
|
+
def get_app_from_fixture_or_auto_discover(request: pytest.FixtureRequest) -> Any:
|
198
|
+
"""Get app from fixture or auto-discovery."""
|
199
|
+
app = None
|
200
|
+
try:
|
201
|
+
app = request.getfixturevalue("app")
|
202
|
+
logger.debug("> Found 'app' fixture")
|
203
|
+
except pytest.FixtureLookupError:
|
204
|
+
logger.debug("> No 'app' fixture found, trying auto-discovery...")
|
205
|
+
app = auto_discover_app()
|
206
|
+
return app
|
207
|
+
|
208
|
+
|
209
|
+
@pytest.fixture
|
210
|
+
def coverage_client(request: pytest.FixtureRequest) -> Any:
|
211
|
+
"""
|
212
|
+
Smart auto-discovering test coverage_client that records API calls for coverage.
|
213
|
+
|
214
|
+
Tries to find an 'app' fixture first, then auto-discovers apps in common locations.
|
215
|
+
Can also wrap existing custom fixtures if configured.
|
216
|
+
"""
|
217
|
+
session = request.node.session
|
218
|
+
|
219
|
+
if not session.config.getoption("--api-cov-report"):
|
220
|
+
pytest.skip("API coverage not enabled. Use --api-cov-report flag.")
|
221
|
+
|
222
|
+
config = get_pytest_api_cov_report_config(request.config)
|
223
|
+
coverage_data = getattr(session, "api_coverage_data", None)
|
224
|
+
if coverage_data is None:
|
225
|
+
pytest.skip("API coverage data not initialized. This should not happen.")
|
226
|
+
|
227
|
+
# Check if we should wrap an existing fixture
|
228
|
+
if config.client_fixture_name != "coverage_client":
|
229
|
+
try:
|
230
|
+
# Get the existing custom fixture
|
231
|
+
existing_client = request.getfixturevalue(config.client_fixture_name)
|
232
|
+
logger.info(f"> Found custom fixture '{config.client_fixture_name}', wrapping with coverage tracking")
|
233
|
+
|
234
|
+
# We still need to discover endpoints, so try to get the app
|
235
|
+
app = get_app_from_fixture_or_auto_discover(request)
|
236
|
+
if app and is_supported_framework(app):
|
237
|
+
try:
|
238
|
+
adapter = get_framework_adapter(app)
|
239
|
+
if not coverage_data.discovered_endpoints.endpoints:
|
240
|
+
endpoints = adapter.get_endpoints()
|
241
|
+
framework_name = type(app).__name__
|
242
|
+
for endpoint in endpoints:
|
243
|
+
coverage_data.add_discovered_endpoint(endpoint, f"{framework_name.lower()}_adapter")
|
244
|
+
logger.info(f"> pytest-api-coverage: Discovered {len(endpoints)} endpoints.")
|
245
|
+
logger.debug(f"> Discovered endpoints: {endpoints}")
|
246
|
+
except Exception as e:
|
247
|
+
logger.warning(f"> pytest-api-coverage: Could not discover endpoints from app. Error: {e}")
|
248
|
+
|
249
|
+
# Wrap the existing client with coverage tracking
|
250
|
+
wrapped_client = wrap_client_with_coverage(existing_client, coverage_data.recorder, request.node.name)
|
251
|
+
yield wrapped_client
|
252
|
+
return
|
253
|
+
|
254
|
+
except pytest.FixtureLookupError:
|
255
|
+
logger.warning(f"> Custom fixture '{config.client_fixture_name}' not found, falling back to auto-discovery")
|
256
|
+
|
257
|
+
# Original auto-discovery logic
|
258
|
+
app = get_app_from_fixture_or_auto_discover(request)
|
259
|
+
|
260
|
+
if app is None:
|
261
|
+
helpful_msg = get_helpful_error_message()
|
262
|
+
print(helpful_msg)
|
263
|
+
pytest.skip("No API app found. See error message above for setup guidance.")
|
264
|
+
|
265
|
+
if not is_supported_framework(app):
|
266
|
+
pytest.skip(f"Unsupported framework: {type(app).__name__}. pytest-api-coverage supports Flask and FastAPI.")
|
267
|
+
|
268
|
+
try:
|
269
|
+
adapter = get_framework_adapter(app)
|
270
|
+
except TypeError as e:
|
271
|
+
pytest.skip(f"Framework detection failed: {e}")
|
272
|
+
|
273
|
+
if not coverage_data.discovered_endpoints.endpoints:
|
274
|
+
try:
|
275
|
+
endpoints = adapter.get_endpoints()
|
276
|
+
framework_name = type(app).__name__
|
277
|
+
for endpoint in endpoints:
|
278
|
+
coverage_data.add_discovered_endpoint(endpoint, f"{framework_name.lower()}_adapter")
|
279
|
+
logger.info(f"> pytest-api-coverage: Discovered {len(endpoints)} endpoints.")
|
280
|
+
logger.debug(f"> Discovered endpoints: {endpoints}")
|
281
|
+
except Exception as e:
|
282
|
+
logger.warning(f"> pytest-api-coverage: Could not discover endpoints. Error: {e}")
|
283
|
+
|
284
|
+
client = adapter.get_tracked_client(coverage_data.recorder, request.node.name)
|
285
|
+
yield client
|
286
|
+
|
287
|
+
|
288
|
+
def pytest_sessionfinish(session: pytest.Session) -> None:
|
289
|
+
"""Generate the API coverage report at the end of the session."""
|
290
|
+
if session.config.getoption("--api-cov-report"):
|
291
|
+
coverage_data = getattr(session, "api_coverage_data", None)
|
292
|
+
if coverage_data is None:
|
293
|
+
logger.warning("> No API coverage data found. Plugin may not have been properly initialized.")
|
294
|
+
return
|
295
|
+
|
296
|
+
logger.debug(f"> pytest-api-coverage: Generating report for {len(coverage_data.recorder)} recorded endpoints.")
|
297
|
+
if hasattr(session.config, "workeroutput"):
|
298
|
+
serializable_recorder = coverage_data.recorder.to_serializable()
|
299
|
+
session.config.workeroutput["api_call_recorder"] = serializable_recorder
|
300
|
+
session.config.workeroutput["discovered_endpoints"] = coverage_data.discovered_endpoints.endpoints
|
301
|
+
logger.debug("> Sent API call data and discovered endpoints to master process")
|
302
|
+
else:
|
303
|
+
logger.debug("> No workeroutput found, generating report for master data.")
|
304
|
+
|
305
|
+
worker_recorder_data = getattr(session.config, "worker_api_call_recorder", {})
|
306
|
+
worker_endpoints = getattr(session.config, "worker_discovered_endpoints", [])
|
307
|
+
|
308
|
+
# Merge worker data into session data
|
309
|
+
if worker_recorder_data or worker_endpoints:
|
310
|
+
coverage_data.merge_worker_data(worker_recorder_data, worker_endpoints)
|
311
|
+
logger.debug(f"> Merged worker data: {len(worker_recorder_data)} endpoints")
|
312
|
+
|
313
|
+
logger.debug(f"> Final merged data: {len(coverage_data.recorder)} recorded endpoints")
|
314
|
+
logger.debug(f"> Using discovered endpoints: {coverage_data.discovered_endpoints.endpoints}")
|
315
|
+
|
316
|
+
api_cov_config = get_pytest_api_cov_report_config(session.config)
|
317
|
+
status = generate_pytest_api_cov_report(
|
318
|
+
api_cov_config=api_cov_config,
|
319
|
+
called_data=coverage_data.recorder.calls,
|
320
|
+
discovered_endpoints=coverage_data.discovered_endpoints.endpoints,
|
321
|
+
)
|
322
|
+
if session.exitstatus == 0:
|
323
|
+
session.exitstatus = status
|
324
|
+
|
325
|
+
if hasattr(session, "api_coverage_data"):
|
326
|
+
delattr(session, "api_coverage_data")
|
327
|
+
|
328
|
+
if hasattr(session.config, "worker_api_call_recorder"):
|
329
|
+
delattr(session.config, "worker_api_call_recorder")
|
330
|
+
|
331
|
+
|
332
|
+
class DeferXdistPlugin:
|
333
|
+
"""Simple class to defer pytest-xdist hook until we know it is installed."""
|
334
|
+
|
335
|
+
def pytest_testnodedown(self, node: Any) -> None:
|
336
|
+
"""Collect API call data from each worker as they finish."""
|
337
|
+
logger.debug("> pytest-api-coverage: Worker node down.")
|
338
|
+
worker_data = node.workeroutput.get("api_call_recorder", {})
|
339
|
+
discovered_endpoints = node.workeroutput.get("discovered_endpoints", [])
|
340
|
+
logger.debug(f"> Worker data: {worker_data}")
|
341
|
+
logger.debug(f"> Worker discovered endpoints: {discovered_endpoints}")
|
342
|
+
|
343
|
+
# Merge API call data
|
344
|
+
if worker_data:
|
345
|
+
logger.debug("> Worker data found, merging with current data.")
|
346
|
+
current = getattr(node.config, "worker_api_call_recorder", {})
|
347
|
+
logger.debug(f"> Current data before merge: {current}")
|
348
|
+
|
349
|
+
# Merge the worker data into current
|
350
|
+
for endpoint, calls in worker_data.items():
|
351
|
+
if endpoint not in current:
|
352
|
+
current[endpoint] = set()
|
353
|
+
elif not isinstance(current[endpoint], set):
|
354
|
+
current[endpoint] = set(current[endpoint])
|
355
|
+
current[endpoint].update(calls)
|
356
|
+
logger.debug(f"> Updated endpoint {endpoint} with calls: {calls}")
|
357
|
+
|
358
|
+
node.config.worker_api_call_recorder = current
|
359
|
+
logger.debug(f"> Updated current data: {current}")
|
360
|
+
|
361
|
+
if discovered_endpoints and not getattr(node.config, "worker_discovered_endpoints", []):
|
362
|
+
node.config.worker_discovered_endpoints = discovered_endpoints
|
363
|
+
logger.debug(f"> Set discovered endpoints from worker: {discovered_endpoints}")
|
@@ -66,3 +66,11 @@ def add_pytest_api_cov_flags(parser: pytest.Parser) -> None:
|
|
66
66
|
default=False,
|
67
67
|
help="Disable use of API coverage sugar in console report.",
|
68
68
|
)
|
69
|
+
|
70
|
+
parser.addoption(
|
71
|
+
"--api-cov-client-fixture-name",
|
72
|
+
action="store",
|
73
|
+
type=str,
|
74
|
+
default=None,
|
75
|
+
help="Name of existing test client fixture to wrap with coverage tracking",
|
76
|
+
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|