From 8302d2e52cc36267a66d1a7c6b0ff52fb56bdbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Mon, 17 Jul 2023 20:51:18 +0200 Subject: [PATCH] I dunno --- pyproject.toml | 2 +- src/pytest_html/basereport.py | 76 +++++++---- src/pytest_html/report_data.py | 18 ++- src/pytest_html/resources/style.css | 4 +- src/pytest_html/table.py | 199 ++++++++++++++++++++++++++-- testing/legacy_test_pytest_html.py | 1 - 6 files changed, 256 insertions(+), 44 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e4bf9116..9bd04733 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ ] dependencies = [ "pytest>=7.0.0", - "pytest-metadata>=2.0.2", + "pytest-metadata>=3.0.0", "Jinja2>=3.0.0", ] dynamic = [ diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py index 52180428..5ea27862 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -12,10 +12,10 @@ from pytest_html import __version__ from pytest_html import extras -from pytest_html.table import Header -from pytest_html.table import Row from pytest_html.util import cleanup_unserializable +# from pytest_html.table import Row + class BaseReport: def __init__(self, report_path, config, report_data, template, css): @@ -60,8 +60,8 @@ def _generate_report(self, self_contained=False): self._write_report(rendered_report) - def _generate_environment(self): - metadata = self._config._metadata + def _generate_environment(self, metadata_key): + metadata = self._config.stash[metadata_key] # self._config._metadata for key in metadata.keys(): value = metadata[key] if self._is_redactable_environment_variable(key): @@ -145,16 +145,20 @@ def _write_report(self, rendered_report): @pytest.hookimpl(trylast=True) def pytest_sessionstart(self, session): - config = session.config - if hasattr(config, "_metadata") and config._metadata: - self._report.set_data("environment", self._generate_environment()) + # config = session.config + from pytest_metadata.plugin import metadata_key + + # if hasattr(config, "_metadata") and config._metadata: + self._report.set_data("environment", self._generate_environment(metadata_key)) session.config.hook.pytest_html_report_title(report=self._report) - header_cells = Header() - session.config.hook.pytest_html_results_table_header(cells=header_cells) - self._report.set_data("resultsTableHeader", header_cells.html) - self._report.set_data("headerPops", header_cells.get_pops()) + # header_cells = Header + headers = self._report.data["resultsTableHeader"] + session.config.hook.pytest_html_results_table_header(cells=headers) + + # self._report.set_data("resultsTableHeader", header_cells.html) + # self._report.set_data("headerPops", header_cells.get_pops()) self._report.set_data("runningState", "Started") self._generate_report() @@ -173,7 +177,8 @@ def pytest_sessionfinish(self, session): @pytest.hookimpl(trylast=True) def pytest_terminal_summary(self, terminalreporter): terminalreporter.write_sep( - "-", f"Generated html report: file://{self._report_path.resolve()}" + "-", + f"Generated html report: file://{self._report_path.resolve().as_posix()}", ) @pytest.hookimpl(trylast=True) @@ -188,31 +193,52 @@ def pytest_runtest_logreport(self, report): DeprecationWarning, ) - data = { - "duration": report.duration, - } + data = dict() + cells = list() + + result = _process_outcome(report) + data["result"] = result + cells.append(f'{result}') test_id = report.nodeid if report.when != "call": test_id += f"::{report.when}" data["testId"] = test_id + cells.append(f'{test_id}') + + cells.append(f'{report.duration}') + cells.append('') + + # headers = self._report.data["resultsTableHeader"] + # row = Row.from_headers(headers) + # data = { + # "duration": report.duration, + # } + # row.set("duration", value=data["duration"]) - row_cells = Row() - self._config.hook.pytest_html_results_table_row(report=report, cells=row_cells) - if row_cells.html is None: - return - data["resultsTableRow"] = row_cells.html - for sortable, value in row_cells.sortables.items(): - data[sortable] = value + # test_id = report.nodeid + # if report.when != "call": + # test_id += f"::{report.when}" + # data["testId"] = test_id + # row.set("test", value=data["testId"]) + + # data["result"] = _process_outcome(report) + # row.set("result", value=data["result"]) + + data["extras"] = self._process_extras(report, test_id) + # row.set("links", value=data["extras"]) + + self._config.hook.pytest_html_results_table_row(report=report, cells=cells) + # if not row: + # return + # ROW CAN PROBABLY REPLACE data + data["resultsTableRow"] = cells processed_logs = _process_logs(report) self._config.hook.pytest_html_results_table_html( report=report, data=processed_logs ) - data["result"] = _process_outcome(report) - data["extras"] = self._process_extras(report, test_id) - if self._report.add_test(data, report, processed_logs): self._generate_report() diff --git a/src/pytest_html/report_data.py b/src/pytest_html/report_data.py index aee5e938..4a39127e 100644 --- a/src/pytest_html/report_data.py +++ b/src/pytest_html/report_data.py @@ -6,18 +6,34 @@ from pytest_html.util import _handle_ansi +# from pytest_html.table import Header + class ReportData: def __init__(self, config): self._config = config + + default_headers = [ + '"Result"', + '"Test"', + '"Duration"', + '"Links"', + ] + + # default_headers = Header() + # default_headers.new("0", "result", sortable=True, _deletable=False) + # default_headers.new("1", "test", sortable=True) + # default_headers.new("2", "duration", sortable=True) + # default_headers.new("3", "links") + self._data = { "title": "", "collectedItems": 0, "runningState": "not_started", "environment": {}, "tests": defaultdict(list), - "resultsTableHeader": {}, "additionalSummary": defaultdict(list), + "resultsTableHeader": default_headers, } collapsed = config.getini("render_collapsed") diff --git a/src/pytest_html/resources/style.css b/src/pytest_html/resources/style.css index cee1863c..7a58b92f 100644 --- a/src/pytest_html/resources/style.css +++ b/src/pytest_html/resources/style.css @@ -207,7 +207,7 @@ div.media { .sortable { cursor: pointer; } -.sortable.asc:after { +.sortable.desc:after { content: " "; position: relative; left: 5px; @@ -217,7 +217,7 @@ div.media { border-left-color: transparent; border-right-color: transparent; } -.sortable.desc:after { +.sortable.asc:after { content: " "; position: relative; left: 5px; diff --git a/src/pytest_html/table.py b/src/pytest_html/table.py index 8eea6579..959b8b10 100644 --- a/src/pytest_html/table.py +++ b/src/pytest_html/table.py @@ -5,6 +5,191 @@ import warnings +class Header(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._append_counter = 0 + + def __setitem__(self, key, value): + warnings.warn( + "list-type assignment is deprecated and support " + "will be removed in a future release. " + "Please use `new()` instead.", + DeprecationWarning, + ) + self.new(key, **Header._values(value)) + + def __delitem__(self, key): + warnings.warn( + "`del` is deprecated and support " + "will be removed in a future release. " + "Please use `delete()` instead.", + DeprecationWarning, + ) + self.delete(key) + + def new( + self, index, name, sortable=False, html=None, _append=None, _deletable=True + ): + index = int(index) + sorted_keys = self._as_list() + sorted_keys.insert(index, name) + for index, name in enumerate(sorted_keys): + if name not in self.keys(): + value = { + "index": index, + "name": name, + "sortable": sortable, + "html": html, + "_append": _append, + "_deletable": _deletable, + } + super().__setitem__(name, value) + else: + self[name]["index"] = index + + def delete(self, name): + value = self[name] + if value["_deletable"]: + super().__delitem__(name) + return value + else: + warnings.warn( + f"{name} is not deletable!", + RuntimeWarning, + ) + + def append(self, data): + warnings.warn( + "`append()` is deprecated and support " + "will be removed in a future release." + "Please use `new()` instead.", + DeprecationWarning, + ) + self.new(len(self), **Header._values(data), _append=self._append_counter) + self._append_counter += 1 + + def insert(self, index, data): + warnings.warn( + "`insert()` is deprecated and support " + "will be removed in a future release." + "Please use `new()` instead.", + DeprecationWarning, + ) + self.new(index, **Header._values(data)) + + def pop(self, _): + warnings.warn( + "`pop()` is deprecated and support " + "will be removed in a future release. " + "Please use `delete()` instead.", + DeprecationWarning, + ) + sorted_keys = self._as_list() + key = sorted_keys.pop() + self.delete(key) + + def _as_list(self): + return sorted(self, key=lambda x: self[x]["index"]) + + @staticmethod + def _values(data): + if not isinstance(data, str) and data.__module__.startswith("py."): + warnings.warn( + "The 'py' module is deprecated and support " + "will be removed in a future release.", + DeprecationWarning, + ) + data = str(data) + match = re.search(r">\s*(.*?)\s*", data) + return { + "name": match.group(1).lower(), + "sortable": True if "sortable" in data else False, + "html": data, + } + + +class Row(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._append_counter = 0 + + def __setitem__(self, key, value): + warnings.warn( + "list-type assignment is deprecated and support " + "will be removed in a future release. " + "Please use `set()` instead.", + DeprecationWarning, + ) + if isinstance(key, int): + self.insert(key, value) + elif isinstance(value, dict): + super().__setitem__(key, value) + else: + self.set(key, html=value) + + def __delitem__(self, _): + warnings.warn( + "`del` is deprecated and support " + "will be removed in a future release. " + "Please use `clear()` instead.", + DeprecationWarning, + ) + self.clear() + + def sett(self, name, html=None, value=None): + if ( + html is not None + and not isinstance(html, str) + and html.__module__.startswith("py.") + ): + warnings.warn( + "The 'py' module is deprecated and support " + "will be removed in a future release.", + DeprecationWarning, + ) + html = str(html) + self[name.lower()].update({"value": value, "html": html}) + + def append(self, data): + warnings.warn( + "`append()` is deprecated and support " + "will be removed in a future release." + "Please use `set()` instead.", + DeprecationWarning, + ) + for key, value in self.items(): + if value["_append"] == self._append_counter: + self.set(key, html=data) + self._append_counter += 1 + break + + def insert(self, index, data): + warnings.warn( + "`insert()` is deprecated and support " + "will be removed in a future release." + "Please use `set()` instead.", + DeprecationWarning, + ) + for key, value in self.items(): + if value["index"] == index: + self.set(key, html=data) + + def pop(self, _): + warnings.warn( + "`pop()` is redundant and support " "will be removed in a future release.", + DeprecationWarning, + ) + pass + + @staticmethod + def from_headers(headers): + row = Row() + for key, value in headers.items(): + row[key] = {"index": value["index"], "_append": value["_append"]} + return row + + class Table: def __init__(self): self._html = {} @@ -70,17 +255,3 @@ def _extract_sortable(self, html): sortable = match.group(1) value = match.group(2) self._sortables[sortable] = value - - -class Header(Cell): - pass - - -class Row(Cell): - def __delitem__(self, key): - # This means the item should be removed - self._html = None - - def pop(self, *args): - # Calling pop on header is sufficient - pass diff --git a/testing/legacy_test_pytest_html.py b/testing/legacy_test_pytest_html.py index a3c90805..22f01d7a 100644 --- a/testing/legacy_test_pytest_html.py +++ b/testing/legacy_test_pytest_html.py @@ -107,7 +107,6 @@ def test_sleep(): def test_can_format_duration_column( self, testdir, duration_formatter, expected_report_content ): - testdir.makeconftest( f""" import pytest