├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── pytest_excel ├── __init__.py └── pytest_excel.py ├── setup.cfg ├── setup.py ├── test_excel_report.py ├── tox.ini └── version.txt /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ------------- 3 | 4 | 5 | **0.1.1 (2016-08-04)** 6 | 7 | * Initial release 8 | 9 | **1.0.0 (2016-08-18)** 10 | 11 | * Added support for Xdist plugin feature. 12 | 13 | 14 | **1.0.1 (2016-08-18)** 15 | 16 | * Added fix for excel file path 17 | 18 | 19 | **1.1.0 (2016-08-18)** 20 | 21 | * Added support to capture xfail and xpass reasons 22 | 23 | **1.2.0 (2016-09-14)** 24 | 25 | * Added support to include markers in the excel report 26 | 27 | **1.2.1 (2017-01-31)** 28 | 29 | * Fix for the issue obserevd on python3.6 30 | * Fix for unicode related issues in python3.6 31 | 32 | 33 | **1.2.2 (2017-02-01)** 34 | 35 | * Fixes issue with report update method in python3.6 36 | 37 | 38 | **1.2.3 (2018-06-14)** 39 | 40 | * Fixes issue with marker information 41 | 42 | **1.4.0 (2020-06-14)** 43 | 44 | * Fixes issue with installation 45 | 46 | **1.4.1 (2020-06-14)** 47 | 48 | * Fixes issue with terminal reporting when no cases gets executed 49 | 50 | **1.4.2 (2020-10-06)** 51 | 52 | * Fixes license details 53 | 54 | **1.5.2 (2023-17-20)** 55 | 56 | * Fixes typo error 57 | 58 | **1.6.0 (2023-09-14)** 59 | 60 | * Replaced openpyexcel with Pandas 61 | 62 | **1.7.0 (2024-06-18)** 63 | 64 | * Remove depreciated warnings 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 santosh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include pytest_excel/test_excel_report.py 3 | include tox.ini -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pytest-excel 2 | ================ 3 | 4 | 5 | pytest-excel is a plugin for `py.test `_ that allows to 6 | to create excel report for test results. 7 | 8 | 9 | Requirements 10 | ------------ 11 | 12 | You will need the following prerequisites in order to use pytest-excel: 13 | 14 | - Python 3.9, 3.10, 3.11 and 3.12 15 | - pytest 16 | - pandas 17 | 18 | 19 | Installation 20 | ------------ 21 | 22 | To install pytest-excel:: 23 | 24 | $�pip install pytest-excel 25 | 26 | Then run your tests with:: 27 | 28 | $ py.test --excelreport=report.xls 29 | 30 | If you would like more detailed output (one test per line), then you may use the verbose option:: 31 | 32 | $ py.test --verbose 33 | 34 | If you would like to run tests without execution to collect test doc string:: 35 | 36 | $ py.test --excelreport=report.xls --collect-only 37 | 38 | 39 | If you would like to get timestamp in the as filename:: 40 | 41 | $ py.test --excelreport=report%Y-%M-dT%H%.xls 42 | -------------------------------------------------------------------------------- /pytest_excel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssrikanta/pytest-excel/6e778563be00eccb3311547639cf34946704c577/pytest_excel/__init__.py -------------------------------------------------------------------------------- /pytest_excel/pytest_excel.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pandas as pd 3 | from datetime import datetime 4 | from collections import OrderedDict 5 | import pytest 6 | from _pytest.mark.structures import Mark 7 | 8 | 9 | _py_ext_re = re.compile(r"\.py$") 10 | 11 | 12 | def pytest_addoption(parser): 13 | group = parser.getgroup("terminal reporting") 14 | group.addoption('--excelreport', '--excel-report', 15 | action="store", 16 | dest="excelpath", 17 | metavar="path", 18 | default=None, 19 | help="create excel report file at given path.") 20 | 21 | 22 | def pytest_configure(config): 23 | excelpath = config.option.excelpath 24 | if excelpath: 25 | config._excel = ExcelReporter(excelpath) 26 | config.pluginmanager.register(config._excel) 27 | 28 | 29 | def pytest_unconfigure(config): 30 | excel = getattr(config, '_excel', None) 31 | if excel: 32 | del config._excel 33 | config.pluginmanager.unregister(excel) 34 | 35 | 36 | def mangle_test_address(address): 37 | path, possible_open_bracket, params = address.partition('[') 38 | names = path.split("::") 39 | try: 40 | names.remove('()') 41 | except ValueError: 42 | pass 43 | 44 | names[0] = names[0].replace("/", '.') 45 | names[0] = _py_ext_re.sub("", names[0]) 46 | names[-1] += possible_open_bracket + params 47 | return names 48 | 49 | 50 | class ExcelReporter(object): 51 | 52 | 53 | def __init__(self, excelpath): 54 | self.results = [] 55 | self.excelpath = datetime.now().strftime(excelpath) 56 | 57 | 58 | def append(self, result): 59 | self.results.append(result) 60 | 61 | 62 | def create_sheet(self, column_heading): 63 | self.wbook = pd.DataFrame(columns = column_heading) 64 | 65 | 66 | def update_worksheet(self): 67 | self.wbook = pd.concat([self.wbook, pd.DataFrame(self.results)], ignore_index = False) 68 | 69 | 70 | def save_excel(self): 71 | self.wbook.to_excel(self.excelpath, index = False) 72 | 73 | 74 | def build_result(self, report, status, message): 75 | 76 | result = OrderedDict() 77 | names = mangle_test_address(report.nodeid) 78 | 79 | result['suite_name'] = names[-2] 80 | result['test_name'] = names[-1] 81 | if report.test_doc is None: 82 | result['description'] = report.test_doc 83 | else: 84 | result['description'] = report.test_doc.strip() 85 | 86 | result['result'] = status 87 | result['duration'] = getattr(report, 'duration', 0.0) 88 | result['timestamp'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') 89 | result['message'] = message 90 | result['file_name'] = report.location[0] 91 | result['full_test_name'] = report.nodeid 92 | result['markers'] = report.test_marker 93 | self.append(result) 94 | 95 | 96 | def append_pass(self, report): 97 | status = "PASSED" 98 | message = None 99 | self.build_result(report, status, message) 100 | 101 | 102 | def append_failure(self, report): 103 | 104 | if hasattr(report, "wasxfail"): 105 | status = "XPASSED" 106 | message = "xfail-marked test passes Reason: %s " % report.wasxfail 107 | 108 | else: 109 | if hasattr(report.longrepr, "reprcrash"): 110 | message = report.longrepr.reprcrash.message 111 | elif isinstance(report.longrepr, (unicode, str)): 112 | message = report.longrepr 113 | else: 114 | message = str(report.longrepr) 115 | 116 | status = "FAILED" 117 | 118 | self.build_result(report, status, message) 119 | 120 | 121 | def append_error(self, report): 122 | 123 | message = report.longrepr 124 | status = "ERROR" 125 | self.build_result(report, status, message) 126 | 127 | 128 | def append_skipped(self, report): 129 | 130 | if hasattr(report, "wasxfail"): 131 | status = "XFAILED" 132 | message = "expected test failure Reason: %s " % report.wasxfail 133 | 134 | else: 135 | status = "SKIPPED" 136 | _, _, message = report.longrepr 137 | if message.startswith("Skipped: "): 138 | message = message[9:] 139 | 140 | self.build_result(report, status, message) 141 | 142 | 143 | def build_tests(self, item): 144 | 145 | result = OrderedDict() 146 | names = mangle_test_address(item.nodeid) 147 | 148 | result['suite_name'] = names[-2] 149 | result['test_name'] = names[-1] 150 | if item.obj.__doc__ is None: 151 | result['description'] = item.obj.__doc__ 152 | else: 153 | result['description'] = item.obj.__doc__.strip() 154 | result['file_name'] = item.location[0] 155 | test_marker = [] 156 | test_message = [] 157 | for k, v in item.keywords.items(): 158 | if isinstance(v, list): 159 | for x in v: 160 | if isinstance(x, Mark): 161 | if x.name != 'usefixtures': 162 | test_marker.append(x.name) 163 | if x.kwargs: 164 | test_message.append(x.kwargs.get('reason')) 165 | 166 | test_markers = ', '.join(test_marker) 167 | result['markers'] = test_markers 168 | 169 | test_messages = ', '.join(test_message) 170 | result['message'] = test_messages 171 | self.append(result) 172 | 173 | 174 | def append_tests(self, item): 175 | 176 | self.build_tests(item) 177 | 178 | 179 | 180 | @pytest.mark.trylast 181 | def pytest_collection_modifyitems(self, session, config, items): 182 | """ called after collection has been performed, may filter or re-order 183 | the items in-place.""" 184 | if session.config.option.collectonly: 185 | for item in items: 186 | self.append_tests(item) 187 | 188 | 189 | 190 | @pytest.mark.hookwrapper 191 | def pytest_runtest_makereport(self, item, call): 192 | 193 | outcome = yield 194 | 195 | report = outcome.get_result() 196 | report.test_doc = item.obj.__doc__ 197 | test_marker = [] 198 | for k, v in item.keywords.items(): 199 | if isinstance(v,list): 200 | for x in v: 201 | if isinstance(x,Mark): 202 | test_marker.append(x.name) 203 | report.test_marker = ', '.join(test_marker) 204 | 205 | 206 | def pytest_runtest_logreport(self, report): 207 | 208 | if report.passed: 209 | if report.when == "call": # ignore setup/teardown 210 | self.append_pass(report) 211 | 212 | elif report.failed: 213 | if report.when == "call": 214 | self.append_failure(report) 215 | 216 | else: 217 | self.append_error(report) 218 | 219 | elif report.skipped: 220 | self.append_skipped(report) 221 | 222 | 223 | def pytest_sessionfinish(self, session): 224 | if not hasattr(session.config, 'slaveinput'): 225 | if self.results: 226 | fieldnames = list(self.results[0]) 227 | self.create_sheet(fieldnames) 228 | self.update_worksheet() 229 | self.save_excel() 230 | 231 | 232 | def pytest_terminal_summary(self, terminalreporter): 233 | if self.results: 234 | terminalreporter.write_sep("-", "excel report: %s" % (self.excelpath)) 235 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import io 4 | 5 | from setuptools import setup 6 | 7 | 8 | 9 | setup(name='pytest-excel', 10 | version='1.7.0', 11 | description='pytest plugin for generating excel reports', 12 | long_description=io.open('README.rst', encoding='utf-8', errors='ignore').read(), 13 | author='santosh', 14 | author_email=u'santosh.srikanta@gmail.com', 15 | url=u'https://github.com/ssrikanta/pytest-excel', 16 | license = 'MIT', 17 | license_file = 'LICENSE', 18 | packages=['pytest_excel'], 19 | entry_points={'pytest11': ['excel = pytest_excel.pytest_excel']}, 20 | install_requires=['pytest>3.6', 'pandas'], 21 | keywords='py.test pytest excel report', 22 | classifiers=[ 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Framework :: Pytest', 25 | 'Intended Audience :: Developers', 26 | ' License :: OSI Approved :: MIT License', 27 | 'Operating System :: POSIX', 28 | 'Operating System :: Microsoft :: Windows', 29 | 'Operating System :: MacOS :: MacOS X', 30 | 'Topic :: Software Development :: Testing', 31 | 'Topic :: Software Development :: Quality Assurance', 32 | 'Topic :: Software Development :: Libraries', 33 | 'Topic :: Utilities', 34 | 'Programming Language :: Python :: 3.8', 35 | 'Programming Language :: Python :: 3.9', 36 | 'Programming Language :: Python :: 3.10', 37 | 'Programming Language :: Python :: 3.11', 38 | 'Programming Language :: Python :: 3.12', 39 | 40 | ] 41 | ) 42 | 43 | -------------------------------------------------------------------------------- /test_excel_report.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | 5 | class Test_Excel_Report(object): 6 | 7 | 8 | def test_excel_report_01(self): 9 | """ 10 | Scenario: test_excel_report_01 11 | """ 12 | assert True 13 | 14 | 15 | @pytest.mark.xfail(reason="passed Simply") 16 | def test_excel_report_02(self): 17 | """ 18 | Scenario: test_excel_report_02 19 | """ 20 | assert True 21 | 22 | 23 | @pytest.mark.skip(reason="Skip for No Reason") 24 | def test_excel_report_03(self): 25 | """ 26 | Scenario: test_excel_report_01 27 | """ 28 | assert True 29 | 30 | 31 | @pytest.mark.xfail(reason="Failed Simply") 32 | def test_excel_report_04(self): 33 | """ 34 | Scenario: test_excel_report_05 35 | """ 36 | assert False 37 | 38 | 39 | def test_excel_report_05(self): 40 | """ 41 | Scenario: test_excel_report_06 42 | """ 43 | assert True is False 44 | 45 | 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27-pytest29-supported-xdist, 8 | py27-pytest29-unsupported-xdist, 9 | py34-pytest29-supported-xdist, 10 | py35-pytest29-supported-xdist, 11 | py35-pytest29-unsupported-xdist, 12 | pypy-pytest29-supported-xdist 13 | 14 | [testenv] 15 | passenv = CI TRAVIS_BUILD_ID TRAVIS TRAVIS_BRANCH TRAVIS_JOB_NUMBER TRAVIS_PULL_REQUEST TRAVIS_JOB_ID TRAVIS_REPO_SLUG TRAVIS_COMMIT 16 | deps = 17 | pytest29: pytest>=2.9,<2.10 18 | openpyxl 19 | supported-xdist: pytest-xdist>=1.14 20 | unsupported-xdist: pytest-xdist<1.14 21 | pytest-rerunfailures 22 | commands = 23 | py.test --cov=./ {posargs:test_excel_report.py} 24 | codecov -e TOXENV 25 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.7.0 2 | --------------------------------------------------------------------------------