Skip to content

Commit 34ff60f

Browse files
authored
fix: Missing logging in report (#603)
1 parent 6013279 commit 34ff60f

File tree

5 files changed

+157
-29
lines changed

5 files changed

+157
-29
lines changed

src/pytest_html/nextgen.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,19 @@ def __init__(self, title, config):
6666
"collectedItems": 0,
6767
"runningState": "not_started",
6868
"environment": {},
69-
"tests": [],
69+
"tests": defaultdict(list),
7070
"resultsTableHeader": {},
7171
"additionalSummary": defaultdict(list),
7272
}
7373

74+
@property
75+
def title(self):
76+
return self._data["title"]
77+
78+
@title.setter
79+
def title(self, title):
80+
self._data["title"] = title
81+
7482
@property
7583
def config(self):
7684
return self._config
@@ -79,19 +87,33 @@ def config(self):
7987
def data(self):
8088
return self._data
8189

82-
def add_test(self, test):
83-
self._data["tests"].append(test)
84-
8590
def set_data(self, key, value):
8691
self._data[key] = value
8792

88-
@property
89-
def title(self):
90-
return self._data["title"]
93+
def add_test(self, test_data, report):
94+
# regardless of pass or fail we must add teardown logging to "call"
95+
if report.when == "teardown":
96+
self.update_test_log(report)
9197

92-
@title.setter
93-
def title(self, title):
94-
self._data["title"] = title
98+
# passed "setup" and "teardown" are not added to the html
99+
if report.when == "call" or _is_error(report):
100+
processed_logs = _process_logs(report)
101+
test_data["log"] = _handle_ansi(processed_logs)
102+
self._data["tests"][report.nodeid].append(test_data)
103+
return True
104+
105+
return False
106+
107+
def update_test_log(self, report):
108+
log = []
109+
for test in self._data["tests"][report.nodeid]:
110+
if test["testId"] == report.nodeid:
111+
for section in report.sections:
112+
header, content = section
113+
if "teardown" in header:
114+
log.append(f" \n{header:-^80} ")
115+
log.append(content)
116+
test["log"] += _handle_ansi("\n".join(log))
95117

96118
def __init__(self, report_path, config, default_css="style.css"):
97119
self._report_path = Path(os.path.expandvars(report_path)).expanduser()
@@ -269,7 +291,6 @@ def pytest_runtest_logreport(self, report):
269291

270292
data = {
271293
"duration": report.duration,
272-
"when": report.when,
273294
}
274295

275296
test_id = report.nodeid
@@ -291,14 +312,11 @@ def pytest_runtest_logreport(self, report):
291312
test_id += f"::{report.when}"
292313
data["testId"] = test_id
293314

294-
# Order here matters!
295-
log = report.longreprtext or report.capstdout or "No log output captured."
296-
data["log"] = _handle_ansi(log)
297315
data["result"] = _process_outcome(report)
298316
data["extras"] = self._process_extras(report, test_id)
299317

300-
self._report.add_test(data)
301-
self._generate_report()
318+
if self._report.add_test(data, report):
319+
self._generate_report()
302320

303321

304322
class NextGenReport(BaseReport):
@@ -313,8 +331,6 @@ def __init__(self, report_path, config):
313331

314332
@property
315333
def css(self):
316-
# print("woot", Path(self._assets_path.name, "style.css"))
317-
# print("waat", self._css_path.relative_to(self._report_path.parent))
318334
return Path(self._assets_path.name, "style.css")
319335

320336
def _data_content(self, content, asset_name, *args, **kwargs):
@@ -392,8 +408,25 @@ def _process_css(default_css, extra_css):
392408
return css
393409

394410

411+
def _is_error(report):
412+
return report.when in ["setup", "teardown"] and report.outcome == "failed"
413+
414+
415+
def _process_logs(report):
416+
log = []
417+
if report.longreprtext:
418+
log.append(report.longreprtext)
419+
for section in report.sections:
420+
header, content = section
421+
log.append(f" \n{header:-^80} ")
422+
log.append(content)
423+
if not log:
424+
log.append("No log output captured.")
425+
return "\n".join(log)
426+
427+
395428
def _process_outcome(report):
396-
if report.when in ["setup", "teardown"] and report.outcome == "failed":
429+
if _is_error(report):
397430
return "Error"
398431
if hasattr(report, "wasxfail"):
399432
if report.outcome in ["passed", "failed"]:

src/pytest_html/scripts/datamanager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { getCollapsedCategory } = require('./storage.js')
33
class DataManager {
44
setManager(data) {
55
const collapsedCategories = [...getCollapsedCategory(), 'passed']
6-
const dataBlob = { ...data, tests: data.tests.map((test, index) => ({
6+
const dataBlob = { ...data, tests: Object.values(data.tests).flat().map((test, index) => ({
77
...test,
88
id: `test_${index}`,
99
collapsed: collapsedCategories.includes(test.result.toLowerCase()),

src/pytest_html/scripts/dom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const dom = {
8484
formattedDuration = formatDuration < 1 ? formattedDuration.ms : formattedDuration.formatted
8585
const resultBody = templateResult.content.cloneNode(true)
8686
resultBody.querySelector('tbody').classList.add(resultLower)
87+
resultBody.querySelector('tbody').id = testId
8788
resultBody.querySelector('.col-result').innerText = result
8889
resultBody.querySelector('.col-result').classList.add(`${collapsed ? 'expander' : 'collapser'}`)
8990
resultBody.querySelector('.col-result').dataset.id = id

src/pytest_html/scripts/main.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ const renderStatic = () => {
2929
}
3030

3131
const renderContent = (tests) => {
32-
const renderSet = tests.filter(({ when, result }) => when === 'call' || result === 'Error' )
33-
const rows = renderSet.map(dom.getResultTBody)
32+
const rows = tests.map(dom.getResultTBody)
3433
const table = document.querySelector('#results-table')
3534
removeChildren(table)
3635
const tableHeader = dom.getListHeader(manager.renderData)
@@ -62,8 +61,6 @@ const renderContent = (tests) => {
6261
}
6362

6463
const renderDerived = (tests, collectedItems, isFinished) => {
65-
const renderSet = tests.filter(({ when, result }) => when === 'call' || result === 'Error')
66-
6764
const possibleResults = [
6865
{ result: 'passed', label: 'Passed' },
6966
{ result: 'skipped', label: 'Skipped' },
@@ -76,15 +73,15 @@ const renderDerived = (tests, collectedItems, isFinished) => {
7673

7774
const currentFilter = getVisible()
7875
possibleResults.forEach(({ result, label }) => {
79-
const count = renderSet.filter((test) => test.result.toLowerCase() === result).length
76+
const count = tests.filter((test) => test.result.toLowerCase() === result).length
8077
const input = document.querySelector(`input[data-test-result="${result}"]`)
8178
document.querySelector(`.${result}`).innerText = `${count} ${label}`
8279

8380
input.disabled = !count
8481
input.checked = currentFilter.includes(result)
8582
})
8683

87-
const numberOfTests = renderSet.filter(({ result }) =>
84+
const numberOfTests = tests.filter(({ result }) =>
8885
['Passed', 'Failed', 'XPassed', 'XFailed'].includes(result)).length
8986

9087
if (isFinished) {

testing/test_integration.py

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def run(pytester, path="report.html", *args):
3131
pytester.runpytest("-s", "--html", path, *args)
3232

3333
chrome_options = webdriver.ChromeOptions()
34-
chrome_options.add_argument("--headless")
34+
if os.environ.get("CI", False):
35+
chrome_options.add_argument("--headless")
3536
chrome_options.add_argument("--window-size=1920x1080")
3637
driver = webdriver.Remote(
3738
command_executor="http://127.0.0.1:4444", options=chrome_options
@@ -90,9 +91,12 @@ def get_text(page, selector):
9091
return get_element(page, selector).string
9192

9293

93-
def get_log(page):
94+
def get_log(page, test_id=None):
9495
# TODO(jim) move to get_text (use .contents)
95-
log = get_element(page, ".summary div[class='log']")
96+
if test_id:
97+
log = get_element(page, f".summary tbody[id$='{test_id}'] div[class='log']")
98+
else:
99+
log = get_element(page, ".summary div[class='log']")
96100
all_text = ""
97101
for text in log.strings:
98102
all_text += text
@@ -527,3 +531,96 @@ def test_pass(): pass
527531
)
528532
page = run(pytester)
529533
assert_results(page, passed=1)
534+
535+
536+
class TestLogCapturing:
537+
LOG_LINE_REGEX = r"\s+this is {}"
538+
539+
@pytest.fixture
540+
def log_cli(self, pytester):
541+
pytester.makeini(
542+
"""
543+
[pytest]
544+
log_cli = 1
545+
log_cli_level = INFO
546+
log_cli_date_format = %Y-%m-%d %H:%M:%S
547+
log_cli_format = %(asctime)s %(levelname)s: %(message)s
548+
"""
549+
)
550+
551+
@pytest.fixture
552+
def test_file(self):
553+
return """
554+
import pytest
555+
import logging
556+
@pytest.fixture
557+
def setup():
558+
logging.info("this is setup")
559+
{setup}
560+
yield
561+
logging.info("this is teardown")
562+
{teardown}
563+
564+
def test_logging(setup):
565+
logging.info("this is test")
566+
assert {assertion}
567+
"""
568+
569+
@pytest.mark.usefixtures("log_cli")
570+
def test_all_pass(self, test_file, pytester):
571+
pytester.makepyfile(test_file.format(setup="", teardown="", assertion=True))
572+
page = run(pytester)
573+
assert_results(page, passed=1)
574+
575+
log = get_log(page)
576+
for when in ["setup", "test", "teardown"]:
577+
assert_that(log).matches(self.LOG_LINE_REGEX.format(when))
578+
579+
@pytest.mark.usefixtures("log_cli")
580+
def test_setup_error(self, test_file, pytester):
581+
pytester.makepyfile(
582+
test_file.format(setup="error", teardown="", assertion=True)
583+
)
584+
page = run(pytester)
585+
assert_results(page, error=1)
586+
587+
log = get_log(page)
588+
assert_that(log).matches(self.LOG_LINE_REGEX.format("setup"))
589+
assert_that(log).does_not_match(self.LOG_LINE_REGEX.format("test"))
590+
assert_that(log).does_not_match(self.LOG_LINE_REGEX.format("teardown"))
591+
592+
@pytest.mark.usefixtures("log_cli")
593+
def test_test_fails(self, test_file, pytester):
594+
pytester.makepyfile(test_file.format(setup="", teardown="", assertion=False))
595+
page = run(pytester)
596+
assert_results(page, failed=1)
597+
598+
log = get_log(page)
599+
for when in ["setup", "test", "teardown"]:
600+
assert_that(log).matches(self.LOG_LINE_REGEX.format(when))
601+
602+
@pytest.mark.usefixtures("log_cli")
603+
@pytest.mark.parametrize(
604+
"assertion, result", [(True, {"passed": 1}), (False, {"failed": 1})]
605+
)
606+
def test_teardown_error(self, test_file, pytester, assertion, result):
607+
pytester.makepyfile(
608+
test_file.format(setup="", teardown="error", assertion=assertion)
609+
)
610+
page = run(pytester)
611+
assert_results(page, error=1, **result)
612+
613+
for test_name in ["test_logging", "test_logging::teardown"]:
614+
log = get_log(page, test_name)
615+
for when in ["setup", "test", "teardown"]:
616+
assert_that(log).matches(self.LOG_LINE_REGEX.format(when))
617+
618+
def test_no_log(self, test_file, pytester):
619+
pytester.makepyfile(test_file.format(setup="", teardown="", assertion=True))
620+
page = run(pytester)
621+
assert_results(page, passed=1)
622+
623+
log = get_log(page, "test_logging")
624+
assert_that(log).contains("No log output captured.")
625+
for when in ["setup", "test", "teardown"]:
626+
assert_that(log).does_not_match(self.LOG_LINE_REGEX.format(when))

0 commit comments

Comments
 (0)