Skip to content

Chore: Decouple ReportData #650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 4 additions & 111 deletions src/pytest_html/basereport.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import os
import re
import warnings
from collections import defaultdict
from functools import partial
from pathlib import Path

import pytest
Expand All @@ -17,99 +15,12 @@
from pytest_html.table import Header
from pytest_html.table import Html
from pytest_html.table import Row
from pytest_html.util import _ansi_styles
from pytest_html.util import cleanup_unserializable

try:
from ansi2html import Ansi2HTMLConverter, style

converter = Ansi2HTMLConverter(inline=False, escaped=False)
_handle_ansi = partial(converter.convert, full=False)
_ansi_styles = style.get_styles()
except ImportError:
from _pytest.logging import _remove_ansi_escape_sequences

_handle_ansi = _remove_ansi_escape_sequences
_ansi_styles = []


class BaseReport:
class ReportData:
def __init__(self, title, config):
self._config = config
self._data = {
"title": title,
"collectedItems": 0,
"runningState": "not_started",
"environment": {},
"tests": defaultdict(list),
"resultsTableHeader": {},
"additionalSummary": defaultdict(list),
}

collapsed = config.getini("render_collapsed")
if collapsed:
if collapsed.lower() == "true":
warnings.warn(
"'render_collapsed = True' is deprecated and support "
"will be removed in the next major release. "
"Please use 'render_collapsed = all' instead.",
DeprecationWarning,
)
self.set_data(
"collapsed", [outcome.lower() for outcome in collapsed.split(",")]
)

@property
def title(self):
return self._data["title"]

@title.setter
def title(self, title):
self._data["title"] = title

@property
def config(self):
return self._config

@property
def data(self):
return self._data

def set_data(self, key, value):
self._data[key] = value

def add_test(self, test_data, report, row, remove_log=False):
for sortable, value in row.sortables.items():
test_data[sortable] = value

# regardless of pass or fail we must add teardown logging to "call"
if report.when == "teardown" and not remove_log:
self.update_test_log(report)

# passed "setup" and "teardown" are not added to the html
if report.when == "call" or (
report.when in ["setup", "teardown"] and report.outcome != "passed"
):
if not remove_log:
processed_logs = _process_logs(report)
test_data["log"] = _handle_ansi(processed_logs)
self._data["tests"][report.nodeid].append(test_data)
return True

return False

def update_test_log(self, report):
log = []
for test in self._data["tests"][report.nodeid]:
if test["testId"] == report.nodeid and "log" in test:
for section in report.sections:
header, content = section
if "teardown" in header:
log.append(f"{' ' + header + ' ':-^80}")
log.append(content)
test["log"] += _handle_ansi("\n".join(log))

def __init__(self, report_path, config, default_css="style.css"):
def __init__(self, report_path, config, report_data, default_css="style.css"):
self._report_path = Path(os.path.expandvars(report_path)).expanduser()
self._report_path.parent.mkdir(parents=True, exist_ok=True)
self._resources_path = Path(__file__).parent.joinpath("resources")
Expand All @@ -122,7 +33,8 @@ def __init__(self, report_path, config, default_css="style.css"):
config.getini("max_asset_filename_length")
)

self._report = self.ReportData(self._report_path.name, config)
self._report = report_data
self._report.title = self._report_path.name

@property
def css(self):
Expand Down Expand Up @@ -336,25 +248,6 @@ def _is_error(report):
return report.when in ["setup", "teardown"] and report.outcome == "failed"


def _process_logs(report):
log = []
if report.longreprtext:
log.append(report.longreprtext.replace("<", "&lt;").replace(">", "&gt;") + "\n")
for section in report.sections:
header, content = section
log.append(f"{' ' + header + ' ':-^80}")
log.append(content)

# weird formatting related to logs
if "log" in header:
log.append("")
if "call" in header:
log.append("")
if not log:
log.append("No log output captured.")
return "\n".join(log)


def _process_outcome(report):
if _is_error(report):
return "Error"
Expand Down
6 changes: 4 additions & 2 deletions src/pytest_html/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from pytest_html.basereport import BaseReport as HTMLReport # noqa: F401
from pytest_html.report import Report
from pytest_html.report_data import ReportData
from pytest_html.selfcontained_report import SelfContainedReport


Expand Down Expand Up @@ -80,10 +81,11 @@ def pytest_configure(config):

if not hasattr(config, "workerinput"):
# prevent opening html_path on worker nodes (xdist)
report_data = ReportData(config)
if config.getoption("self_contained_html"):
html = SelfContainedReport(html_path, config)
html = SelfContainedReport(html_path, config, report_data)
else:
html = Report(html_path, config)
html = Report(html_path, config, report_data)

config.pluginmanager.register(html)

Expand Down
4 changes: 2 additions & 2 deletions src/pytest_html/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@


class Report(BaseReport):
def __init__(self, report_path, config):
super().__init__(report_path, config)
def __init__(self, report_path, config, report_data):
super().__init__(report_path, config, report_data)
self._assets_path = Path(self._report_path.parent, "assets")
self._assets_path.mkdir(parents=True, exist_ok=True)
self._css_path = Path(self._assets_path, "style.css")
Expand Down
100 changes: 100 additions & 0 deletions src/pytest_html/report_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import warnings
from collections import defaultdict

from pytest_html.util import _handle_ansi


class ReportData:
def __init__(self, config):
self._config = config
self._data = {
"title": "",
"collectedItems": 0,
"runningState": "not_started",
"environment": {},
"tests": defaultdict(list),
"resultsTableHeader": {},
"additionalSummary": defaultdict(list),
}

collapsed = config.getini("render_collapsed")
if collapsed:
if collapsed.lower() == "true":
warnings.warn(
"'render_collapsed = True' is deprecated and support "
"will be removed in the next major release. "
"Please use 'render_collapsed = all' instead.",
DeprecationWarning,
)
self.set_data(
"collapsed", [outcome.lower() for outcome in collapsed.split(",")]
)

@property
def title(self):
return self._data["title"]

@title.setter
def title(self, title):
self._data["title"] = title

@property
def config(self):
return self._config

@property
def data(self):
return self._data

def set_data(self, key, value):
self._data[key] = value

def add_test(self, test_data, report, row, remove_log=False):
for sortable, value in row.sortables.items():
test_data[sortable] = value

# regardless of pass or fail we must add teardown logging to "call"
if report.when == "teardown" and not remove_log:
self.update_test_log(report)

# passed "setup" and "teardown" are not added to the html
if report.when == "call" or (
report.when in ["setup", "teardown"] and report.outcome != "passed"
):
if not remove_log:
processed_logs = _process_logs(report)
test_data["log"] = _handle_ansi(processed_logs)
self._data["tests"][report.nodeid].append(test_data)
return True

return False

def update_test_log(self, report):
log = []
for test in self._data["tests"][report.nodeid]:
if test["testId"] == report.nodeid and "log" in test:
for section in report.sections:
header, content = section
if "teardown" in header:
log.append(f"{' ' + header + ' ':-^80}")
log.append(content)
test["log"] += _handle_ansi("\n".join(log))


def _process_logs(report):
log = []
if report.longreprtext:
log.append(report.longreprtext.replace("<", "&lt;").replace(">", "&gt;") + "\n")
for section in report.sections:
header, content = section
log.append(f"{' ' + header + ' ':-^80}")
log.append(content)

# weird formatting related to logs
if "log" in header:
log.append("")
if "call" in header:
log.append("")
if not log:
log.append("No log output captured.")
return "\n".join(log)
4 changes: 2 additions & 2 deletions src/pytest_html/selfcontained_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@


class SelfContainedReport(BaseReport):
def __init__(self, report_path, config):
super().__init__(report_path, config)
def __init__(self, report_path, config, report_data):
super().__init__(report_path, config, report_data)

@property
def css(self):
Expand Down
22 changes: 12 additions & 10 deletions src/pytest_html/util.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import importlib
import json
from functools import lru_cache
from functools import partial
from typing import Any
from typing import Dict


@lru_cache()
def ansi_support():
try:
# from ansi2html import Ansi2HTMLConverter, style # NOQA
return importlib.import_module("ansi2html")
except ImportError:
# ansi2html is not installed
pass
try:
from ansi2html import Ansi2HTMLConverter, style

converter = Ansi2HTMLConverter(inline=False, escaped=False)
_handle_ansi = partial(converter.convert, full=False)
_ansi_styles = style.get_styles()
except ImportError:
from _pytest.logging import _remove_ansi_escape_sequences

_handle_ansi = _remove_ansi_escape_sequences
_ansi_styles = []


def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]:
Expand Down