├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── README.md ├── pyproject.toml ├── pytest_pretty └── __init__.py ├── requirements ├── all.txt ├── linting.in ├── linting.txt └── pyproject.txt ├── screenshots ├── realtime-error-summary.png ├── table-of-failures.png └── test-run-summary.png └── tests └── test_simple.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '**' 9 | pull_request: {} 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: set up python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.10' 22 | 23 | - uses: actions/cache@v3 24 | id: cache 25 | with: 26 | path: ${{ env.pythonLocation }} 27 | key: > 28 | lint 29 | ${{ runner.os }} 30 | ${{ env.pythonLocation }} 31 | ${{ hashFiles('requirements/linting.txt') }} 32 | 33 | - name: install 34 | if: steps.cache.outputs.cache-hit != 'true' 35 | run: pip install -r requirements/linting.txt 36 | 37 | - uses: pre-commit/action@v3.0.0 38 | with: 39 | extra_args: --all-files --verbose 40 | 41 | test: 42 | name: test py${{ matrix.python-version }} on ${{ matrix.os }} 43 | 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | os: [ubuntu, macos, windows] 48 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 49 | 50 | runs-on: ${{ matrix.os }}-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | 54 | - name: set up python 55 | uses: actions/setup-python@v4 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | 59 | - id: cache-py 60 | name: cache python 61 | uses: actions/cache@v3 62 | with: 63 | path: ${{ env.pythonLocation }} 64 | key: > 65 | py 66 | ${{ runner.os }} 67 | ${{ env.pythonLocation }} 68 | ${{ hashFiles('requirements/pyproject.txt') }} 69 | ${{ hashFiles('pyproject.toml') }} 70 | 71 | - run: pip install -r requirements/pyproject.txt 72 | if: steps.cache-py.outputs.cache-hit != 'true' 73 | 74 | - run: pip install -e . 75 | - run: pip freeze 76 | - run: pytest 77 | 78 | # https://github.com/marketplace/actions/alls-green#why used for branch protection checks 79 | check: 80 | if: always() 81 | needs: [lint, test] 82 | runs-on: ubuntu-latest 83 | steps: 84 | - name: Decide whether the needed jobs succeeded or failed 85 | uses: re-actors/alls-green@release/v1 86 | with: 87 | jobs: ${{ toJSON(needs) }} 88 | 89 | deploy: 90 | name: Deploy 91 | needs: [check] 92 | if: "success() && startsWith(github.ref, 'refs/tags/')" 93 | runs-on: ubuntu-latest 94 | 95 | steps: 96 | - uses: actions/checkout@v2 97 | 98 | - name: set up python 99 | uses: actions/setup-python@v4 100 | with: 101 | python-version: '3.10' 102 | 103 | - name: install 104 | run: pip install -U twine build 105 | 106 | - name: check GITHUB_REF matches package version 107 | uses: samuelcolvin/check-python-version@v3 108 | with: 109 | version_file_path: pytest_pretty/__init__.py 110 | 111 | - name: build 112 | run: python -m build 113 | 114 | - run: twine check --strict dist/* 115 | 116 | - name: upload to pypi 117 | run: twine upload dist/* 118 | env: 119 | TWINE_USERNAME: __token__ 120 | TWINE_PASSWORD: ${{ secrets.pypi_token }} 121 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | env/ 3 | venv/ 4 | .venv/ 5 | env3*/ 6 | Pipfile 7 | *.lock 8 | *.py[cod] 9 | *.egg-info/ 10 | .python-version 11 | /build/ 12 | dist/ 13 | .cache/ 14 | .mypy_cache/ 15 | test.py 16 | .coverage 17 | .hypothesis 18 | /htmlcov/ 19 | /benchmarks/*.json 20 | /docs/.changelog.md 21 | /docs/.version.md 22 | /docs/.tmp_schema_mappings.html 23 | /docs/.tmp_examples/ 24 | /docs/.tmp-projections/ 25 | /docs/usage/.tmp-projections/ 26 | /site/ 27 | /site.zip 28 | .pytest_cache/ 29 | .vscode/ 30 | _build/ 31 | pydantic/*.c 32 | pydantic/*.so 33 | .auto-format 34 | /sandbox/ 35 | /.ghtopdep_cache/ 36 | /fastapi/ 37 | /codecov.sh 38 | /worktrees/ 39 | /.ruff_cache/ 40 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-toml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | 10 | - repo: local 11 | hooks: 12 | - id: lint 13 | name: Lint 14 | entry: make lint 15 | types: [python] 16 | language: system 17 | pass_filenames: false 18 | - id: pyupgrade 19 | name: Pyupgrade 20 | entry: pyupgrade --py37-plus 21 | types: [python] 22 | language: system 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 to present Samuel Colvin 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | sources = pytest_pretty 3 | 4 | .PHONY: install 5 | install: 6 | pip install -U pip 7 | pip install -r requirements/all.txt 8 | pip install -e . 9 | pre-commit install 10 | 11 | .PHONY: refresh-lockfiles 12 | refresh-lockfiles: 13 | @echo "Updating requirements/*.txt files using pip-compile" 14 | find requirements/ -name '*.txt' ! -name 'all.txt' -type f -delete 15 | pip-compile -q --resolver backtracking -o requirements/linting.txt requirements/linting.in 16 | pip-compile -q --resolver backtracking -o requirements/pyproject.txt pyproject.toml 17 | pip install --dry-run -r requirements/all.txt 18 | 19 | .PHONY: format 20 | format: 21 | pyupgrade --py37-plus --exit-zero-even-if-changed `find $(sources) -name "*.py" -type f` 22 | isort $(sources) 23 | black $(sources) 24 | 25 | .PHONY: lint 26 | lint: 27 | ruff $(sources) 28 | isort $(sources) --check-only --df 29 | black $(sources) --check --diff 30 | 31 | .PHONY: all 32 | all: lint 33 | 34 | .PHONY: clean 35 | clean: 36 | rm -rf `find . -name __pycache__` 37 | rm -f `find . -type f -name '*.py[co]' ` 38 | rm -f `find . -type f -name '*~' ` 39 | rm -f `find . -type f -name '.*~' ` 40 | rm -rf .cache 41 | rm -rf .pytest_cache 42 | rm -rf htmlcov 43 | rm -rf *.egg-info 44 | rm -f .coverage 45 | rm -f .coverage.* 46 | rm -rf build 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-pretty 2 | 3 | [![CI](https://github.com/samuelcolvin/pytest-pretty/workflows/CI/badge.svg?event=push)](https://github.com/samuelcolvin/pytest-pretty/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) 4 | [![pypi](https://img.shields.io/pypi/v/pytest-pretty.svg)](https://pypi.python.org/pypi/pytest-pretty) 5 | [![versions](https://img.shields.io/pypi/pyversions/pytest-pretty.svg)](https://github.com/samuelcolvin/pytest-pretty) 6 | [![license](https://img.shields.io/github/license/samuelcolvin/pytest-pretty.svg)](https://github.com/samuelcolvin/pytest-pretty/blob/main/LICENSE) 7 | 8 | Opinionated pytest plugin to make output slightly easier to read and errors easy to find and fix. 9 | 10 | pytest-pretty's only dependencies are [rich](https://pypi.org/project/rich/) and pytest itself. 11 | 12 | ### Realtime error summary 13 | 14 | One-line info on which test has failed while tests are running: 15 | 16 | ![Realtime Error Summary](./screenshots/realtime-error-summary.png) 17 | 18 | ### Table of failures 19 | 20 | A rich table of failures with both test line number and error line number: 21 | 22 | ![Table of Failures](./screenshots/table-of-failures.png) 23 | 24 | This is extremely useful for navigating to failed tests without having to scroll through the entire test output. 25 | 26 | ### Prettier Summary of a Test Run 27 | 28 | Including time taken for the test run: 29 | 30 | ![Test Run Summary](./screenshots/test-run-summary.png) 31 | 32 | ## Installation 33 | 34 | ```sh 35 | pip install -U pytest-pretty 36 | ``` 37 | 38 | ## Usage with GitHub Actions 39 | 40 | If you're using pytest-pretty (or indeed, just pytest) with GitHub Actions, it's worth adding the following to the top of your workflow `.yml` file: 41 | 42 | ```yaml 43 | env: 44 | COLUMNS: 120 45 | ``` 46 | 47 | This will mean the pytest output is wider and easier to use, more importantly, it'll make the error summary table printed by pytest-pretty much easier to read, see [this](https://github.com/Textualize/rich/issues/2769) discussion for more details. 48 | 49 | ## `pytester_pretty` fixture 50 | 51 | The `pytest_pretty` provides `pytester_pretty` fixture that work with modified version of output. It is designed to drop in places replacement of `pytester` fixture and uses it internaly. 52 | 53 | So to use them it is required to set `pytest_plugins = "pytester"` as mentioned in pytest documentation 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['hatchling'] 3 | build-backend = 'hatchling.build' 4 | 5 | [tool.hatch.version] 6 | path = 'pytest_pretty/__init__.py' 7 | 8 | [tool.hatch.build.targets.sdist] 9 | # limit which files are included in the sdist (.tar.gz) asset, 10 | # see https://github.com/pydantic/pydantic/pull/4542 11 | include = [ 12 | '/README.md', 13 | '/Makefile', 14 | '/pytest_pretty', 15 | '/requirements', 16 | ] 17 | 18 | [project] 19 | name = 'pytest-pretty' 20 | description = 'pytest plugin for printing summary data as I want it' 21 | authors = [ 22 | {name = 'Samuel Colvin', email = 's@muelcolvin.com'}, 23 | ] 24 | license = {file = 'LICENSE'} 25 | readme = 'README.md' 26 | classifiers = [ 27 | 'Development Status :: 5 - Production/Stable', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 3', 30 | 'Programming Language :: Python :: 3 :: Only', 31 | 'Programming Language :: Python :: 3.7', 32 | 'Programming Language :: Python :: 3.8', 33 | 'Programming Language :: Python :: 3.9', 34 | 'Programming Language :: Python :: 3.10', 35 | 'Programming Language :: Python :: 3.11', 36 | 'Intended Audience :: Developers', 37 | 'Intended Audience :: Information Technology', 38 | 'Intended Audience :: System Administrators', 39 | 'License :: OSI Approved :: MIT License', 40 | 'Operating System :: Unix', 41 | 'Operating System :: POSIX :: Linux', 42 | 'Environment :: Console', 43 | 'Environment :: MacOS X', 44 | 'Framework :: Hypothesis', 45 | 'Topic :: Software Development :: Libraries :: Python Modules', 46 | 'Topic :: Internet', 47 | ] 48 | requires-python = '>=3.7' 49 | dynamic = ['version'] 50 | dependencies = [ 51 | 'pytest>=7', 52 | 'rich>=12', 53 | ] 54 | 55 | [project.entry-points.pytest11] 56 | pretty = 'pytest_pretty' 57 | 58 | [project.urls] 59 | repository = 'https://github.com/samuelcolvin/pytest-pretty' 60 | 61 | [tool.pytest.ini_options] 62 | testpaths = 'tests' 63 | filterwarnings = 'error' 64 | xfail_strict = true 65 | 66 | [tool.ruff] 67 | line-length = 120 68 | extend-select = ['Q'] 69 | flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'} 70 | 71 | [tool.coverage.run] 72 | source = ['pytest_pretty'] 73 | branch = true 74 | 75 | [tool.coverage.report] 76 | precision = 2 77 | exclude_lines = [ 78 | 'pragma: no cover', 79 | 'raise NotImplementedError', 80 | 'if TYPE_CHECKING:', 81 | '@overload', 82 | ] 83 | 84 | [tool.black] 85 | color = true 86 | line-length = 120 87 | target-version = ['py310'] 88 | skip-string-normalization = true 89 | 90 | [tool.isort] 91 | line_length = 120 92 | known_first_party = 'pydantic' 93 | multi_line_output = 3 94 | include_trailing_comma = true 95 | force_grid_wrap = 0 96 | combine_as_imports = true 97 | -------------------------------------------------------------------------------- /pytest_pretty/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations as _annotations 2 | 3 | import re 4 | import sys 5 | from itertools import dropwhile 6 | from time import perf_counter_ns 7 | from typing import TYPE_CHECKING, Callable 8 | 9 | import pytest 10 | from _pytest.terminal import TerminalReporter 11 | from rich.console import Console 12 | from rich.markup import escape 13 | from rich.table import Table 14 | 15 | if TYPE_CHECKING: 16 | from _pytest.reports import TestReport 17 | 18 | SummaryStats = tuple[list[tuple[str, dict[str, bool]]], str] 19 | 20 | __version__ = '1.2.0' 21 | start_time = 0 22 | end_time = 0 23 | console = Console() 24 | 25 | 26 | def pytest_sessionstart(session): 27 | global start_time 28 | start_time = perf_counter_ns() 29 | 30 | 31 | def pytest_sessionfinish(session, exitstatus): 32 | global end_time 33 | end_time = perf_counter_ns() 34 | 35 | 36 | class CustomTerminalReporter(TerminalReporter): 37 | def pytest_runtest_logreport(self, report: TestReport) -> None: 38 | super().pytest_runtest_logreport(report) 39 | if not report.failed: 40 | return None 41 | if report.when == 'teardown' and 'devtools-insert-assert:' in repr(report.longrepr): 42 | # special case for devtools insert_assert "failures" 43 | return None 44 | file, line, func = report.location 45 | self._write_progress_information_filling_space() 46 | self.ensure_newline() 47 | summary = f'{file}:{line} {func}' 48 | self._tw.write(summary, red=True) 49 | try: 50 | msg = report.longrepr.reprcrash.message 51 | except AttributeError: 52 | pass 53 | else: 54 | msg = msg.replace('\n', ' ') 55 | available_space = self._tw.fullwidth - len(summary) - 15 56 | if available_space > 5: 57 | self._tw.write(f' - {msg[:available_space]}…') 58 | 59 | def summary_stats(self) -> None: 60 | time_taken_ns = end_time - start_time 61 | summary_items, _ = self.build_summary_stats_line() 62 | console.print(f'[bold]Results ({time_taken_ns / 1_000_000_000:0.2f}s):[/]', highlight=False) 63 | for summary_item in summary_items: 64 | msg, text_format = summary_item 65 | text_format.pop('bold', None) 66 | color = next(k for k, v in text_format.items() if v) 67 | count, label = msg.split(' ', 1) 68 | console.print(f'{count:>10} {label}', style=color) 69 | 70 | def short_test_summary(self) -> None: 71 | summary_items, _ = self.build_summary_stats_line() 72 | fail_reports = self.stats.get('failed', []) 73 | if fail_reports: 74 | table = Table(title='Summary of Failures', padding=(0, 2), border_style='cyan') 75 | table.add_column('File') 76 | table.add_column('Function', style='bold') 77 | table.add_column('Function Line', style='bold') 78 | table.add_column('Error Line') 79 | table.add_column('Error') 80 | for report in fail_reports: 81 | file, function_line, func = report.location 82 | try: 83 | repr_entries = report.longrepr.chain[-1][0].reprentries 84 | error_line = str(repr_entries[0].reprfileloc.lineno) 85 | error = repr_entries[-1].reprfileloc.message 86 | except AttributeError: 87 | error_line = '' 88 | error = '' 89 | 90 | table.add_row( 91 | escape(file), 92 | escape(func), 93 | str(function_line + 1), 94 | escape(error_line), 95 | escape(error), 96 | ) 97 | console.print(table) 98 | 99 | 100 | @pytest.hookimpl(trylast=True) 101 | def pytest_configure(config): 102 | # Get the standard terminal reporter plugin and replace it with our 103 | standard_reporter = config.pluginmanager.getplugin('terminalreporter') 104 | custom_reporter = CustomTerminalReporter(config, sys.stdout) 105 | config.pluginmanager.unregister(standard_reporter) 106 | config.pluginmanager.register(custom_reporter, 'terminalreporter') 107 | 108 | 109 | ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') 110 | stat_re = re.compile(r'(\d+) (\w+)') 111 | 112 | 113 | def create_new_parseoutcomes(runresult_instance) -> Callable[[], dict[str, int]]: 114 | """ 115 | In this function there is a new implementation of `RunResult.parseoutcomes` 116 | https://github.com/pytest-dev/pytest/blob/4a46ee8bc957b06265c016cc837862447dde79d2/src/_pytest/pytester.py#L557 117 | 118 | 119 | Decision of reimplement this method is made based on implementation of 120 | `RunResult.assert_outcomes` 121 | 122 | https://github.com/pytest-dev/pytest/blob/4a46ee8bc957b06265c016cc837862447dde79d2/src/_pytest/pytester.py#L613 123 | """ 124 | 125 | def parseoutcomes() -> dict[str, int]: 126 | lines_with_stats = dropwhile(lambda x: 'Results' not in x, runresult_instance.outlines) 127 | next(lines_with_stats) # drop Results line 128 | res = {} 129 | for i, line in enumerate(lines_with_stats): 130 | line = ansi_escape.sub('', line).strip() # clean colors 131 | match = stat_re.match(line) 132 | 133 | if match is None: 134 | break 135 | 136 | res[match.group(2)] = int(match.group(1)) 137 | 138 | return res 139 | 140 | return parseoutcomes 141 | 142 | 143 | class PytesterWrapper: 144 | """ 145 | This is class for for make almost transparent wrapper 146 | arround pytester output and allow substitute 147 | `parseoutcomes` method of `RunResult` instance. 148 | """ 149 | 150 | __slot__ = ('_pytester',) 151 | 152 | def __init__(self, pytester): 153 | object.__setattr__(self, '_pytester', pytester) 154 | 155 | def runpytest(self, *args, **kwargs): 156 | """wraper to overwritte `parseoutcomes` method of `RunResult` instance""" 157 | res = self._pytester.runpytest(*args, **kwargs) 158 | assert res is not None 159 | res.parseoutcomes = create_new_parseoutcomes(res) 160 | return res 161 | 162 | def __getattr__(self, name): 163 | return getattr(self._pytester, name) 164 | 165 | def __setattr__(self, name, value): 166 | setattr(self._pytester, name, value) 167 | 168 | 169 | @pytest.fixture() 170 | def pytester_pretty(pytester): 171 | return PytesterWrapper(pytester) 172 | -------------------------------------------------------------------------------- /requirements/all.txt: -------------------------------------------------------------------------------- 1 | -r ./linting.txt 2 | -r ./pyproject.txt 3 | -------------------------------------------------------------------------------- /requirements/linting.in: -------------------------------------------------------------------------------- 1 | black 2 | ruff 3 | isort[colors] 4 | pyupgrade 5 | pre-commit 6 | -------------------------------------------------------------------------------- /requirements/linting.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements/linting.txt --resolver=backtracking requirements/linting.in 6 | # 7 | black==22.12.0 8 | # via -r requirements/linting.in 9 | cfgv==3.3.1 10 | # via pre-commit 11 | click==8.1.3 12 | # via black 13 | colorama==0.4.6 14 | # via isort 15 | distlib==0.3.6 16 | # via virtualenv 17 | filelock==3.9.0 18 | # via virtualenv 19 | identify==2.5.13 20 | # via pre-commit 21 | isort[colors]==5.11.4 22 | # via -r requirements/linting.in 23 | mypy-extensions==0.4.3 24 | # via black 25 | nodeenv==1.7.0 26 | # via pre-commit 27 | pathspec==0.10.3 28 | # via black 29 | platformdirs==2.6.2 30 | # via 31 | # black 32 | # virtualenv 33 | pre-commit==2.21.0 34 | # via -r requirements/linting.in 35 | pyupgrade==3.3.1 36 | # via -r requirements/linting.in 37 | pyyaml==6.0 38 | # via pre-commit 39 | ruff==0.0.229 40 | # via -r requirements/linting.in 41 | tokenize-rt==5.0.0 42 | # via pyupgrade 43 | tomli==2.0.1 44 | # via black 45 | virtualenv==20.17.1 46 | # via pre-commit 47 | 48 | # The following packages are considered to be unsafe in a requirements file: 49 | # setuptools 50 | -------------------------------------------------------------------------------- /requirements/pyproject.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements/pyproject.txt --resolver=backtracking pyproject.toml 6 | # 7 | attrs==22.2.0 8 | # via pytest 9 | exceptiongroup==1.1.0 10 | # via pytest 11 | iniconfig==2.0.0 12 | # via pytest 13 | markdown-it-py==2.1.0 14 | # via rich 15 | mdurl==0.1.2 16 | # via markdown-it-py 17 | packaging==23.0 18 | # via pytest 19 | pluggy==1.0.0 20 | # via pytest 21 | pygments==2.14.0 22 | # via rich 23 | pytest==7.2.1 24 | # via pytest-pretty (pyproject.toml) 25 | rich==13.2.0 26 | # via pytest-pretty (pyproject.toml) 27 | tomli==2.0.1 28 | # via pytest 29 | -------------------------------------------------------------------------------- /screenshots/realtime-error-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelcolvin/pytest-pretty/af6cd38ea37e5a3879428f401804d1da72177c81/screenshots/realtime-error-summary.png -------------------------------------------------------------------------------- /screenshots/table-of-failures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelcolvin/pytest-pretty/af6cd38ea37e5a3879428f401804d1da72177c81/screenshots/table-of-failures.png -------------------------------------------------------------------------------- /screenshots/test-run-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelcolvin/pytest-pretty/af6cd38ea37e5a3879428f401804d1da72177c81/screenshots/test-run-summary.png -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest_plugins = "pytester" 4 | 5 | 6 | test_str = """ 7 | import pytest 8 | 9 | def test_add(): 10 | assert 1 + 2 == 3 11 | 12 | 13 | @pytest.mark.xfail(reason='This test is expected to fail') 14 | def test_subtract(): 15 | assert 2 - 1 == 0 16 | 17 | 18 | @pytest.mark.skipif(True, reason='This test is skipped') 19 | def test_multiply(): 20 | assert 2 * 2 == 5 21 | """ 22 | 23 | def test_base(pytester): 24 | pytester.makepyfile(test_str) 25 | result = pytester.runpytest() 26 | assert any("passed" in x and "1" in x for x in result.outlines) 27 | assert any("skipped" in x and "1" in x for x in result.outlines) 28 | assert any("xfailed" in x and "1" in x for x in result.outlines) 29 | 30 | 31 | def test_fixture(pytester_pretty): 32 | pytester_pretty.makepyfile(test_str) 33 | result = pytester_pretty.runpytest() 34 | result.assert_outcomes(passed=1, skipped=1, xfailed=1) 35 | --------------------------------------------------------------------------------