pytest-json-ctrf 0.5.2__tar.gz → 0.6.0__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_json_ctrf-0.6.0/MANIFEST.in +2 -0
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/PKG-INFO +32 -10
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/README.md +30 -8
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/ctrf/Report.py +37 -30
- pytest_json_ctrf-0.6.0/ctrf/TestObject.py +166 -0
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/ctrf/main.py +19 -1
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/pyproject.toml +8 -7
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/pytest_json_ctrf.egg-info/PKG-INFO +32 -10
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/pytest_json_ctrf.egg-info/SOURCES.txt +2 -3
- pytest_json_ctrf-0.6.0/pytest_json_ctrf.egg-info/top_level.txt +1 -0
- pytest_json_ctrf-0.5.2/ctrf/TestObject.py +0 -107
- pytest_json_ctrf-0.5.2/pytest_json_ctrf.egg-info/top_level.txt +0 -2
- pytest_json_ctrf-0.5.2/tests/test_example.py +0 -65
- pytest_json_ctrf-0.5.2/tests/test_plugin.py +0 -91
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/ctrf/BaseMetadataReport.py +0 -0
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/ctrf/CommonTestReportPlugin.py +0 -0
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/ctrf/__init__.py +0 -0
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/pytest_json_ctrf.egg-info/dependency_links.txt +0 -0
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/pytest_json_ctrf.egg-info/entry_points.txt +0 -0
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/pytest_json_ctrf.egg-info/requires.txt +0 -0
- {pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-json-ctrf
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Pytest plugin to generate json report in CTRF (Common Test Report Format)
|
|
5
5
|
Author: Oleksii Ostapov
|
|
6
|
-
Project-URL:
|
|
6
|
+
Project-URL: QAMania, https://qamania.org
|
|
7
7
|
Project-URL: Homepage, https://github.com/infopulse/pytest-common-test-report-json
|
|
8
8
|
Classifier: Framework :: Pytest
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -24,7 +24,9 @@ Do not worry if report in GitHub does not appear immediately. It takes some time
|
|
|
24
24
|
- Generates JSON report
|
|
25
25
|
- Tested to work correctly with and without [pytest-xdist](https://pypi.org/project/pytest-xdist/)
|
|
26
26
|
- Tested to get browser name from [pytest-playwright](https://pypi.org/project/pytest-playwright/)
|
|
27
|
-
-
|
|
27
|
+
- Reruns from [pytest-rerunfailures](https://pypi.org/project/pytest-rerunfailures/) are reported as `retries`, `retryAttempts` and `flaky`
|
|
28
|
+
- Each test gets a `suite` hierarchy: file, then classes
|
|
29
|
+
- Parametrized tests are supported: each case is reported as a separate test named by its full pytest node id (e.g. `test_file.py::test_param[1]`), with its values in `parameters`
|
|
28
30
|
|
|
29
31
|
## Installation
|
|
30
32
|
|
|
@@ -41,13 +43,20 @@ pytest --ctrf report.json
|
|
|
41
43
|
```
|
|
42
44
|
Environment Variables may be used to specify the required Environment Object
|
|
43
45
|
fields when using the CTRF [slack-test-reporter][ctrf-slack-test-reporter-url].
|
|
46
|
+
All of them are optional: a variable that is not set is left out of the report, see [.env.example](./.env.example).
|
|
44
47
|
|
|
45
48
|
```bash
|
|
46
49
|
CTRF_BUILD_NAME="Pytest JSON CTRF Report"
|
|
47
|
-
CTRF_BUILD_NUMBER=
|
|
48
|
-
CTRF_BUILD_URL="https://ctrf.io
|
|
50
|
+
CTRF_BUILD_NUMBER=123
|
|
51
|
+
CTRF_BUILD_URL="https://ctrf.io"
|
|
52
|
+
CTRF_TEST_ENVIRONMENT="staging"
|
|
53
|
+
CTRF_TEST_TYPE="e2e"
|
|
49
54
|
```
|
|
50
55
|
|
|
56
|
+
`CTRF_BUILD_NUMBER` must be an integer. Otherwise a warning is issued and `buildNumber` is omitted from the report.
|
|
57
|
+
`osPlatform`, `osRelease` and `osVersion` are detected automatically.
|
|
58
|
+
`CTRF_TEST_TYPE` is written to every test's `type` field.
|
|
59
|
+
|
|
51
60
|
## JSON example
|
|
52
61
|
|
|
53
62
|
More info here: https://ctrf.io/docs/schema/examples
|
|
@@ -55,11 +64,12 @@ More info here: https://ctrf.io/docs/schema/examples
|
|
|
55
64
|
```json
|
|
56
65
|
{
|
|
57
66
|
"reportFormat": "CTRF",
|
|
58
|
-
"specVersion": "
|
|
59
|
-
"generatedBy": "pytest",
|
|
67
|
+
"specVersion": "1.0.0",
|
|
68
|
+
"generatedBy": "pytest",
|
|
60
69
|
"results": {
|
|
61
70
|
"tool": {
|
|
62
|
-
"name": "
|
|
71
|
+
"name": "pytest",
|
|
72
|
+
"version": "9.0.3"
|
|
63
73
|
},
|
|
64
74
|
"summary": {
|
|
65
75
|
"tests": 3,
|
|
@@ -73,8 +83,12 @@ More info here: https://ctrf.io/docs/schema/examples
|
|
|
73
83
|
},
|
|
74
84
|
"environment": {
|
|
75
85
|
"buildName": "Pytest JSON CTRF Report",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
86
|
+
"buildUrl": "https://ctrf.io",
|
|
87
|
+
"osPlatform": "linux",
|
|
88
|
+
"osRelease": "6.8.0-45-generic",
|
|
89
|
+
"osVersion": "#45-Ubuntu SMP PREEMPT_DYNAMIC",
|
|
90
|
+
"testEnvironment": "staging",
|
|
91
|
+
"buildNumber": 123
|
|
78
92
|
},
|
|
79
93
|
"tests": [
|
|
80
94
|
{
|
|
@@ -114,6 +128,14 @@ The `pytest_runtest_logreport` hook in the controller node is used to collect th
|
|
|
114
128
|
- https://github.com/infopulse/Playwright-course-python -> The report will be used in the demo project as +1 report option
|
|
115
129
|
|
|
116
130
|
## Changelog
|
|
131
|
+
- 0.6.0 - fixed issue 12 - report now validates against the CTRF schema. Thanks to [@jamesarosen](https://github.com/jamesarosen) for the detailed report!
|
|
132
|
+
- `filePath`, `rawStatus` and integer `buildNumber`; environment fields are added only when their variables are set (no defaults), plus `osPlatform`, `osRelease`, `osVersion` and `type` (`CTRF_TEST_TYPE`)
|
|
133
|
+
- parametrized tests are named by the full node id and get `parameters`
|
|
134
|
+
- `message` is the real failure message; skip and xfail reasons are reported in `message`
|
|
135
|
+
- reruns (pytest-rerunfailures) are reported as `retries`, `retryAttempts` and `flaky`
|
|
136
|
+
- new `suite` field and summary `other`, `flaky`, `suites`, `duration`
|
|
137
|
+
- CI validates reports against the CTRF schema
|
|
138
|
+
- 0.5.3 - fixed issue 6 - @pytest.mark.parametrize will no longer be added to tags to prevent exhausting memory
|
|
117
139
|
- 0.5.1 - added mandatory root object fields: reportFormat, specVersion, generatedBy
|
|
118
140
|
- 0.5.0 - Changed logic of handling parametrized tests. Previously they were marked as retries, now they are reported as separate tests
|
|
119
141
|
- 0.4.1 - Introduced backward compatibility with python 3.8, fixed issues with the time formats
|
|
@@ -9,7 +9,9 @@ Do not worry if report in GitHub does not appear immediately. It takes some time
|
|
|
9
9
|
- Generates JSON report
|
|
10
10
|
- Tested to work correctly with and without [pytest-xdist](https://pypi.org/project/pytest-xdist/)
|
|
11
11
|
- Tested to get browser name from [pytest-playwright](https://pypi.org/project/pytest-playwright/)
|
|
12
|
-
-
|
|
12
|
+
- Reruns from [pytest-rerunfailures](https://pypi.org/project/pytest-rerunfailures/) are reported as `retries`, `retryAttempts` and `flaky`
|
|
13
|
+
- Each test gets a `suite` hierarchy: file, then classes
|
|
14
|
+
- Parametrized tests are supported: each case is reported as a separate test named by its full pytest node id (e.g. `test_file.py::test_param[1]`), with its values in `parameters`
|
|
13
15
|
|
|
14
16
|
## Installation
|
|
15
17
|
|
|
@@ -26,13 +28,20 @@ pytest --ctrf report.json
|
|
|
26
28
|
```
|
|
27
29
|
Environment Variables may be used to specify the required Environment Object
|
|
28
30
|
fields when using the CTRF [slack-test-reporter][ctrf-slack-test-reporter-url].
|
|
31
|
+
All of them are optional: a variable that is not set is left out of the report, see [.env.example](./.env.example).
|
|
29
32
|
|
|
30
33
|
```bash
|
|
31
34
|
CTRF_BUILD_NAME="Pytest JSON CTRF Report"
|
|
32
|
-
CTRF_BUILD_NUMBER=
|
|
33
|
-
CTRF_BUILD_URL="https://ctrf.io
|
|
35
|
+
CTRF_BUILD_NUMBER=123
|
|
36
|
+
CTRF_BUILD_URL="https://ctrf.io"
|
|
37
|
+
CTRF_TEST_ENVIRONMENT="staging"
|
|
38
|
+
CTRF_TEST_TYPE="e2e"
|
|
34
39
|
```
|
|
35
40
|
|
|
41
|
+
`CTRF_BUILD_NUMBER` must be an integer. Otherwise a warning is issued and `buildNumber` is omitted from the report.
|
|
42
|
+
`osPlatform`, `osRelease` and `osVersion` are detected automatically.
|
|
43
|
+
`CTRF_TEST_TYPE` is written to every test's `type` field.
|
|
44
|
+
|
|
36
45
|
## JSON example
|
|
37
46
|
|
|
38
47
|
More info here: https://ctrf.io/docs/schema/examples
|
|
@@ -40,11 +49,12 @@ More info here: https://ctrf.io/docs/schema/examples
|
|
|
40
49
|
```json
|
|
41
50
|
{
|
|
42
51
|
"reportFormat": "CTRF",
|
|
43
|
-
"specVersion": "
|
|
44
|
-
"generatedBy": "pytest",
|
|
52
|
+
"specVersion": "1.0.0",
|
|
53
|
+
"generatedBy": "pytest",
|
|
45
54
|
"results": {
|
|
46
55
|
"tool": {
|
|
47
|
-
"name": "
|
|
56
|
+
"name": "pytest",
|
|
57
|
+
"version": "9.0.3"
|
|
48
58
|
},
|
|
49
59
|
"summary": {
|
|
50
60
|
"tests": 3,
|
|
@@ -58,8 +68,12 @@ More info here: https://ctrf.io/docs/schema/examples
|
|
|
58
68
|
},
|
|
59
69
|
"environment": {
|
|
60
70
|
"buildName": "Pytest JSON CTRF Report",
|
|
61
|
-
"
|
|
62
|
-
"
|
|
71
|
+
"buildUrl": "https://ctrf.io",
|
|
72
|
+
"osPlatform": "linux",
|
|
73
|
+
"osRelease": "6.8.0-45-generic",
|
|
74
|
+
"osVersion": "#45-Ubuntu SMP PREEMPT_DYNAMIC",
|
|
75
|
+
"testEnvironment": "staging",
|
|
76
|
+
"buildNumber": 123
|
|
63
77
|
},
|
|
64
78
|
"tests": [
|
|
65
79
|
{
|
|
@@ -99,6 +113,14 @@ The `pytest_runtest_logreport` hook in the controller node is used to collect th
|
|
|
99
113
|
- https://github.com/infopulse/Playwright-course-python -> The report will be used in the demo project as +1 report option
|
|
100
114
|
|
|
101
115
|
## Changelog
|
|
116
|
+
- 0.6.0 - fixed issue 12 - report now validates against the CTRF schema. Thanks to [@jamesarosen](https://github.com/jamesarosen) for the detailed report!
|
|
117
|
+
- `filePath`, `rawStatus` and integer `buildNumber`; environment fields are added only when their variables are set (no defaults), plus `osPlatform`, `osRelease`, `osVersion` and `type` (`CTRF_TEST_TYPE`)
|
|
118
|
+
- parametrized tests are named by the full node id and get `parameters`
|
|
119
|
+
- `message` is the real failure message; skip and xfail reasons are reported in `message`
|
|
120
|
+
- reruns (pytest-rerunfailures) are reported as `retries`, `retryAttempts` and `flaky`
|
|
121
|
+
- new `suite` field and summary `other`, `flaky`, `suites`, `duration`
|
|
122
|
+
- CI validates reports against the CTRF schema
|
|
123
|
+
- 0.5.3 - fixed issue 6 - @pytest.mark.parametrize will no longer be added to tags to prevent exhausting memory
|
|
102
124
|
- 0.5.1 - added mandatory root object fields: reportFormat, specVersion, generatedBy
|
|
103
125
|
- 0.5.0 - Changed logic of handling parametrized tests. Previously they were marked as retries, now they are reported as separate tests
|
|
104
126
|
- 0.4.1 - Introduced backward compatibility with python 3.8, fixed issues with the time formats
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
2
4
|
import pytest
|
|
3
5
|
import time
|
|
4
6
|
import json
|
|
5
|
-
|
|
7
|
+
import warnings
|
|
8
|
+
from uuid import uuid4
|
|
6
9
|
from datetime import datetime, timezone
|
|
7
10
|
from pytest import TestReport
|
|
8
11
|
from collections import OrderedDict
|
|
@@ -12,7 +15,6 @@ from .TestObject import TestObject, TestStatus
|
|
|
12
15
|
class Report:
|
|
13
16
|
def __init__(self):
|
|
14
17
|
self.test_items = OrderedDict()
|
|
15
|
-
self.prepared_tests = dict()
|
|
16
18
|
self.start_time = None
|
|
17
19
|
self.stop_time = None
|
|
18
20
|
|
|
@@ -30,27 +32,47 @@ class Report:
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
def list_tests_by_status(self, status: TestStatus) -> list:
|
|
33
|
-
return [test for test in self.
|
|
35
|
+
return [test for test in self.test_items.values() if test.status == status]
|
|
34
36
|
|
|
35
37
|
def _get_summary(self) -> dict:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
tests = list(self.test_items.values())
|
|
39
|
+
summary = {
|
|
40
|
+
'tests': len(tests),
|
|
38
41
|
'passed': len(self.list_tests_by_status(TestStatus.PASSED)),
|
|
39
42
|
'failed': len(self.list_tests_by_status(TestStatus.FAILED)),
|
|
40
43
|
'skipped': len(self.list_tests_by_status(TestStatus.SKIPPED)),
|
|
41
44
|
'pending': len(self.list_tests_by_status(TestStatus.PENDING)),
|
|
42
|
-
'other':
|
|
45
|
+
'other': len(self.list_tests_by_status(TestStatus.OTHER)),
|
|
46
|
+
'flaky': len([test for test in tests if test.flaky]),
|
|
47
|
+
# a suite is a file or class that directly contains tests
|
|
48
|
+
'suites': len({tuple(test.suite) for test in tests if test.suite}),
|
|
43
49
|
'start': self.start_time,
|
|
44
|
-
'stop': self.stop_time
|
|
50
|
+
'stop': self.stop_time,
|
|
45
51
|
}
|
|
52
|
+
if self.start_time is not None and self.stop_time is not None:
|
|
53
|
+
summary['duration'] = self.stop_time - self.start_time
|
|
54
|
+
return summary
|
|
46
55
|
|
|
47
56
|
@staticmethod
|
|
48
57
|
def _get_environment() -> dict:
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
58
|
+
env = {
|
|
59
|
+
"osPlatform": sys.platform,
|
|
60
|
+
"osRelease": platform.release(),
|
|
61
|
+
"osVersion": platform.version(),
|
|
53
62
|
}
|
|
63
|
+
for field, variable in (("buildName", "CTRF_BUILD_NAME"),
|
|
64
|
+
("buildUrl", "CTRF_BUILD_URL"),
|
|
65
|
+
("testEnvironment", "CTRF_TEST_ENVIRONMENT")):
|
|
66
|
+
value = os.getenv(variable)
|
|
67
|
+
if value:
|
|
68
|
+
env[field] = value
|
|
69
|
+
raw = os.getenv("CTRF_BUILD_NUMBER")
|
|
70
|
+
if raw:
|
|
71
|
+
try:
|
|
72
|
+
env["buildNumber"] = int(raw)
|
|
73
|
+
except ValueError:
|
|
74
|
+
warnings.warn(f"CTRF_BUILD_NUMBER={raw!r} is not an integer; buildNumber omitted from the report")
|
|
75
|
+
return env
|
|
54
76
|
|
|
55
77
|
def collect(self, report: TestReport) -> None:
|
|
56
78
|
if report.nodeid not in self.test_items.keys():
|
|
@@ -61,34 +83,19 @@ class Report:
|
|
|
61
83
|
test.update(report)
|
|
62
84
|
self.test_items[report.nodeid] = test
|
|
63
85
|
|
|
64
|
-
def process_retries(self) -> None:
|
|
65
|
-
for test_id, test_data in self.test_items.items():
|
|
66
|
-
name = str(test_id)
|
|
67
|
-
test: Optional[TestObject] = self.prepared_tests.get(name)
|
|
68
|
-
if test:
|
|
69
|
-
test.status = test_data.status
|
|
70
|
-
test.raw_status = test_data.raw_status
|
|
71
|
-
if test_data.status != TestStatus.PASSED:
|
|
72
|
-
test.status = test_data.status
|
|
73
|
-
test.raw_status = test_data.raw_status
|
|
74
|
-
test.message = test_data.message
|
|
75
|
-
test.trace = test_data.trace
|
|
76
|
-
test.retries += 1
|
|
77
|
-
else:
|
|
78
|
-
self.prepared_tests[name] = test_data
|
|
79
|
-
|
|
80
86
|
def get_report(self) -> dict:
|
|
81
|
-
|
|
87
|
+
test_type = os.getenv("CTRF_TEST_TYPE") or None
|
|
82
88
|
return {
|
|
83
89
|
'reportFormat': 'CTRF',
|
|
84
|
-
'specVersion': '
|
|
90
|
+
'specVersion': '1.0.0',
|
|
91
|
+
'reportId': str(uuid4()),
|
|
85
92
|
'timestamp': datetime.now(timezone.utc).isoformat(),
|
|
86
93
|
'generatedBy': 'pytest',
|
|
87
94
|
'results': {
|
|
88
95
|
"tool": self._get_tool(),
|
|
89
96
|
"summary": self._get_summary(),
|
|
90
97
|
"environment": self._get_environment(),
|
|
91
|
-
"tests": [test.serialize() for test in self.
|
|
98
|
+
"tests": [test.serialize(test_type) for test in self.test_items.values()]
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Optional, List, Dict
|
|
3
|
+
|
|
4
|
+
from pytest import TestReport
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestStatus(Enum):
|
|
8
|
+
PASSED = "passed"
|
|
9
|
+
FAILED = "failed"
|
|
10
|
+
SKIPPED = "skipped"
|
|
11
|
+
PENDING = "pending"
|
|
12
|
+
OTHER = "other"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestObject:
|
|
16
|
+
name: str
|
|
17
|
+
_status: TestStatus
|
|
18
|
+
raw_status: Optional[str]
|
|
19
|
+
start: int
|
|
20
|
+
stop: int
|
|
21
|
+
retries: int
|
|
22
|
+
message: Optional[str]
|
|
23
|
+
|
|
24
|
+
worker_id: Optional[str]
|
|
25
|
+
file_path: str
|
|
26
|
+
suite: List[str]
|
|
27
|
+
tags: List[str]
|
|
28
|
+
browser: Optional[str]
|
|
29
|
+
parameters: Optional[Dict[str, Any]]
|
|
30
|
+
trace: Optional[str]
|
|
31
|
+
retry_attempts: List[Dict[str, Any]]
|
|
32
|
+
|
|
33
|
+
def __init__(self, report: TestReport, worker_id: Optional[str] = None):
|
|
34
|
+
self.name = report.nodeid
|
|
35
|
+
self.worker_id = worker_id
|
|
36
|
+
self.file_path = report.location[0]
|
|
37
|
+
self.suite = self._suite(report.nodeid)
|
|
38
|
+
self.tags: List[str] = []
|
|
39
|
+
self.browser = None
|
|
40
|
+
self.parameters = None
|
|
41
|
+
self.retry_attempts = []
|
|
42
|
+
self._reset_attempt()
|
|
43
|
+
|
|
44
|
+
def _reset_attempt(self) -> None:
|
|
45
|
+
self._status = TestStatus.PENDING
|
|
46
|
+
self.raw_status = None
|
|
47
|
+
self.message = None
|
|
48
|
+
self.trace = None
|
|
49
|
+
self.start = 0
|
|
50
|
+
self.stop = 0
|
|
51
|
+
self._setup_seen = False
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _suite(nodeid: str) -> List[str]:
|
|
55
|
+
# "dir/test_file.py::TestClass::test_name[param]" -> ["dir/test_file.py", "TestClass"]
|
|
56
|
+
# parameters are cut off first, they may contain "::"
|
|
57
|
+
return nodeid.split('[', 1)[0].split('::')[:-1]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def status(self) -> TestStatus:
|
|
61
|
+
return self._status
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def retries(self) -> int:
|
|
65
|
+
return len(self.retry_attempts)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def flaky(self) -> bool:
|
|
69
|
+
return self._status == TestStatus.PASSED and any(
|
|
70
|
+
attempt['status'] == TestStatus.FAILED.value for attempt in self.retry_attempts)
|
|
71
|
+
|
|
72
|
+
def _finish_attempt(self) -> None:
|
|
73
|
+
attempt = {
|
|
74
|
+
'attempt': len(self.retry_attempts) + 1,
|
|
75
|
+
'status': self._status.value,
|
|
76
|
+
'duration': max(self.stop - self.start, 0),
|
|
77
|
+
'start': self.start,
|
|
78
|
+
'stop': self.stop,
|
|
79
|
+
'message': self.message,
|
|
80
|
+
'trace': self.trace,
|
|
81
|
+
}
|
|
82
|
+
self.retry_attempts.append({key: value for key, value in attempt.items() if value is not None})
|
|
83
|
+
self._reset_attempt()
|
|
84
|
+
|
|
85
|
+
def set_status(self, report: TestReport) -> TestStatus:
|
|
86
|
+
# the first non-passed outcome wins; message and trace are taken from that same report
|
|
87
|
+
if self._status in (TestStatus.SKIPPED, TestStatus.FAILED, TestStatus.OTHER):
|
|
88
|
+
return self._status
|
|
89
|
+
elif report.skipped:
|
|
90
|
+
self._status = TestStatus.SKIPPED
|
|
91
|
+
if hasattr(report, 'wasxfail'):
|
|
92
|
+
self.raw_status = 'xfailed'
|
|
93
|
+
self.message = report.wasxfail or None
|
|
94
|
+
self.trace = report.longreprtext or None
|
|
95
|
+
else:
|
|
96
|
+
self.message = self._skip_reason(report)
|
|
97
|
+
elif report.failed or report.outcome == 'rerun':
|
|
98
|
+
# "rerun" is a failed attempt that pytest-rerunfailures will run again
|
|
99
|
+
self._status = TestStatus.FAILED
|
|
100
|
+
self.raw_status = f"{report.when}_{report.outcome}"
|
|
101
|
+
self.message = self._failure_message(report)
|
|
102
|
+
self.trace = report.longreprtext or None
|
|
103
|
+
elif report.passed:
|
|
104
|
+
self._status = TestStatus.PASSED
|
|
105
|
+
else:
|
|
106
|
+
self._status = TestStatus.OTHER
|
|
107
|
+
return self._status
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _failure_message(report: TestReport) -> Optional[str]:
|
|
111
|
+
# same text pytest shows in the short test summary, e.g. "AssertionError: assert 1 == 2"
|
|
112
|
+
crash = getattr(report.longrepr, 'reprcrash', None)
|
|
113
|
+
if crash is not None and crash.message:
|
|
114
|
+
return crash.message
|
|
115
|
+
# plain-text longrepr (some plugins, xdist worker crashes): the last line is usually the error
|
|
116
|
+
lines = [line for line in report.longreprtext.splitlines() if line.strip()]
|
|
117
|
+
return lines[-1].strip() if lines else None
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _skip_reason(report: TestReport) -> Optional[str]:
|
|
121
|
+
# for skips pytest stores longrepr as a (path, lineno, reason) tuple
|
|
122
|
+
if isinstance(report.longrepr, tuple) and len(report.longrepr) == 3:
|
|
123
|
+
reason = str(report.longrepr[2])
|
|
124
|
+
if reason.startswith('Skipped: '):
|
|
125
|
+
reason = reason[len('Skipped: '):]
|
|
126
|
+
return reason or None
|
|
127
|
+
return report.longreprtext or None
|
|
128
|
+
|
|
129
|
+
def update(self, report: TestReport) -> None:
|
|
130
|
+
if report.when == "setup":
|
|
131
|
+
# every attempt starts with setup, a second one means the test is being rerun
|
|
132
|
+
if self._setup_seen:
|
|
133
|
+
self._finish_attempt()
|
|
134
|
+
self._setup_seen = True
|
|
135
|
+
self.start = int(report.start * 1000)
|
|
136
|
+
self.set_status(report)
|
|
137
|
+
if report.when == "teardown":
|
|
138
|
+
self.stop = int(report.stop * 1000)
|
|
139
|
+
if hasattr(report, '_ctrf_metadata'):
|
|
140
|
+
self.tags = report._ctrf_metadata.get('tags')
|
|
141
|
+
self.browser = report._ctrf_metadata.get('browser')
|
|
142
|
+
self.parameters = report._ctrf_metadata.get('parameters')
|
|
143
|
+
|
|
144
|
+
def serialize(self, test_type: Optional[str] = None) -> dict:
|
|
145
|
+
result: Dict[str, Any] = {
|
|
146
|
+
'name': self.name,
|
|
147
|
+
'status': self._status.value,
|
|
148
|
+
'rawStatus': self.raw_status,
|
|
149
|
+
'duration': self.stop - self.start,
|
|
150
|
+
'start': self.start,
|
|
151
|
+
'stop': self.stop,
|
|
152
|
+
'suite': self.suite,
|
|
153
|
+
'type': test_type,
|
|
154
|
+
'retries': self.retries,
|
|
155
|
+
'retryAttempts': self.retry_attempts,
|
|
156
|
+
'flaky': True if self.flaky else None,
|
|
157
|
+
'filePath': self.file_path,
|
|
158
|
+
'tags': self.tags,
|
|
159
|
+
'browser': self.browser,
|
|
160
|
+
'parameters': self.parameters,
|
|
161
|
+
'trace': self.trace,
|
|
162
|
+
'message': self.message
|
|
163
|
+
}
|
|
164
|
+
if self.worker_id:
|
|
165
|
+
result['extra'] = {'worker': self.worker_id}
|
|
166
|
+
return {key: value for key, value in result.items() if value not in [None, '', []]}
|
|
@@ -37,6 +37,8 @@ def ctrf_json_metadata(request: FixtureRequest):
|
|
|
37
37
|
tags = list()
|
|
38
38
|
for mark in request.node.iter_markers():
|
|
39
39
|
tag = mark.name
|
|
40
|
+
if tag == 'parametrize':
|
|
41
|
+
continue
|
|
40
42
|
if mark.args:
|
|
41
43
|
for arg in mark.args:
|
|
42
44
|
tag += f'::{arg}'
|
|
@@ -46,7 +48,23 @@ def ctrf_json_metadata(request: FixtureRequest):
|
|
|
46
48
|
tags.append(tag)
|
|
47
49
|
request.node._ctrf_metadata.setdefault('tags', tags)
|
|
48
50
|
if hasattr(request.node, 'callspec'):
|
|
49
|
-
|
|
51
|
+
params = request.node.callspec.params
|
|
52
|
+
browser = params.get('browser_name')
|
|
50
53
|
if browser:
|
|
51
54
|
request.node._ctrf_metadata.setdefault('browser', browser)
|
|
55
|
+
request.node._ctrf_metadata.setdefault(
|
|
56
|
+
'parameters', {key: _serializable_param(value) for key, value in params.items()})
|
|
52
57
|
return request.node._ctrf_metadata
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
MAX_PARAM_LENGTH = 200
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _serializable_param(value):
|
|
64
|
+
# keep only primitives: report metadata is sent between xdist workers and dumped to json;
|
|
65
|
+
# long values are truncated to keep the report small (see issue 6)
|
|
66
|
+
if not isinstance(value, (str, int, float, bool, type(None))):
|
|
67
|
+
value = repr(value)
|
|
68
|
+
if isinstance(value, str) and len(value) > MAX_PARAM_LENGTH:
|
|
69
|
+
value = value[:MAX_PARAM_LENGTH] + '...'
|
|
70
|
+
return value
|
|
@@ -6,11 +6,11 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
exclude = [
|
|
7
7
|
".github", "tests", "build", "dist", ".venv",
|
|
8
8
|
"pytest_json_ctrf.egg-info", ".env", ".gitignore",
|
|
9
|
-
"CHANGELOG.md", "assets", "test-results"]
|
|
9
|
+
"CHANGELOG.md", "assets", "test-results", "schema_validator"]
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "pytest-json-ctrf"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.6.0"
|
|
14
14
|
|
|
15
15
|
dependencies = [
|
|
16
16
|
"pytest>6.0.0",
|
|
@@ -30,7 +30,7 @@ classifiers = [
|
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
|
-
"
|
|
33
|
+
"QAMania" = "https://qamania.org"
|
|
34
34
|
"Homepage" = "https://github.com/infopulse/pytest-common-test-report-json"
|
|
35
35
|
|
|
36
36
|
[tool.pytest.ini_options]
|
|
@@ -42,8 +42,9 @@ markers = [
|
|
|
42
42
|
[project.entry-points.pytest11]
|
|
43
43
|
ctrf = "ctrf.main"
|
|
44
44
|
|
|
45
|
-
[
|
|
46
|
-
dev
|
|
47
|
-
"
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"jsonschema>=4.23.0",
|
|
48
|
+
"pytest-rerunfailures>=14.0",
|
|
49
|
+
"pytest-xdist>=3.6.1",
|
|
48
50
|
]
|
|
49
|
-
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-json-ctrf
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Pytest plugin to generate json report in CTRF (Common Test Report Format)
|
|
5
5
|
Author: Oleksii Ostapov
|
|
6
|
-
Project-URL:
|
|
6
|
+
Project-URL: QAMania, https://qamania.org
|
|
7
7
|
Project-URL: Homepage, https://github.com/infopulse/pytest-common-test-report-json
|
|
8
8
|
Classifier: Framework :: Pytest
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -24,7 +24,9 @@ Do not worry if report in GitHub does not appear immediately. It takes some time
|
|
|
24
24
|
- Generates JSON report
|
|
25
25
|
- Tested to work correctly with and without [pytest-xdist](https://pypi.org/project/pytest-xdist/)
|
|
26
26
|
- Tested to get browser name from [pytest-playwright](https://pypi.org/project/pytest-playwright/)
|
|
27
|
-
-
|
|
27
|
+
- Reruns from [pytest-rerunfailures](https://pypi.org/project/pytest-rerunfailures/) are reported as `retries`, `retryAttempts` and `flaky`
|
|
28
|
+
- Each test gets a `suite` hierarchy: file, then classes
|
|
29
|
+
- Parametrized tests are supported: each case is reported as a separate test named by its full pytest node id (e.g. `test_file.py::test_param[1]`), with its values in `parameters`
|
|
28
30
|
|
|
29
31
|
## Installation
|
|
30
32
|
|
|
@@ -41,13 +43,20 @@ pytest --ctrf report.json
|
|
|
41
43
|
```
|
|
42
44
|
Environment Variables may be used to specify the required Environment Object
|
|
43
45
|
fields when using the CTRF [slack-test-reporter][ctrf-slack-test-reporter-url].
|
|
46
|
+
All of them are optional: a variable that is not set is left out of the report, see [.env.example](./.env.example).
|
|
44
47
|
|
|
45
48
|
```bash
|
|
46
49
|
CTRF_BUILD_NAME="Pytest JSON CTRF Report"
|
|
47
|
-
CTRF_BUILD_NUMBER=
|
|
48
|
-
CTRF_BUILD_URL="https://ctrf.io
|
|
50
|
+
CTRF_BUILD_NUMBER=123
|
|
51
|
+
CTRF_BUILD_URL="https://ctrf.io"
|
|
52
|
+
CTRF_TEST_ENVIRONMENT="staging"
|
|
53
|
+
CTRF_TEST_TYPE="e2e"
|
|
49
54
|
```
|
|
50
55
|
|
|
56
|
+
`CTRF_BUILD_NUMBER` must be an integer. Otherwise a warning is issued and `buildNumber` is omitted from the report.
|
|
57
|
+
`osPlatform`, `osRelease` and `osVersion` are detected automatically.
|
|
58
|
+
`CTRF_TEST_TYPE` is written to every test's `type` field.
|
|
59
|
+
|
|
51
60
|
## JSON example
|
|
52
61
|
|
|
53
62
|
More info here: https://ctrf.io/docs/schema/examples
|
|
@@ -55,11 +64,12 @@ More info here: https://ctrf.io/docs/schema/examples
|
|
|
55
64
|
```json
|
|
56
65
|
{
|
|
57
66
|
"reportFormat": "CTRF",
|
|
58
|
-
"specVersion": "
|
|
59
|
-
"generatedBy": "pytest",
|
|
67
|
+
"specVersion": "1.0.0",
|
|
68
|
+
"generatedBy": "pytest",
|
|
60
69
|
"results": {
|
|
61
70
|
"tool": {
|
|
62
|
-
"name": "
|
|
71
|
+
"name": "pytest",
|
|
72
|
+
"version": "9.0.3"
|
|
63
73
|
},
|
|
64
74
|
"summary": {
|
|
65
75
|
"tests": 3,
|
|
@@ -73,8 +83,12 @@ More info here: https://ctrf.io/docs/schema/examples
|
|
|
73
83
|
},
|
|
74
84
|
"environment": {
|
|
75
85
|
"buildName": "Pytest JSON CTRF Report",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
86
|
+
"buildUrl": "https://ctrf.io",
|
|
87
|
+
"osPlatform": "linux",
|
|
88
|
+
"osRelease": "6.8.0-45-generic",
|
|
89
|
+
"osVersion": "#45-Ubuntu SMP PREEMPT_DYNAMIC",
|
|
90
|
+
"testEnvironment": "staging",
|
|
91
|
+
"buildNumber": 123
|
|
78
92
|
},
|
|
79
93
|
"tests": [
|
|
80
94
|
{
|
|
@@ -114,6 +128,14 @@ The `pytest_runtest_logreport` hook in the controller node is used to collect th
|
|
|
114
128
|
- https://github.com/infopulse/Playwright-course-python -> The report will be used in the demo project as +1 report option
|
|
115
129
|
|
|
116
130
|
## Changelog
|
|
131
|
+
- 0.6.0 - fixed issue 12 - report now validates against the CTRF schema. Thanks to [@jamesarosen](https://github.com/jamesarosen) for the detailed report!
|
|
132
|
+
- `filePath`, `rawStatus` and integer `buildNumber`; environment fields are added only when their variables are set (no defaults), plus `osPlatform`, `osRelease`, `osVersion` and `type` (`CTRF_TEST_TYPE`)
|
|
133
|
+
- parametrized tests are named by the full node id and get `parameters`
|
|
134
|
+
- `message` is the real failure message; skip and xfail reasons are reported in `message`
|
|
135
|
+
- reruns (pytest-rerunfailures) are reported as `retries`, `retryAttempts` and `flaky`
|
|
136
|
+
- new `suite` field and summary `other`, `flaky`, `suites`, `duration`
|
|
137
|
+
- CI validates reports against the CTRF schema
|
|
138
|
+
- 0.5.3 - fixed issue 6 - @pytest.mark.parametrize will no longer be added to tags to prevent exhausting memory
|
|
117
139
|
- 0.5.1 - added mandatory root object fields: reportFormat, specVersion, generatedBy
|
|
118
140
|
- 0.5.0 - Changed logic of handling parametrized tests. Previously they were marked as retries, now they are reported as separate tests
|
|
119
141
|
- 0.4.1 - Introduced backward compatibility with python 3.8, fixed issues with the time formats
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
MANIFEST.in
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
ctrf/BaseMetadataReport.py
|
|
@@ -11,6 +12,4 @@ pytest_json_ctrf.egg-info/SOURCES.txt
|
|
|
11
12
|
pytest_json_ctrf.egg-info/dependency_links.txt
|
|
12
13
|
pytest_json_ctrf.egg-info/entry_points.txt
|
|
13
14
|
pytest_json_ctrf.egg-info/requires.txt
|
|
14
|
-
pytest_json_ctrf.egg-info/top_level.txt
|
|
15
|
-
tests/test_example.py
|
|
16
|
-
tests/test_plugin.py
|
|
15
|
+
pytest_json_ctrf.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ctrf
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import Any, Optional, List, Dict
|
|
3
|
-
|
|
4
|
-
from pytest import TestReport
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestStatus(Enum):
|
|
8
|
-
PASSED = "passed"
|
|
9
|
-
FAILED = "failed"
|
|
10
|
-
SKIPPED = "skipped"
|
|
11
|
-
PENDING = "pending"
|
|
12
|
-
OTHER = "other"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestObject:
|
|
16
|
-
name: str
|
|
17
|
-
_status: TestStatus
|
|
18
|
-
raw_status: Optional[str]
|
|
19
|
-
start: int
|
|
20
|
-
stop: int
|
|
21
|
-
retries: int
|
|
22
|
-
message: Optional[str]
|
|
23
|
-
|
|
24
|
-
worker_id: Optional[str]
|
|
25
|
-
file_path: str
|
|
26
|
-
tags: List[str]
|
|
27
|
-
browser: Optional[str]
|
|
28
|
-
trace: Optional[str]
|
|
29
|
-
|
|
30
|
-
def __init__(self, report: TestReport, worker_id: Optional[str] = None):
|
|
31
|
-
self.name = report.nodeid.split('[')[0]
|
|
32
|
-
self._status = TestStatus.PENDING
|
|
33
|
-
self._status = self.set_status(report)
|
|
34
|
-
self.raw_status = None
|
|
35
|
-
self.start = 0
|
|
36
|
-
self.stop = 0
|
|
37
|
-
self.retries = 0
|
|
38
|
-
self.message = None
|
|
39
|
-
|
|
40
|
-
self.worker_id = worker_id
|
|
41
|
-
self.file_path = report.location[0]
|
|
42
|
-
self.tags: List[str] = []
|
|
43
|
-
self.browser = None
|
|
44
|
-
self.trace = None
|
|
45
|
-
|
|
46
|
-
@property
|
|
47
|
-
def status(self) -> TestStatus:
|
|
48
|
-
return self._status
|
|
49
|
-
|
|
50
|
-
@status.setter
|
|
51
|
-
def status(self, value: TestStatus) -> None:
|
|
52
|
-
if self._status in (TestStatus.PENDING, TestStatus.PASSED):
|
|
53
|
-
self._status = value
|
|
54
|
-
return
|
|
55
|
-
elif self._status in (TestStatus.SKIPPED, TestStatus.FAILED):
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
def set_status(self, report: TestReport) -> TestStatus:
|
|
59
|
-
if self._status in (TestStatus.SKIPPED, TestStatus.FAILED):
|
|
60
|
-
return self._status
|
|
61
|
-
elif report.skipped:
|
|
62
|
-
self._status = TestStatus.SKIPPED
|
|
63
|
-
elif report.failed:
|
|
64
|
-
self._status = TestStatus.FAILED
|
|
65
|
-
self.raw_status = f"{report.when}_{report.outcome}"
|
|
66
|
-
self.message = f"The test failed in the {report.when} phase"
|
|
67
|
-
if hasattr(report, 'longreprtext'):
|
|
68
|
-
if "AssertionError" in report.longreprtext:
|
|
69
|
-
self.message += " due to an assertion error"
|
|
70
|
-
elif "Exception" in report.longreprtext:
|
|
71
|
-
self.message += " due to an exception"
|
|
72
|
-
elif report.passed:
|
|
73
|
-
self._status = TestStatus.PASSED
|
|
74
|
-
else:
|
|
75
|
-
self._status = TestStatus.OTHER
|
|
76
|
-
return self._status
|
|
77
|
-
|
|
78
|
-
def update(self, report: TestReport) -> None:
|
|
79
|
-
self.set_status(report)
|
|
80
|
-
if report.when == "setup" and self.start == 0:
|
|
81
|
-
self.start = int(report.start * 1000)
|
|
82
|
-
if report.when == "teardown" and self.stop == 0:
|
|
83
|
-
self.stop = int(report.stop * 1000)
|
|
84
|
-
if report.longrepr and len(report.longreprtext) > 0:
|
|
85
|
-
self.trace = report.longreprtext
|
|
86
|
-
if hasattr(report, '_ctrf_metadata'):
|
|
87
|
-
self.tags = report._ctrf_metadata.get('tags')
|
|
88
|
-
self.browser = report._ctrf_metadata.get('browser')
|
|
89
|
-
|
|
90
|
-
def serialize(self) -> dict:
|
|
91
|
-
result: Dict[str, Any] = {
|
|
92
|
-
'name': self.name,
|
|
93
|
-
'status': self._status.value,
|
|
94
|
-
'raw_status': self.raw_status,
|
|
95
|
-
'duration': self.stop - self.start,
|
|
96
|
-
'start': self.start,
|
|
97
|
-
'stop': self.stop,
|
|
98
|
-
'retries': None if self.retries == 1 else self.retries,
|
|
99
|
-
'file_path': self.file_path,
|
|
100
|
-
'tags': self.tags,
|
|
101
|
-
'browser': self.browser,
|
|
102
|
-
'trace': self.trace,
|
|
103
|
-
'message': self.message
|
|
104
|
-
}
|
|
105
|
-
if self.worker_id:
|
|
106
|
-
result['extra'] = {'worker': self.worker_id}
|
|
107
|
-
return {key: value for key, value in result.items() if value not in [None, '', []]}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
from pytest import mark, fixture
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
@fixture
|
|
5
|
-
def pre_fail():
|
|
6
|
-
raise Exception("This is a test exception")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@fixture
|
|
10
|
-
def post_fail():
|
|
11
|
-
yield
|
|
12
|
-
raise Exception("This is a test exception")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_do_calculations_1_pass():
|
|
16
|
-
assert 2 + 2 == 4
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@mark.regression
|
|
20
|
-
def test_do_calculations_2_pass():
|
|
21
|
-
assert 3 + 3 == 6
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@mark.skip
|
|
25
|
-
def test_do_calculations_3_skip():
|
|
26
|
-
assert 4 + 4 == 8
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@mark.smoke
|
|
30
|
-
def test_do_calculations_4_smoke():
|
|
31
|
-
assert 5 + 5 == 10
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_do_calculations_5_fail():
|
|
35
|
-
assert 6 + 6 == 13
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_do_calculations_6_error():
|
|
39
|
-
raise Exception("This is a test exception")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_do_calculations_7_pre_error(pre_fail):
|
|
43
|
-
assert 7 + 7 == 14
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_do_calculations_8_post_error(post_fail):
|
|
47
|
-
assert 8 + 8 == 16
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@mark.parametrize('param', [1, 2, 3, 4, 5])
|
|
51
|
-
def test_parametrized(param):
|
|
52
|
-
assert param < 10
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@mark.parametrize('param', [6, 7, 8, 9, 10])
|
|
56
|
-
def test_parametrized_fail(param):
|
|
57
|
-
assert param != 8
|
|
58
|
-
|
|
59
|
-
@mark.test_id(1234, hello='world')
|
|
60
|
-
def test_with_test_id():
|
|
61
|
-
assert 1 == 1
|
|
62
|
-
|
|
63
|
-
@mark.test_id(1235)
|
|
64
|
-
def test_with_test_id_2():
|
|
65
|
-
assert 1 == 1
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pytest import Pytester, fixture
|
|
3
|
-
|
|
4
|
-
pytest_plugins = ["pytester", "xdist", "ctrf"]
|
|
5
|
-
test_file = "tests/test_example.py"
|
|
6
|
-
basic_test_args = ["-k", "test_example"]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@fixture
|
|
10
|
-
def ctrf_report_sync(pytester: Pytester):
|
|
11
|
-
pytester.copy_example(test_file)
|
|
12
|
-
pytester.runpytest(*[*basic_test_args, "--ctrf", "report.json"])
|
|
13
|
-
with open(pytester.path / "report.json") as file:
|
|
14
|
-
yield json.load(file)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@fixture
|
|
18
|
-
def ctrf_report_xdist(pytester: Pytester):
|
|
19
|
-
pytester.copy_example(test_file)
|
|
20
|
-
pytester.runpytest(*[*basic_test_args, "--ctrf", "report.json", "-n", "3"])
|
|
21
|
-
with open(pytester.path / "report.json") as file:
|
|
22
|
-
yield json.load(file)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_without_plugin_no_xdist(pytester: Pytester):
|
|
26
|
-
pytester.copy_example(test_file)
|
|
27
|
-
result = pytester.runpytest(*basic_test_args)
|
|
28
|
-
result.assert_outcomes(passed=15, skipped=1, failed=3, errors=2)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_without_plugin_with_xdist(pytester: Pytester):
|
|
32
|
-
pytester.copy_example(test_file)
|
|
33
|
-
result = pytester.runpytest(*[*basic_test_args, "-n", "3"])
|
|
34
|
-
result.assert_outcomes(passed=15, skipped=1, failed=3, errors=2)
|
|
35
|
-
assert "created: 3/3 workers" in result.stdout.str()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_with_markers(pytester: Pytester):
|
|
39
|
-
pytester.copy_example(test_file)
|
|
40
|
-
args = [*basic_test_args, "--ctrf", "report.json", "-m smoke"]
|
|
41
|
-
pytester.runpytest(*args)
|
|
42
|
-
with open(pytester.path / "report.json") as file:
|
|
43
|
-
report = json.load(file)
|
|
44
|
-
assert report["results"]["summary"]["tests"] == 1
|
|
45
|
-
assert report["results"]["summary"]["passed"] == 1
|
|
46
|
-
assert report["results"]["summary"]["failed"] == 0
|
|
47
|
-
assert report["results"]["summary"]["skipped"] == 0
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_with_plugin_no_xdist(ctrf_report_sync):
|
|
51
|
-
assert ctrf_report_sync["results"]["summary"]["tests"] == 20
|
|
52
|
-
assert ctrf_report_sync["results"]["summary"]["passed"] == 14
|
|
53
|
-
assert ctrf_report_sync["results"]["summary"]["failed"] == 5
|
|
54
|
-
assert ctrf_report_sync["results"]["summary"]["skipped"] == 1
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_with_plugin_failed_details(ctrf_report_sync):
|
|
58
|
-
for test in ctrf_report_sync["results"]["tests"]:
|
|
59
|
-
if test["status"] == "failed" and test.get("raw_status") == "call_failed":
|
|
60
|
-
assert test.get('trace') is not None
|
|
61
|
-
assert test.get("message") is not None
|
|
62
|
-
assert test.get("raw_status") is not None
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_any_test_has_timestamps(ctrf_report_sync):
|
|
66
|
-
for test in ctrf_report_sync["results"]["tests"]:
|
|
67
|
-
assert test.get("start") is not None
|
|
68
|
-
assert test.get("stop") is not None
|
|
69
|
-
assert test.get("duration") is not None
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def test_with_plugin_with_xdist(ctrf_report_xdist):
|
|
73
|
-
assert ctrf_report_xdist["results"]["summary"]["tests"] == 20
|
|
74
|
-
assert ctrf_report_xdist["results"]["summary"]["passed"] == 14
|
|
75
|
-
assert ctrf_report_xdist["results"]["summary"]["failed"] == 5
|
|
76
|
-
assert ctrf_report_xdist["results"]["summary"]["skipped"] == 1
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_with_plugin_failed_details_xdist(ctrf_report_xdist):
|
|
80
|
-
for test in ctrf_report_xdist["results"]["tests"]:
|
|
81
|
-
if test["status"] == "failed" and test.get("raw_status") == "call_failed":
|
|
82
|
-
assert test.get('trace') is not None
|
|
83
|
-
assert test.get("message") is not None
|
|
84
|
-
assert test.get("raw_status") is not None
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def test_any_test_has_timestamps_xdist(ctrf_report_xdist):
|
|
88
|
-
for test in ctrf_report_xdist["results"]["tests"]:
|
|
89
|
-
assert test.get("start") is not None
|
|
90
|
-
assert test.get("stop") is not None
|
|
91
|
-
assert test.get("duration") is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/pytest_json_ctrf.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytest_json_ctrf-0.5.2 → pytest_json_ctrf-0.6.0}/pytest_json_ctrf.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|