diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py index 2f169110..52180428 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -9,28 +9,21 @@ from pathlib import Path import pytest -from jinja2 import Environment -from jinja2 import FileSystemLoader -from jinja2 import select_autoescape 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 _ansi_styles from pytest_html.util import cleanup_unserializable class BaseReport: - def __init__(self, report_path, config, report_data, default_css="style.css"): + def __init__(self, report_path, config, report_data, template, 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") self._config = config - self._template = _read_template([self._resources_path]) - self._css = _process_css( - Path(self._resources_path, default_css), self._config.getoption("css") - ) + self._template = template + self._css = css self._max_asset_filename_length = int( config.getini("max_asset_filename_length") ) @@ -224,32 +217,6 @@ def pytest_runtest_logreport(self, report): self._generate_report() -def _process_css(default_css, extra_css): - with open(default_css, encoding="utf-8") as f: - css = f.read() - - # Add user-provided CSS - for path in extra_css: - css += "\n/******************************" - css += "\n * CUSTOM CSS" - css += f"\n * {path}" - css += "\n ******************************/\n\n" - with open(path, encoding="utf-8") as f: - css += f.read() - - # ANSI support - if _ansi_styles: - ansi_css = [ - "\n/******************************", - " * ANSI2HTML STYLES", - " ******************************/\n", - ] - ansi_css.extend([str(r) for r in _ansi_styles]) - css += "\n".join(ansi_css) - - return css - - def _is_error(report): return report.when in ["setup", "teardown"] and report.outcome == "failed" @@ -282,13 +249,3 @@ def _process_outcome(report): return "XFailed" return report.outcome.capitalize() - - -def _read_template(search_paths, template_name="index.jinja2"): - env = Environment( - loader=FileSystemLoader(search_paths), - autoescape=select_autoescape( - enabled_extensions=("jinja2",), - ), - ) - return env.get_template(template_name) diff --git a/src/pytest_html/plugin.py b/src/pytest_html/plugin.py index 6683c54a..3bca2fd6 100644 --- a/src/pytest_html/plugin.py +++ b/src/pytest_html/plugin.py @@ -1,6 +1,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +import os import warnings from pathlib import Path @@ -11,6 +12,8 @@ from pytest_html.report import Report from pytest_html.report_data import ReportData from pytest_html.selfcontained_report import SelfContainedReport +from pytest_html.util import _process_css +from pytest_html.util import _read_template def pytest_addhooks(pluginmanager): @@ -68,10 +71,14 @@ def pytest_addoption(parser): def pytest_configure(config): html_path = config.getoption("htmlpath") if html_path: + extra_css = [ + Path(os.path.expandvars(css)).expanduser() + for css in config.getoption("css") + ] missing_css_files = [] - for css_path in config.getoption("css"): - if not Path(css_path).exists(): - missing_css_files.append(css_path) + for css_path in extra_css: + if not css_path.exists(): + missing_css_files.append(str(css_path)) if missing_css_files: os_error = ( @@ -82,11 +89,17 @@ def pytest_configure(config): if not hasattr(config, "workerinput"): # prevent opening html_path on worker nodes (xdist) + resources_path = Path(__file__).parent.joinpath("resources") + default_css = Path(resources_path, "style.css") + template = _read_template([resources_path]) + processed_css = _process_css(default_css, extra_css) report_data = ReportData(config) if config.getoption("self_contained_html"): - html = SelfContainedReport(html_path, config, report_data) + html = SelfContainedReport( + html_path, config, report_data, template, processed_css + ) else: - html = Report(html_path, config, report_data) + html = Report(html_path, config, report_data, template, processed_css) config.pluginmanager.register(html) diff --git a/src/pytest_html/report.py b/src/pytest_html/report.py index 0ef36f6c..22ef7e14 100644 --- a/src/pytest_html/report.py +++ b/src/pytest_html/report.py @@ -10,8 +10,8 @@ class Report(BaseReport): - def __init__(self, report_path, config, report_data): - super().__init__(report_path, config, report_data) + def __init__(self, report_path, config, report_data, template, css): + super().__init__(report_path, config, report_data, template, css) 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") diff --git a/src/pytest_html/selfcontained_report.py b/src/pytest_html/selfcontained_report.py index dc48d59f..ecc44600 100644 --- a/src/pytest_html/selfcontained_report.py +++ b/src/pytest_html/selfcontained_report.py @@ -9,8 +9,8 @@ class SelfContainedReport(BaseReport): - def __init__(self, report_path, config, report_data): - super().__init__(report_path, config, report_data) + def __init__(self, report_path, config, report_data, template, css): + super().__init__(report_path, config, report_data, template, css) @property def css(self): diff --git a/src/pytest_html/util.py b/src/pytest_html/util.py index a40bdd30..a9b11d53 100644 --- a/src/pytest_html/util.py +++ b/src/pytest_html/util.py @@ -6,6 +6,9 @@ from typing import Any from typing import Dict +from jinja2 import Environment +from jinja2 import FileSystemLoader +from jinja2 import select_autoescape try: from ansi2html import Ansi2HTMLConverter, style @@ -30,3 +33,39 @@ def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]: v = str(v) result[k] = v return result + + +def _read_template(search_paths, template_name="index.jinja2"): + env = Environment( + loader=FileSystemLoader(search_paths), + autoescape=select_autoescape( + enabled_extensions=("jinja2",), + ), + ) + return env.get_template(template_name) + + +def _process_css(default_css, extra_css): + with open(default_css, encoding="utf-8") as f: + css = f.read() + + # Add user-provided CSS + for path in extra_css: + css += "\n/******************************" + css += "\n * CUSTOM CSS" + css += f"\n * {path}" + css += "\n ******************************/\n\n" + with open(path, encoding="utf-8") as f: + css += f.read() + + # ANSI support + if _ansi_styles: + ansi_css = [ + "\n/******************************", + " * ANSI2HTML STYLES", + " ******************************/\n", + ] + ansi_css.extend([str(r) for r in _ansi_styles]) + css += "\n".join(ansi_css) + + return css diff --git a/testing/test_unit.py b/testing/test_unit.py index f60132e4..be8ef387 100644 --- a/testing/test_unit.py +++ b/testing/test_unit.py @@ -1,3 +1,11 @@ +import importlib.resources +import os +import sys + +import pkg_resources +import pytest +from assertpy import assert_that + pytest_plugins = ("pytester",) @@ -7,6 +15,22 @@ def run(pytester, path="report.html", cmd_flags=None): return pytester.runpytest("--html", path, *cmd_flags) +def file_content(): + try: + return ( + importlib.resources.files("pytest_html") + .joinpath("assets", "style.css") + .read_bytes() + .decode("utf-8") + .strip() + ) + except AttributeError: + # Needed for python < 3.9 + return pkg_resources.resource_string( + "pytest_html", os.path.join("assets", "style.css") + ).decode("utf-8") + + def test_duration_format_deprecation_warning(pytester): pytester.makeconftest( """ @@ -44,3 +68,67 @@ def pytest_html_results_summary(prefix, summary, postfix, session): pytester.makepyfile("def test_pass(): pass") result = run(pytester) result.assert_outcomes(passed=1) + + +@pytest.fixture +def css_file_path(pytester): + css_one = """ + h1 { + color: red; + } + """ + css_two = """ + h2 { + color: blue; + } + """ + css_dir = pytester.path / "extra_css" + css_dir.mkdir() + file_path = css_dir / "one.css" + with open(file_path, "w") as f: + f.write(css_one) + + pytester.makefile(".css", two=css_two) + pytester.makepyfile("def test_pass(): pass") + + return file_path + + +@pytest.fixture(params=[True, False]) +def expandvar(request, css_file_path, monkeypatch): + if request.param: + monkeypatch.setenv("EXTRA_CSS", str(css_file_path)) + return "%EXTRA_CSS%" if sys.platform == "win32" else "${EXTRA_CSS}" + return css_file_path + + +def test_custom_css(pytester, css_file_path, expandvar): + result = run( + pytester, "report.html", cmd_flags=["--css", expandvar, "--css", "two.css"] + ) + result.assert_outcomes(passed=1) + + path = pytester.path.joinpath("assets", "style.css") + + with open(str(path)) as f: + css = f.read() + assert_that(css).contains("* " + str(css_file_path)).contains("* two.css") + + +def test_custom_css_selfcontained(pytester, css_file_path, expandvar): + result = run( + pytester, + "report.html", + cmd_flags=[ + "--css", + expandvar, + "--css", + "two.css", + "--self-contained-html", + ], + ) + result.assert_outcomes(passed=1) + + with open(pytester.path / "report.html") as f: + html = f.read() + assert_that(html).contains("* " + str(css_file_path)).contains("* two.css")