airflow-pytest-plugin 0.3.0__tar.gz → 0.3.2__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.
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/PKG-INFO +51 -8
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/README.md +50 -7
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/pyproject.toml +1 -1
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/__init__.py +4 -0
- airflow_pytest_plugin-0.3.2/src/airflow_pytest_plugin/config.py +177 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/models.py +13 -0
- airflow_pytest_plugin-0.3.2/src/airflow_pytest_plugin/retention.py +253 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/sources/base.py +8 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/sources/filesystem.py +41 -11
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/routes/common.py +35 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/routes/compare.py +42 -2
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/routes/failures.py +22 -2
- airflow_pytest_plugin-0.3.2/src/airflow_pytest_plugin/web/routes/flaky.py +188 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/routes/monitoring.py +20 -3
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/routes/reports.py +189 -9
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/templates.py +537 -77
- airflow_pytest_plugin-0.3.2/tests/test_config.py +145 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_filesystem_source.py +43 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_models.py +23 -0
- airflow_pytest_plugin-0.3.2/tests/test_retention.py +287 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_web_app.py +206 -1
- airflow_pytest_plugin-0.3.0/src/airflow_pytest_plugin/config.py +0 -79
- airflow_pytest_plugin-0.3.0/src/airflow_pytest_plugin/web/routes/flaky.py +0 -106
- airflow_pytest_plugin-0.3.0/tests/test_config.py +0 -82
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/.gitignore +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/LICENSE +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/examples/example_dag.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/compat/__init__.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/compat/airflow.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/layout.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/plugin.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/producer/__init__.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/producer/archiving_parser.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/sources/__init__.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/version.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/__init__.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/__main__.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/app.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/web/routes/__init__.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/conftest.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_archiving_parser.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_authz_integration.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_compat.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_layout.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_package.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_plugin.py +0 -0
- {airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: airflow-pytest-plugin
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: View airflow-pytest-operator results in the Airflow 3 web UI.
|
|
5
5
|
Project-URL: Homepage, https://github.com/IKrysanov/airflow-pytest-plugin
|
|
6
6
|
Project-URL: Repository, https://github.com/IKrysanov/airflow-pytest-plugin
|
|
@@ -98,9 +98,14 @@ It has two halves that share one on-disk layout:
|
|
|
98
98
|
## Screenshots
|
|
99
99
|
|
|
100
100
|
**Overview** — the run list with the historical chart (per-status legend
|
|
101
|
-
toggles, run numbers,
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
toggles, run numbers, a carousel beyond 30 runs, an optional **pass-rate trend
|
|
102
|
+
line** with a success-threshold gridline, and **tick runs in the list to filter
|
|
103
|
+
the chart to just their trend**) beside a **flaky-tests panel** (with its own
|
|
104
|
+
search and a quarantined-only toggle), KPI cards (including a clickable **unique
|
|
105
|
+
tests** count), and Airflow-matched colours and font. The run list is **grouped by
|
|
106
|
+
dag·task** by default (a checkbox toggles the flat list) — collapsible groups with
|
|
107
|
+
run count, pass-rate, average duration and last status, each sortable on its own;
|
|
108
|
+
select a whole group to chart its trend:
|
|
104
109
|
|
|
105
110
|

|
|
106
111
|
|
|
@@ -112,9 +117,10 @@ output on expand:
|
|
|
112
117
|

|
|
113
118
|
|
|
114
119
|
**Flaky tests & comparison** — from a run, *Flaky tests* lists the tests that
|
|
115
|
-
both pass and fail across recent runs
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
both pass and fail across recent runs, each with a recent-outcome strip, a
|
|
121
|
+
**flakiness score**, a **trend** arrow, and a **quarantine** badge, over a
|
|
122
|
+
configurable analysis window; *Compare to previous* diffs it against the prior
|
|
123
|
+
run; expanding a case offers its full **history**:
|
|
118
124
|
|
|
119
125
|

|
|
120
126
|
|
|
@@ -227,9 +233,10 @@ runtime. Endpoints (relative to the mount):
|
|
|
227
233
|
| `GET /` | the single-page viewer (HTML) |
|
|
228
234
|
| `GET /api/reports?dag_id=&run_id=` | summaries, newest first |
|
|
229
235
|
| `GET /api/reports/{report_id}` | one report with per-case rows |
|
|
236
|
+
| `GET /api/groups?dag_id=&task_id=` | runs aggregated by dag·task (count, pass-rate, avg duration, last status) |
|
|
230
237
|
| `GET /api/failures?dag_id=&run_id=&task_id=` | failed/errored cases across the visible runs |
|
|
231
238
|
| `GET /api/compare?base=&head=` | per-test diff between two runs (newly failed / fixed / …) |
|
|
232
|
-
| `GET /api/flaky?dag_id=&task_id=&window=` | tests
|
|
239
|
+
| `GET /api/flaky?dag_id=&task_id=&window=` | flaky tests with score, trend, and a quarantine flag |
|
|
233
240
|
| `GET /api/test-history?dag_id=&task_id=&node_id=&limit=` | one test's outcome per run |
|
|
234
241
|
| `GET /api/unique-tests?dag_id=&task_id=&run_id=&full=` | distinct test count (+ list when `full`) |
|
|
235
242
|
| `DELETE /api/reports/{report_id}` | delete a report (RBAC-gated) |
|
|
@@ -295,6 +302,13 @@ Download them from a report's detail view, or `GET
|
|
|
295
302
|
| built-in default | `/opt/airflow/pytest-reports` | fallback |
|
|
296
303
|
| `AIRFLOW_PYTEST_PLUGIN_ENABLE` (env) | `True` | reader on/off — see below |
|
|
297
304
|
| `AIRFLOW_PYTEST_SCAN_CACHE_TTL` (env) | `2.0` | seconds a directory scan is reused (`0` disables) |
|
|
305
|
+
| `AIRFLOW_PYTEST_RETENTION_MAX_AGE_DAYS` (env/cfg) | — | delete runs older than N days |
|
|
306
|
+
| `AIRFLOW_PYTEST_RETENTION_MAX_RUNS` (env/cfg) | — | keep at most N newest runs per dag·task |
|
|
307
|
+
| `AIRFLOW_PYTEST_RETENTION_MAX_TOTAL_MB` (env/cfg) | — | total report-tree budget in MB |
|
|
308
|
+
| `AIRFLOW_PYTEST_FLAKY_WINDOW` (env/cfg) | `30` | default recent runs the flaky detector scans |
|
|
309
|
+
| `AIRFLOW_PYTEST_FLAKY_QUARANTINE_SCORE` (env/cfg) | `0.5` | flakiness score (0–1) that flags a test for quarantine |
|
|
310
|
+
| `AIRFLOW_PYTEST_FLAKY_MIN_SCORE` (env/cfg) | `0.1` | flakiness score (0–1) below which a test is not counted as flaky |
|
|
311
|
+
| `AIRFLOW_PYTEST_SUCCESS_THRESHOLD` (env/cfg) | `0.85` | pass-rate (0–1) over executed tests at/above which a run counts as successful (*Passing runs*); `1.0` = strict, zero failures/errors |
|
|
298
312
|
|
|
299
313
|
**Enable / disable the reader.** Set `AIRFLOW_PYTEST_PLUGIN_ENABLE` to a falsey
|
|
300
314
|
value (`0`, `false`, `no`, `off`) to stop the plugin registering its UI and API
|
|
@@ -314,6 +328,34 @@ single scan for `AIRFLOW_PYTEST_SCAN_CACHE_TTL` seconds (default `2.0`; deletes
|
|
|
314
328
|
invalidate it immediately). New runs therefore appear within a couple of seconds (or
|
|
315
329
|
on **Refresh**); set it to `0` for no caching, or higher on a very large tree.
|
|
316
330
|
|
|
331
|
+
## Retention (auto-cleanup)
|
|
332
|
+
|
|
333
|
+
Reports accumulate forever unless you prune them. Set any of the
|
|
334
|
+
`AIRFLOW_PYTEST_RETENTION_*` knobs above (all opt-in; unset = keep everything) and
|
|
335
|
+
schedule `prune_reports` from a maintenance DAG:
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
from airflow import DAG
|
|
339
|
+
from airflow.providers.standard.operators.python import PythonOperator
|
|
340
|
+
from airflow_pytest_plugin import prune_reports
|
|
341
|
+
|
|
342
|
+
with DAG("pytest_reports_retention", schedule="@daily", catchup=False, ...):
|
|
343
|
+
PythonOperator(task_id="prune", python_callable=prune_reports)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Each knob is a dimension and they combine as a union — a run is deleted if **any**
|
|
347
|
+
applies:
|
|
348
|
+
|
|
349
|
+
- **age** — older than `…_MAX_AGE_DAYS`;
|
|
350
|
+
- **count** — beyond the newest `…_MAX_RUNS` of its dag·task;
|
|
351
|
+
- **size** — oldest-first until the tree fits `…_MAX_TOTAL_MB`.
|
|
352
|
+
|
|
353
|
+
The **newest run of each dag·task is always kept**, so a task's latest result never
|
|
354
|
+
disappears. `prune_reports(dry_run=True)` reports what *would* go without deleting
|
|
355
|
+
(its `RetentionResult` carries `deleted`, `freed_bytes`, `scanned`). Cleanup is
|
|
356
|
+
scheduler-driven — the plugin never deletes on its own. For a custom policy, build a
|
|
357
|
+
`RetentionPolicy(...)` and pass it (`prune_reports(policy)`).
|
|
358
|
+
|
|
317
359
|
## Architecture (SOLID)
|
|
318
360
|
|
|
319
361
|
Mirrors the operator's layering — each piece has one reason to change:
|
|
@@ -324,6 +366,7 @@ Mirrors the operator's layering — each piece has one reason to change:
|
|
|
324
366
|
| `producer.ArchivingResultParser` | write JUnit XML + `meta.json` (extends the operator's parser) |
|
|
325
367
|
| `sources.ReportSource` / `FileSystemReportSource` | read/index reports behind an interface (Dependency Inversion) |
|
|
326
368
|
| `web.create_app` | map HTTP onto a `ReportSource` — knows nothing about the filesystem |
|
|
369
|
+
| `retention` | pure `select_expired` decision + a `prune` orchestrator over any `ReportSource` |
|
|
327
370
|
| `plugin.PytestReportsPlugin` | register the app with Airflow |
|
|
328
371
|
| `compat` | the only module that imports Airflow; version differences resolved once |
|
|
329
372
|
| `models` | JSON-serializable view types; the web layer never sees operator types |
|
|
@@ -54,9 +54,14 @@ It has two halves that share one on-disk layout:
|
|
|
54
54
|
## Screenshots
|
|
55
55
|
|
|
56
56
|
**Overview** — the run list with the historical chart (per-status legend
|
|
57
|
-
toggles, run numbers,
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
toggles, run numbers, a carousel beyond 30 runs, an optional **pass-rate trend
|
|
58
|
+
line** with a success-threshold gridline, and **tick runs in the list to filter
|
|
59
|
+
the chart to just their trend**) beside a **flaky-tests panel** (with its own
|
|
60
|
+
search and a quarantined-only toggle), KPI cards (including a clickable **unique
|
|
61
|
+
tests** count), and Airflow-matched colours and font. The run list is **grouped by
|
|
62
|
+
dag·task** by default (a checkbox toggles the flat list) — collapsible groups with
|
|
63
|
+
run count, pass-rate, average duration and last status, each sortable on its own;
|
|
64
|
+
select a whole group to chart its trend:
|
|
60
65
|
|
|
61
66
|

|
|
62
67
|
|
|
@@ -68,9 +73,10 @@ output on expand:
|
|
|
68
73
|

|
|
69
74
|
|
|
70
75
|
**Flaky tests & comparison** — from a run, *Flaky tests* lists the tests that
|
|
71
|
-
both pass and fail across recent runs
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
both pass and fail across recent runs, each with a recent-outcome strip, a
|
|
77
|
+
**flakiness score**, a **trend** arrow, and a **quarantine** badge, over a
|
|
78
|
+
configurable analysis window; *Compare to previous* diffs it against the prior
|
|
79
|
+
run; expanding a case offers its full **history**:
|
|
74
80
|
|
|
75
81
|

|
|
76
82
|
|
|
@@ -183,9 +189,10 @@ runtime. Endpoints (relative to the mount):
|
|
|
183
189
|
| `GET /` | the single-page viewer (HTML) |
|
|
184
190
|
| `GET /api/reports?dag_id=&run_id=` | summaries, newest first |
|
|
185
191
|
| `GET /api/reports/{report_id}` | one report with per-case rows |
|
|
192
|
+
| `GET /api/groups?dag_id=&task_id=` | runs aggregated by dag·task (count, pass-rate, avg duration, last status) |
|
|
186
193
|
| `GET /api/failures?dag_id=&run_id=&task_id=` | failed/errored cases across the visible runs |
|
|
187
194
|
| `GET /api/compare?base=&head=` | per-test diff between two runs (newly failed / fixed / …) |
|
|
188
|
-
| `GET /api/flaky?dag_id=&task_id=&window=` | tests
|
|
195
|
+
| `GET /api/flaky?dag_id=&task_id=&window=` | flaky tests with score, trend, and a quarantine flag |
|
|
189
196
|
| `GET /api/test-history?dag_id=&task_id=&node_id=&limit=` | one test's outcome per run |
|
|
190
197
|
| `GET /api/unique-tests?dag_id=&task_id=&run_id=&full=` | distinct test count (+ list when `full`) |
|
|
191
198
|
| `DELETE /api/reports/{report_id}` | delete a report (RBAC-gated) |
|
|
@@ -251,6 +258,13 @@ Download them from a report's detail view, or `GET
|
|
|
251
258
|
| built-in default | `/opt/airflow/pytest-reports` | fallback |
|
|
252
259
|
| `AIRFLOW_PYTEST_PLUGIN_ENABLE` (env) | `True` | reader on/off — see below |
|
|
253
260
|
| `AIRFLOW_PYTEST_SCAN_CACHE_TTL` (env) | `2.0` | seconds a directory scan is reused (`0` disables) |
|
|
261
|
+
| `AIRFLOW_PYTEST_RETENTION_MAX_AGE_DAYS` (env/cfg) | — | delete runs older than N days |
|
|
262
|
+
| `AIRFLOW_PYTEST_RETENTION_MAX_RUNS` (env/cfg) | — | keep at most N newest runs per dag·task |
|
|
263
|
+
| `AIRFLOW_PYTEST_RETENTION_MAX_TOTAL_MB` (env/cfg) | — | total report-tree budget in MB |
|
|
264
|
+
| `AIRFLOW_PYTEST_FLAKY_WINDOW` (env/cfg) | `30` | default recent runs the flaky detector scans |
|
|
265
|
+
| `AIRFLOW_PYTEST_FLAKY_QUARANTINE_SCORE` (env/cfg) | `0.5` | flakiness score (0–1) that flags a test for quarantine |
|
|
266
|
+
| `AIRFLOW_PYTEST_FLAKY_MIN_SCORE` (env/cfg) | `0.1` | flakiness score (0–1) below which a test is not counted as flaky |
|
|
267
|
+
| `AIRFLOW_PYTEST_SUCCESS_THRESHOLD` (env/cfg) | `0.85` | pass-rate (0–1) over executed tests at/above which a run counts as successful (*Passing runs*); `1.0` = strict, zero failures/errors |
|
|
254
268
|
|
|
255
269
|
**Enable / disable the reader.** Set `AIRFLOW_PYTEST_PLUGIN_ENABLE` to a falsey
|
|
256
270
|
value (`0`, `false`, `no`, `off`) to stop the plugin registering its UI and API
|
|
@@ -270,6 +284,34 @@ single scan for `AIRFLOW_PYTEST_SCAN_CACHE_TTL` seconds (default `2.0`; deletes
|
|
|
270
284
|
invalidate it immediately). New runs therefore appear within a couple of seconds (or
|
|
271
285
|
on **Refresh**); set it to `0` for no caching, or higher on a very large tree.
|
|
272
286
|
|
|
287
|
+
## Retention (auto-cleanup)
|
|
288
|
+
|
|
289
|
+
Reports accumulate forever unless you prune them. Set any of the
|
|
290
|
+
`AIRFLOW_PYTEST_RETENTION_*` knobs above (all opt-in; unset = keep everything) and
|
|
291
|
+
schedule `prune_reports` from a maintenance DAG:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from airflow import DAG
|
|
295
|
+
from airflow.providers.standard.operators.python import PythonOperator
|
|
296
|
+
from airflow_pytest_plugin import prune_reports
|
|
297
|
+
|
|
298
|
+
with DAG("pytest_reports_retention", schedule="@daily", catchup=False, ...):
|
|
299
|
+
PythonOperator(task_id="prune", python_callable=prune_reports)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Each knob is a dimension and they combine as a union — a run is deleted if **any**
|
|
303
|
+
applies:
|
|
304
|
+
|
|
305
|
+
- **age** — older than `…_MAX_AGE_DAYS`;
|
|
306
|
+
- **count** — beyond the newest `…_MAX_RUNS` of its dag·task;
|
|
307
|
+
- **size** — oldest-first until the tree fits `…_MAX_TOTAL_MB`.
|
|
308
|
+
|
|
309
|
+
The **newest run of each dag·task is always kept**, so a task's latest result never
|
|
310
|
+
disappears. `prune_reports(dry_run=True)` reports what *would* go without deleting
|
|
311
|
+
(its `RetentionResult` carries `deleted`, `freed_bytes`, `scanned`). Cleanup is
|
|
312
|
+
scheduler-driven — the plugin never deletes on its own. For a custom policy, build a
|
|
313
|
+
`RetentionPolicy(...)` and pass it (`prune_reports(policy)`).
|
|
314
|
+
|
|
273
315
|
## Architecture (SOLID)
|
|
274
316
|
|
|
275
317
|
Mirrors the operator's layering — each piece has one reason to change:
|
|
@@ -280,6 +322,7 @@ Mirrors the operator's layering — each piece has one reason to change:
|
|
|
280
322
|
| `producer.ArchivingResultParser` | write JUnit XML + `meta.json` (extends the operator's parser) |
|
|
281
323
|
| `sources.ReportSource` / `FileSystemReportSource` | read/index reports behind an interface (Dependency Inversion) |
|
|
282
324
|
| `web.create_app` | map HTTP onto a `ReportSource` — knows nothing about the filesystem |
|
|
325
|
+
| `retention` | pure `select_expired` decision + a `prune` orchestrator over any `ReportSource` |
|
|
283
326
|
| `plugin.PytestReportsPlugin` | register the app with Airflow |
|
|
284
327
|
| `compat` | the only module that imports Airflow; version differences resolved once |
|
|
285
328
|
| `models` | JSON-serializable view types; the web layer never sees operator types |
|
{airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/__init__.py
RENAMED
|
@@ -22,6 +22,7 @@ from .config import get_reports_root
|
|
|
22
22
|
from .layout import ReportLayout
|
|
23
23
|
from .models import CaseView, ReportDetail, ReportRef, ReportSummary
|
|
24
24
|
from .producer import ArchivingResultParser
|
|
25
|
+
from .retention import RetentionPolicy, RetentionResult, prune_reports
|
|
25
26
|
from .sources import FileSystemReportSource, ReportSource
|
|
26
27
|
from .version import __version__ as __version__
|
|
27
28
|
|
|
@@ -38,6 +39,9 @@ __all__ = [
|
|
|
38
39
|
"ReportDetail",
|
|
39
40
|
"CaseView",
|
|
40
41
|
"ReportLayout",
|
|
42
|
+
"RetentionPolicy",
|
|
43
|
+
"RetentionResult",
|
|
44
|
+
"prune_reports",
|
|
41
45
|
"get_reports_root",
|
|
42
46
|
"create_app",
|
|
43
47
|
"__version__",
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Copyright 2026 the airflow-pytest-plugin contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Resolve the report root (env, then Airflow config, then default)."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from .compat import get_conf_value
|
|
22
|
+
|
|
23
|
+
ENV_VAR = "AIRFLOW_PYTEST_REPORTS_ROOT"
|
|
24
|
+
CONF_SECTION = "pytest_reports"
|
|
25
|
+
CONF_KEY = "reports_root"
|
|
26
|
+
DEFAULT_ROOT = "/opt/airflow/pytest-reports"
|
|
27
|
+
|
|
28
|
+
#: Toggles whether the reader plugin (UI + API) registers with Airflow. Default on.
|
|
29
|
+
ENABLE_ENV_VAR = "AIRFLOW_PYTEST_PLUGIN_ENABLE"
|
|
30
|
+
_FALSEY = frozenset({"0", "false", "no", "off", "n", "f"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_plugin_enabled() -> bool:
|
|
34
|
+
"""Whether the reader plugin should register with Airflow.
|
|
35
|
+
|
|
36
|
+
Reads ``AIRFLOW_PYTEST_PLUGIN_ENABLE`` -- ``True`` (the default when unset/empty)
|
|
37
|
+
registers the UI + API; a falsey value (``0``/``false``/``no``/``off``) disables
|
|
38
|
+
it. Only gates the reader; the producer-side parser is unaffected.
|
|
39
|
+
"""
|
|
40
|
+
raw = os.environ.get(ENABLE_ENV_VAR)
|
|
41
|
+
if raw is None or not raw.strip():
|
|
42
|
+
return True
|
|
43
|
+
return raw.strip().lower() not in _FALSEY
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
#: How long (seconds) the filesystem source may reuse a directory scan. A short
|
|
47
|
+
#: window lets the several summary-driven endpoints on one page load (list + flaky +
|
|
48
|
+
#: unique-tests, plus filter typing) share one tree walk instead of rescanning each.
|
|
49
|
+
SCAN_TTL_ENV_VAR = "AIRFLOW_PYTEST_SCAN_CACHE_TTL"
|
|
50
|
+
DEFAULT_SCAN_TTL = 2.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_scan_cache_ttl() -> float:
|
|
54
|
+
"""Resolve the directory-scan cache TTL in seconds (``0`` disables caching).
|
|
55
|
+
|
|
56
|
+
Reads ``AIRFLOW_PYTEST_SCAN_CACHE_TTL``; falls back to ``DEFAULT_SCAN_TTL``.
|
|
57
|
+
A malformed or negative value falls back to the default.
|
|
58
|
+
"""
|
|
59
|
+
raw = os.environ.get(SCAN_TTL_ENV_VAR)
|
|
60
|
+
if raw is None or not raw.strip():
|
|
61
|
+
return DEFAULT_SCAN_TTL
|
|
62
|
+
try:
|
|
63
|
+
ttl = float(raw.strip())
|
|
64
|
+
except ValueError:
|
|
65
|
+
return DEFAULT_SCAN_TTL
|
|
66
|
+
return ttl if ttl >= 0 else DEFAULT_SCAN_TTL
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
#: Retention knobs (all opt-in; unset = keep everything). Each reads its env var,
|
|
70
|
+
#: then the matching ``[pytest_reports]`` cfg key. Non-positive/invalid -> unset.
|
|
71
|
+
RETENTION_MAX_AGE_DAYS_ENV = "AIRFLOW_PYTEST_RETENTION_MAX_AGE_DAYS"
|
|
72
|
+
RETENTION_MAX_RUNS_ENV = "AIRFLOW_PYTEST_RETENTION_MAX_RUNS"
|
|
73
|
+
RETENTION_MAX_TOTAL_MB_ENV = "AIRFLOW_PYTEST_RETENTION_MAX_TOTAL_MB"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _positive_int_setting(env_var: str, conf_key: str) -> int | None:
|
|
77
|
+
raw = os.environ.get(env_var)
|
|
78
|
+
if raw is None or not raw.strip():
|
|
79
|
+
raw = get_conf_value(CONF_SECTION, conf_key)
|
|
80
|
+
if raw is None or not str(raw).strip():
|
|
81
|
+
return None
|
|
82
|
+
try:
|
|
83
|
+
value = int(str(raw).strip())
|
|
84
|
+
except ValueError:
|
|
85
|
+
return None
|
|
86
|
+
return value if value > 0 else None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_retention_max_age_days() -> int | None:
|
|
90
|
+
"""Delete runs older than this many days (``None`` = no age limit)."""
|
|
91
|
+
return _positive_int_setting(RETENTION_MAX_AGE_DAYS_ENV, "retention_max_age_days")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_retention_max_runs() -> int | None:
|
|
95
|
+
"""Keep at most this many newest runs per dag·task (``None`` = unlimited)."""
|
|
96
|
+
return _positive_int_setting(RETENTION_MAX_RUNS_ENV, "retention_max_runs")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_retention_max_total_mb() -> int | None:
|
|
100
|
+
"""Total report-tree budget in MB (``None`` = unlimited)."""
|
|
101
|
+
return _positive_int_setting(RETENTION_MAX_TOTAL_MB_ENV, "retention_max_total_mb")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
#: Flaky-detector defaults (overridable per request via the ``window`` query param).
|
|
105
|
+
FLAKY_WINDOW_ENV = "AIRFLOW_PYTEST_FLAKY_WINDOW"
|
|
106
|
+
DEFAULT_FLAKY_WINDOW = 30
|
|
107
|
+
FLAKY_QUARANTINE_SCORE_ENV = "AIRFLOW_PYTEST_FLAKY_QUARANTINE_SCORE"
|
|
108
|
+
DEFAULT_FLAKY_QUARANTINE_SCORE = 0.5
|
|
109
|
+
#: Flakiness-score floor: below it a test is too steady to count as flaky, so a lone
|
|
110
|
+
#: blip in a long history (a near-zero flip rate) drops off the list.
|
|
111
|
+
FLAKY_MIN_SCORE_ENV = "AIRFLOW_PYTEST_FLAKY_MIN_SCORE"
|
|
112
|
+
DEFAULT_FLAKY_MIN_SCORE = 0.1
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_flaky_window() -> int:
|
|
116
|
+
"""Default number of recent runs the flaky detector looks at per dag·task."""
|
|
117
|
+
return (
|
|
118
|
+
_positive_int_setting(FLAKY_WINDOW_ENV, "flaky_window") or DEFAULT_FLAKY_WINDOW
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _unit_float_setting(env_var: str, conf_key: str, default: float) -> float:
|
|
123
|
+
"""A 0–1 float from env, then cfg; default on missing/invalid/out-of-range."""
|
|
124
|
+
raw = os.environ.get(env_var)
|
|
125
|
+
if raw is None or not raw.strip():
|
|
126
|
+
raw = get_conf_value(CONF_SECTION, conf_key)
|
|
127
|
+
if raw is None or not str(raw).strip():
|
|
128
|
+
return default
|
|
129
|
+
try:
|
|
130
|
+
value = float(str(raw).strip())
|
|
131
|
+
except ValueError:
|
|
132
|
+
return default
|
|
133
|
+
return value if 0.0 <= value <= 1.0 else default
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_flaky_quarantine_score() -> float:
|
|
137
|
+
"""Flakiness score (0–1) at/above which a test is flagged for quarantine."""
|
|
138
|
+
return _unit_float_setting(
|
|
139
|
+
FLAKY_QUARANTINE_SCORE_ENV,
|
|
140
|
+
"flaky_quarantine_score",
|
|
141
|
+
DEFAULT_FLAKY_QUARANTINE_SCORE,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_flaky_min_score() -> float:
|
|
146
|
+
"""Flakiness score (0–1) below which a test is NOT counted as flaky."""
|
|
147
|
+
return _unit_float_setting(
|
|
148
|
+
FLAKY_MIN_SCORE_ENV, "flaky_min_score", DEFAULT_FLAKY_MIN_SCORE
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
#: Pass-rate (0–1) at/above which a run counts as successful ("Passing runs"). The
|
|
153
|
+
#: rate is over executed tests, so a run can carry a few failures and still pass.
|
|
154
|
+
#: At 1.0 a run is successful only with zero failures/errors (the strict default of
|
|
155
|
+
#: older versions). 0.85 is a common "good enough" bar (ISTQB defines no fixed number).
|
|
156
|
+
SUCCESS_THRESHOLD_ENV = "AIRFLOW_PYTEST_SUCCESS_THRESHOLD"
|
|
157
|
+
DEFAULT_SUCCESS_THRESHOLD = 0.85
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_success_threshold() -> float:
|
|
161
|
+
"""Pass-rate (0–1) at/above which a run counts as successful."""
|
|
162
|
+
return _unit_float_setting(
|
|
163
|
+
SUCCESS_THRESHOLD_ENV, "success_threshold", DEFAULT_SUCCESS_THRESHOLD
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_reports_root() -> str:
|
|
168
|
+
"""Resolve the report root directory (absolute path)."""
|
|
169
|
+
env = os.environ.get(ENV_VAR)
|
|
170
|
+
if env and env.strip():
|
|
171
|
+
return os.path.abspath(env.strip())
|
|
172
|
+
|
|
173
|
+
conf = get_conf_value(CONF_SECTION, CONF_KEY)
|
|
174
|
+
if conf and conf.strip():
|
|
175
|
+
return os.path.abspath(conf.strip())
|
|
176
|
+
|
|
177
|
+
return os.path.abspath(DEFAULT_ROOT)
|
{airflow_pytest_plugin-0.3.0 → airflow_pytest_plugin-0.3.2}/src/airflow_pytest_plugin/models.py
RENAMED
|
@@ -117,6 +117,19 @@ class ReportSummary:
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
def run_succeeds(passed: int, failed: int, errors: int, threshold: float) -> bool:
|
|
121
|
+
"""Whether a run counts as successful at a given pass-rate ``threshold`` (0–1).
|
|
122
|
+
|
|
123
|
+
The rate is over *executed* tests -- ``passed / (passed + failed + errors)`` --
|
|
124
|
+
so skipped tests neither help nor hurt; a run with nothing executed is a pass.
|
|
125
|
+
At ``threshold == 1.0`` this is exactly "no failures or errors".
|
|
126
|
+
"""
|
|
127
|
+
executed = passed + failed + errors
|
|
128
|
+
if executed <= 0:
|
|
129
|
+
return True
|
|
130
|
+
return passed / executed >= threshold
|
|
131
|
+
|
|
132
|
+
|
|
120
133
|
@dataclass(frozen=True)
|
|
121
134
|
class CaseView:
|
|
122
135
|
"""One test case, flattened for the detail table."""
|