pytest-json-ctrf 0.1.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.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytest-json-ctrf
3
+ Version: 0.1.0
4
+ Summary: Pytest plugin to generate json report in CTRF (Common Test Report Format)
5
+ Author: Oleksii Ostapov
6
+ Project-URL: Infopulse, https://infopulse.com/
7
+ Project-URL: Homepage, https://infopulse.com/pytest-json-ctrf
8
+ Classifier: Framework :: Pytest
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: pytest>6.0.0
15
+
16
+ # CTRF for pytest
17
+
18
+ Pytest implementation of Common Test Report Format (CTRF) for test results.
19
+ Test report will be generated in JSON format.
20
+ Test report can be used to prettify the report in GitHub Actions with [github-actions-ctrf](https://github.com/ctrf-io/github-actions-ctrf).
21
+ Do not worry if report in GitHub does not appear immediately. It takes some time to process the report.
22
+
23
+ ## Features
24
+ - Generates JSON report
25
+ - Tested to work correctly with and without [pytest-xdist](https://pypi.org/project/pytest-xdist/)
26
+ - Tested to get browser name from [pytest-playwright](https://pypi.org/project/pytest-playwright/)
27
+ - Parametrized tests are supported and reported as 1 test with multiple retries
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install pytest-json-ctrf
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ generate report.json file in the root directory of the project. File path is mandatory
38
+
39
+ ```bash
40
+ pytest --ctrf report.json
41
+ ```
42
+
43
+ ## Json exampe
44
+
45
+ More info here: https://ctrf.io/docs/schema/examples
46
+
47
+ ```json
48
+ {
49
+ "results": {
50
+ "tool": {
51
+ "name": "jest"
52
+ },
53
+ "summary": {
54
+ "tests": 3,
55
+ "passed": 1,
56
+ "failed": 1,
57
+ "pending": 0,
58
+ "skipped": 1,
59
+ "other": 0,
60
+ "start": 1706644023,
61
+ "stop": 1706644043
62
+ },
63
+ "tests": [
64
+ {
65
+ "name": "User should be able to login",
66
+ "status": "passed",
67
+ "duration": 1200
68
+ },
69
+ {
70
+ "name": "User profile information should be correct",
71
+ "status": "failed",
72
+ "duration": 800
73
+ },
74
+ {
75
+ "name": "User should be able to logout",
76
+ "status": "skipped",
77
+ "duration": 0
78
+ }
79
+ ]
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Report Example
85
+ ![Example Image](./assets/report_example.png)
86
+
87
+ ## Credits
88
+
89
+ - https://ctrf.io/ -> nice data format
90
+ - https://github.com/numirias/pytest-json-report -> Source of inspiration and dealing with xdist sync
91
+ - https://github.com/testomatio/pytestomatio -> Source of inspiration for creating pytest plugins
92
+ - https://github.com/infopulse/Playwright-course-python -> The report will be used in the demo project as +1 report option
93
+
@@ -0,0 +1,78 @@
1
+ # CTRF for pytest
2
+
3
+ Pytest implementation of Common Test Report Format (CTRF) for test results.
4
+ Test report will be generated in JSON format.
5
+ Test report can be used to prettify the report in GitHub Actions with [github-actions-ctrf](https://github.com/ctrf-io/github-actions-ctrf).
6
+ Do not worry if report in GitHub does not appear immediately. It takes some time to process the report.
7
+
8
+ ## Features
9
+ - Generates JSON report
10
+ - Tested to work correctly with and without [pytest-xdist](https://pypi.org/project/pytest-xdist/)
11
+ - Tested to get browser name from [pytest-playwright](https://pypi.org/project/pytest-playwright/)
12
+ - Parametrized tests are supported and reported as 1 test with multiple retries
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install pytest-json-ctrf
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ generate report.json file in the root directory of the project. File path is mandatory
23
+
24
+ ```bash
25
+ pytest --ctrf report.json
26
+ ```
27
+
28
+ ## Json exampe
29
+
30
+ More info here: https://ctrf.io/docs/schema/examples
31
+
32
+ ```json
33
+ {
34
+ "results": {
35
+ "tool": {
36
+ "name": "jest"
37
+ },
38
+ "summary": {
39
+ "tests": 3,
40
+ "passed": 1,
41
+ "failed": 1,
42
+ "pending": 0,
43
+ "skipped": 1,
44
+ "other": 0,
45
+ "start": 1706644023,
46
+ "stop": 1706644043
47
+ },
48
+ "tests": [
49
+ {
50
+ "name": "User should be able to login",
51
+ "status": "passed",
52
+ "duration": 1200
53
+ },
54
+ {
55
+ "name": "User profile information should be correct",
56
+ "status": "failed",
57
+ "duration": 800
58
+ },
59
+ {
60
+ "name": "User should be able to logout",
61
+ "status": "skipped",
62
+ "duration": 0
63
+ }
64
+ ]
65
+ }
66
+ }
67
+ ```
68
+
69
+ ## Report Example
70
+ ![Example Image](./assets/report_example.png)
71
+
72
+ ## Credits
73
+
74
+ - https://ctrf.io/ -> nice data format
75
+ - https://github.com/numirias/pytest-json-report -> Source of inspiration and dealing with xdist sync
76
+ - https://github.com/testomatio/pytestomatio -> Source of inspiration for creating pytest plugins
77
+ - https://github.com/infopulse/Playwright-course-python -> The report will be used in the demo project as +1 report option
78
+
@@ -0,0 +1,34 @@
1
+ from pytest import Config, hookimpl, Item, CallInfo, CollectReport, Function, TestReport, fixture, FixtureRequest
2
+ from .Report import Report
3
+ from collections import OrderedDict
4
+
5
+ metadata = '_ctrf_metadata'
6
+
7
+
8
+ class BaseMetadataReport:
9
+ '''This class needed only to get metadata
10
+ from test nodes from different workers and
11
+ transfer it the report objects'''
12
+
13
+ def __init__(self):
14
+ self._report = Report()
15
+
16
+ def pytest_configure(self, config: Config):
17
+ if not hasattr(config, '_ctrf'):
18
+ setattr(config, '_ctrf', self)
19
+
20
+ if config.option.ctrf and not self._report:
21
+ self._report = Report()
22
+
23
+ @hookimpl(hookwrapper=True)
24
+ def pytest_runtest_protocol(self, item: Item, nextitem: Item):
25
+ setattr(item, metadata, {})
26
+ yield
27
+ delattr(item, metadata)
28
+
29
+ @hookimpl(hookwrapper=True)
30
+ def pytest_runtest_makereport(self, item, call):
31
+ report = (yield).get_result()
32
+ meta = getattr(item, metadata)
33
+ if meta:
34
+ setattr(report, metadata, meta)
@@ -0,0 +1,26 @@
1
+ from pytest import Config, hookimpl, Item, CallInfo, CollectReport, Function, TestReport, fixture, FixtureRequest
2
+ from .Report import Report
3
+ from collections import OrderedDict
4
+ from .BaseMetadataReport import metadata, BaseMetadataReport
5
+
6
+
7
+ class CTRF(BaseMetadataReport):
8
+ def __init__(self):
9
+ super().__init__()
10
+ self._json_tests = OrderedDict()
11
+
12
+ @hookimpl(tryfirst=True)
13
+ def pytest_sessionstart(self, session):
14
+ if self._report:
15
+ self._report.start()
16
+
17
+ @hookimpl(tryfirst=True)
18
+ def pytest_sessionfinish(self, session):
19
+ if self._report:
20
+ self._report.stop()
21
+ self._report.save(session.config.option.ctrf)
22
+
23
+ def pytest_runtest_logreport(self, report: TestReport):
24
+ if not hasattr(report, metadata):
25
+ setattr(report, metadata, {})
26
+ self._report.collect(report)
@@ -0,0 +1,78 @@
1
+ import os
2
+ import pytest
3
+ import time
4
+ import json
5
+ from pytest import Item, CallInfo, TestReport, Mark
6
+ from collections import OrderedDict
7
+ from .TestObject import TestObject, TestStatus
8
+
9
+
10
+ class Report:
11
+ def __init__(self):
12
+ self.test_items = OrderedDict()
13
+ self.prepared_tests = dict()
14
+ self.start_time = None
15
+ self.stop_time = None
16
+
17
+ def start(self) -> None:
18
+ self.start_time = time.time()
19
+
20
+ def stop(self) -> None:
21
+ self.stop_time = time.time()
22
+
23
+ @staticmethod
24
+ def _get_tool() -> dict:
25
+ return {
26
+ "name": "pytest",
27
+ "version": str(pytest.__version__)
28
+ }
29
+
30
+ def _get_summary(self) -> dict:
31
+ return {
32
+ 'tests': len(self.prepared_tests),
33
+ 'passed': len([test for test in self.prepared_tests.values() if test.status == TestStatus.PASSED]),
34
+ 'failed': len([test for test in self.prepared_tests.values() if test.status == TestStatus.FAILED]),
35
+ 'skipped': len([test for test in self.prepared_tests.values() if test.status == TestStatus.SKIPPED]),
36
+ 'pending': len([test for test in self.prepared_tests.values() if test.status == TestStatus.PENDING]),
37
+ 'other': 0,
38
+ 'start': self.start_time,
39
+ 'stop': self.stop_time
40
+ }
41
+
42
+ def collect(self, report: TestReport) -> None:
43
+ if report.nodeid not in self.test_items.keys():
44
+ test = TestObject()
45
+ else:
46
+ test = self.test_items.get(report.nodeid)
47
+ test.update(report, getattr(report, 'worker_id', None))
48
+ self.test_items[report.nodeid] = test
49
+
50
+ def process_retries(self) -> None:
51
+ for test_id, test_data in self.test_items.items():
52
+ name = test_id.split('[')[0]
53
+ test = self.prepared_tests.get(name)
54
+ if test:
55
+ test.retries += 1
56
+ else:
57
+ self.prepared_tests[name] = test_data
58
+
59
+ def get_report(self) -> dict:
60
+ self.process_retries()
61
+ return {'results':
62
+ {
63
+ "tool": self._get_tool(),
64
+ "summary": self._get_summary(),
65
+ "tests": [test.serialize() for test in self.prepared_tests.values()]
66
+ }
67
+ }
68
+
69
+ def save(self, report_file: str) -> None:
70
+ dirname = os.path.dirname(report_file)
71
+ if dirname:
72
+ try:
73
+ os.makedirs(dirname)
74
+ except Exception as e:
75
+ print(e)
76
+ raise Exception(f"Can't create directory {dirname}")
77
+ with open(report_file, 'w') as file:
78
+ json.dump(self.get_report(), file, default=str, indent=4)
@@ -0,0 +1,73 @@
1
+ from enum import Enum
2
+ from pytest import TestReport
3
+
4
+
5
+ class TestStatus(Enum):
6
+ PASSED = "passed"
7
+ FAILED = "failed"
8
+ SKIPPED = "skipped"
9
+ PENDING = "pending"
10
+ OTHER = "other"
11
+
12
+
13
+ class TestObject:
14
+ name: str = None
15
+ status: TestStatus = TestStatus.PENDING
16
+ raw_status: str = None
17
+ duration: float = 0
18
+ start: float = None
19
+ stop: float = None
20
+ retries: int = 0
21
+
22
+ worker_id: str = None
23
+ file_path: str = None
24
+ tags: list[str] = None
25
+ browser: str = None
26
+ trace: str = None
27
+
28
+ def set_status(self, report: TestReport) -> None:
29
+ if self.status in (TestStatus.SKIPPED, TestStatus.FAILED):
30
+ return
31
+ elif report.skipped:
32
+ self.status = TestStatus.SKIPPED
33
+ elif report.failed:
34
+ self.status = TestStatus.FAILED
35
+ self.raw_status = f"{report.when}_{report.outcome}"
36
+ elif report.passed:
37
+ self.status = TestStatus.PASSED
38
+ else:
39
+ self.status = TestStatus.OTHER
40
+
41
+ def update(self, report: TestReport, worker_id: str = None) -> None:
42
+ self.name = report.head_line.split('[')[0]
43
+ self.set_status(report)
44
+ self.duration += report.duration
45
+ if report.when == "setup" and not self.start:
46
+ self.start = report.start
47
+ if report.when == "teardown" and not self.stop:
48
+ self.stop = report.stop
49
+ self.file_path = report.location[0]
50
+ if report.longrepr and len(report.longreprtext) > 0:
51
+ self.trace = report.longreprtext
52
+ if hasattr(report, '_ctrf_metadata'):
53
+ self.tags = report._ctrf_metadata.get('tags')
54
+ self.browser = report._ctrf_metadata.get('browser')
55
+ self.worker_id = worker_id
56
+
57
+ def serialize(self) -> dict:
58
+ result = {
59
+ 'name': self.name,
60
+ 'status': self.status.value,
61
+ 'raw_status': self.raw_status,
62
+ 'duration': self.duration,
63
+ 'start': self.start,
64
+ 'stop': self.stop,
65
+ 'retries': None if self.retries == 1 else self.retries,
66
+ 'file_path': self.file_path,
67
+ 'tags': self.tags,
68
+ 'browser': self.browser,
69
+ 'trace': self.trace
70
+ }
71
+ if self.worker_id:
72
+ result['extra'] = {'worker': self.worker_id}
73
+ return {key: value for key, value in result.items() if value not in [None, '', []]}
File without changes
@@ -0,0 +1,41 @@
1
+ from pytest import Config, Parser, FixtureRequest, fixture, Function
2
+ from plugin.CommonTestReportPlugin import CTRF
3
+ from plugin.BaseMetadataReport import BaseMetadataReport
4
+
5
+
6
+ def pytest_addoption(parser: Parser):
7
+ parser.addoption('--ctrf',
8
+ action='store',
9
+ help='generate test report. Report file name is optional')
10
+
11
+
12
+ def pytest_configure(config: Config):
13
+ if not config.option.ctrf:
14
+ return
15
+ if hasattr(config, 'workerinput'):
16
+ plugin = BaseMetadataReport()
17
+ else:
18
+ plugin = CTRF()
19
+ setattr(config, '_ctrf', plugin)
20
+ config.pluginmanager.register(plugin, name='ctrf')
21
+
22
+
23
+ def pytest_unconfigure(config: Config):
24
+ ctrf = getattr(config, '_ctrf', None)
25
+ if ctrf is not None:
26
+ delattr(config, '_ctrf')
27
+ if config.pluginmanager.hasplugin('ctrf'):
28
+ config.pluginmanager.unregister(name='ctrf')
29
+
30
+
31
+ @fixture(autouse=True)
32
+ def ctrf_json_metadata(request: FixtureRequest):
33
+ if not request.config.option.ctrf:
34
+ return
35
+ tags = [mark.name for mark in request.node.iter_markers()]
36
+ request.node._ctrf_metadata.setdefault('tags', tags)
37
+ if hasattr(request.node, 'callspec'):
38
+ browser = request.node.callspec.params.get('browser_name')
39
+ if browser:
40
+ request.node._ctrf_metadata.setdefault('browser', browser)
41
+ return request.node._ctrf_metadata
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=65.5.1", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools.packages.find]
6
+ exclude = [".github", "tests", "build", "dist", ".venv", "pytestomatio.egg-info", ".env", ".gitignore", "CHANGELOG.md", "assets"]
7
+
8
+ [project]
9
+ name = "pytest-json-ctrf"
10
+ version = "0.1.0"
11
+
12
+ dependencies = [
13
+ "pytest>6.0.0",
14
+ ]
15
+
16
+ authors = [
17
+ { name = "Oleksii Ostapov" },
18
+ ]
19
+ description = "Pytest plugin to generate json report in CTRF (Common Test Report Format)"
20
+ readme = "README.md"
21
+ requires-python = ">=3.10"
22
+ classifiers = [
23
+ "Framework :: Pytest",
24
+ "Programming Language :: Python :: 3",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ ]
28
+
29
+ [project.urls]
30
+ "Infopulse" = "https://infopulse.com/"
31
+ "Homepage" = "https://infopulse.com/pytest-json-ctrf"
32
+
33
+ [tool.pytest.ini_options]
34
+ markers = [
35
+ "smoke: marker for smoke tests",
36
+ "regression: marker for regression tests",
37
+ ]
38
+
39
+ [project.entry-points.pytest11]
40
+ pytestomatio = "plugin.main"
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytest-json-ctrf
3
+ Version: 0.1.0
4
+ Summary: Pytest plugin to generate json report in CTRF (Common Test Report Format)
5
+ Author: Oleksii Ostapov
6
+ Project-URL: Infopulse, https://infopulse.com/
7
+ Project-URL: Homepage, https://infopulse.com/pytest-json-ctrf
8
+ Classifier: Framework :: Pytest
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: pytest>6.0.0
15
+
16
+ # CTRF for pytest
17
+
18
+ Pytest implementation of Common Test Report Format (CTRF) for test results.
19
+ Test report will be generated in JSON format.
20
+ Test report can be used to prettify the report in GitHub Actions with [github-actions-ctrf](https://github.com/ctrf-io/github-actions-ctrf).
21
+ Do not worry if report in GitHub does not appear immediately. It takes some time to process the report.
22
+
23
+ ## Features
24
+ - Generates JSON report
25
+ - Tested to work correctly with and without [pytest-xdist](https://pypi.org/project/pytest-xdist/)
26
+ - Tested to get browser name from [pytest-playwright](https://pypi.org/project/pytest-playwright/)
27
+ - Parametrized tests are supported and reported as 1 test with multiple retries
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install pytest-json-ctrf
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ generate report.json file in the root directory of the project. File path is mandatory
38
+
39
+ ```bash
40
+ pytest --ctrf report.json
41
+ ```
42
+
43
+ ## Json exampe
44
+
45
+ More info here: https://ctrf.io/docs/schema/examples
46
+
47
+ ```json
48
+ {
49
+ "results": {
50
+ "tool": {
51
+ "name": "jest"
52
+ },
53
+ "summary": {
54
+ "tests": 3,
55
+ "passed": 1,
56
+ "failed": 1,
57
+ "pending": 0,
58
+ "skipped": 1,
59
+ "other": 0,
60
+ "start": 1706644023,
61
+ "stop": 1706644043
62
+ },
63
+ "tests": [
64
+ {
65
+ "name": "User should be able to login",
66
+ "status": "passed",
67
+ "duration": 1200
68
+ },
69
+ {
70
+ "name": "User profile information should be correct",
71
+ "status": "failed",
72
+ "duration": 800
73
+ },
74
+ {
75
+ "name": "User should be able to logout",
76
+ "status": "skipped",
77
+ "duration": 0
78
+ }
79
+ ]
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Report Example
85
+ ![Example Image](./assets/report_example.png)
86
+
87
+ ## Credits
88
+
89
+ - https://ctrf.io/ -> nice data format
90
+ - https://github.com/numirias/pytest-json-report -> Source of inspiration and dealing with xdist sync
91
+ - https://github.com/testomatio/pytestomatio -> Source of inspiration for creating pytest plugins
92
+ - https://github.com/infopulse/Playwright-course-python -> The report will be used in the demo project as +1 report option
93
+
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ plugin/BaseMetadataReport.py
4
+ plugin/CommonTestReportPlugin.py
5
+ plugin/Report.py
6
+ plugin/TestObject.py
7
+ plugin/__init__.py
8
+ plugin/main.py
9
+ pytest_json_ctrf.egg-info/PKG-INFO
10
+ pytest_json_ctrf.egg-info/SOURCES.txt
11
+ pytest_json_ctrf.egg-info/dependency_links.txt
12
+ pytest_json_ctrf.egg-info/entry_points.txt
13
+ pytest_json_ctrf.egg-info/requires.txt
14
+ pytest_json_ctrf.egg-info/top_level.txt
15
+ tests/test_example.py
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ pytestomatio = plugin.main
@@ -0,0 +1 @@
1
+ pytest>6.0.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,52 @@
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