From e90e9f567b67802686818a51d5f4818956df1d83 Mon Sep 17 00:00:00 2001 From: Teejay Date: Sun, 10 Sep 2023 22:36:41 -0700 Subject: [PATCH] Merge v0.2.0 (#5) * Add type hints * Remove html support * Refactor plugin * Update unittests * Bump triple --- .github/workflows/ci.yml | 2 +- README.md | 2 +- pyproject.toml | 14 ++--- pytest_timestamps/plugin.py | 115 +++++++++++++++--------------------- tests/test_plugin.py | 47 +++++++++++++-- 5 files changed, 96 insertions(+), 84 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe23435..c2ff712 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index f405902..2277a8a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The timestamps used depend whether Pytest is running in `verbose` mode.\ **non verbose:** module\ ![](https://i.ibb.co/0qLXFjB/Screenshot-from-2022-01-10-22-00-26.png) -Timestamps will also be added to the test report if [pytest-html](https://github.com/pytest-dev/pytest-html) is installed. +***Note:*** *fixture setup/teardown times are omitted from the timestamp* diff --git a/pyproject.toml b/pyproject.toml index 410d884..e7425b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,18 @@ [tool.poetry] name = "pytest-timestamps" -version = "0.1.4" +version = "0.2.0" description = "A simple plugin to view timestamps for each test" authors = ["TJ "] packages = [{ include = "pytest_timestamps" }] [tool.poetry.dependencies] -python = ">=3.7, <4.0" -pytest = ">=5.2" +python = ">=3.8, <4.0" +pytest = "^7.3" [tool.poetry.group.dev.dependencies] -freezegun = "^1.0" -ruff = "^0.0.260" -black = "^23.3.0" +ruff = "^0.0.287" +black = "^23.9.1" +mypy = "^1.5.1" [tool.poetry.plugins."pytest11"] timestamps = "pytest_timestamps.plugin" @@ -45,4 +45,4 @@ legacy_tox_ini = """ commands = poetry install --only dev poetry run pytest -v -""" \ No newline at end of file +""" diff --git a/pytest_timestamps/plugin.py b/pytest_timestamps/plugin.py index f6f3b1c..500d789 100644 --- a/pytest_timestamps/plugin.py +++ b/pytest_timestamps/plugin.py @@ -1,48 +1,62 @@ from datetime import datetime +from _pytest.config import Config + import pytest from _pytest.terminal import TerminalReporter +from _pytest.reports import TestReport + +from typing import Final, Optional + + +def format(timestamp: float) -> str: + return datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") + +class Timestamp: + start: Optional[float] = None + stop: Optional[float] = None -def format_timestamp(ts): - return datetime.fromtimestamp(ts).strftime("%H:%M:%S") + def clear(self) -> None: + self.start = None + self.stop = None + def get(self) -> str: + return f"[{format(self.start)} - {format(self.stop)}]" # type: ignore -class Timestamped(TerminalReporter): - color = "blue" + def is_valid(self) -> bool: + return bool(self.start and self.stop) - def __init__(self, reporter): - TerminalReporter.__init__(self, reporter.config) - self._node = None - self._fspath = None - self._last_fspath = None + def update(self, report: TestReport) -> None: + if report.when == "call": + if not self.start: + self.start = report.start + self.stop = report.stop - def _get_timestamps(self): - if self.verbosity > 0: - times = self._node - else: - times = self._fspath - return [format_timestamp(i) for i in times] - def _write_ts_to_terminal(self): - start, stop = self._get_timestamps() - ts_line = f"[{start} - {stop}]" - w = self._width_of_current_line - fill = self._tw.fullwidth - w - 10 - self.write(ts_line.rjust(fill), **{self.color: True}) +class TimestampReporter(TerminalReporter): # type: ignore + color: str = "blue" + dedent: int = 10 + timestamp: Final = Timestamp() - def _write_progress_information_filling_space(self): - self._write_ts_to_terminal() + def __init__(self, config: Config) -> None: + TerminalReporter.__init__(self, config) + + def _write_timestamp(self) -> None: + line_width = self._width_of_current_line + total_width = self._tw.fullwidth + fill = total_width - line_width - self.dedent + timestamp = self.timestamp.get().rjust(fill) + self.write(timestamp, **{self.color: True}) + + def _write_progress_information_filling_space(self) -> None: + if self.timestamp.is_valid(): + self._write_timestamp() super()._write_progress_information_filling_space() + self.timestamp.clear() - def pytest_runtest_logreport(self, report): - if len(report.timestamps) == 3: - if report.fspath != self._last_fspath: - self._last_fspath = report.fspath - self._fspath = report.timestamps[1:3] - else: - self._fspath[1] = report.timestamps[2] - self._node = report.timestamps[1:3] + def pytest_runtest_logreport(self, report: TestReport) -> None: + self.timestamp.update(report) super().pytest_runtest_logreport(report) @@ -51,41 +65,4 @@ def pytest_configure(config): if config.pluginmanager.has_plugin("terminalreporter"): reporter = config.pluginmanager.get_plugin("terminalreporter") config.pluginmanager.unregister(reporter, "terminalreporter") - config.pluginmanager.register(Timestamped(reporter), "terminalreporter") - if config.pluginmanager.has_plugin("html"): - global html - from py.xml import html - - -@pytest.hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item, call): - """Record timestamps to the test report.""" - - if call.when == "setup": - item._timestamps = [call.start] - - elif call.when == "call": - item._timestamps.append(call.start) - item._timestamps.append(call.stop) - - else: - item._timestamps.append(call.stop) - - output = yield - report = output.get_result() - report.timestamps = item._timestamps - - -@pytest.hookimpl(optionalhook=True) -def pytest_html_results_table_header(cells): - cells.insert(2, html.th("Setup Start")) - cells.insert(3, html.th("Test Start")) - cells.insert(4, html.th("Test Stop")) - cells.insert(5, html.th("Teardown Stop")) - - -@pytest.hookimpl(optionalhook=True) -def pytest_html_results_table_row(report, cells): - if len(report.timestamps) == 4: - for idx, ts in enumerate(report.timestamps): - cells.insert(idx + 2, html.td(format_timestamp(ts))) + config.pluginmanager.register(TimestampReporter(config), "terminalreporter") diff --git a/tests/test_plugin.py b/tests/test_plugin.py index ca399f0..5119e57 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,14 +1,20 @@ import pytest -from freezegun import freeze_time +from pytest_timestamps.plugin import TimestampReporter + +from unittest.mock import patch pytest_plugins = "pytester" @pytest.fixture def timestamp(): - ts = "01:01:01" - with freeze_time(f"2000-01-01 {ts}"): - yield f"[{ts} - {ts}]" + instance = TimestampReporter.timestamp + + # Prevent times from being cleared once printed to terminal + with patch.object(instance, "clear"): + yield instance + + instance.clear() def test_timestamps_normal(pytester, timestamp): @@ -21,8 +27,9 @@ def test_plugin(): """ ) result = pytester.runpytest() + assert timestamp.is_valid() result.assert_outcomes(passed=1) - assert timestamp in result.stdout.str() + assert timestamp.get() in result.stdout.str() def test_timestamps_verbose(pytester, timestamp): @@ -35,5 +42,33 @@ def test_plugin(): """ ) result = pytester.runpytest("-v") + assert timestamp.is_valid() result.assert_outcomes(passed=1) - assert timestamp in result.stdout.str() + assert timestamp.get() in result.stdout.str() + + +def test_timestamp_is_cleared(pytester, timestamp): + pytester.makepyfile( + """ + import pytest + + def test_plugin(): + assert True + """ + ) + pytester.runpytest() + timestamp.clear.assert_called_once() + + +def test_timestamps_with_skip_decorator(pytester): + pytester.makepyfile( + """ + import pytest + + @pytest.mark.skip + def test_plugin(): + assert True + """ + ) + result = pytester.runpytest() + result.assert_outcomes(skipped=1)