Skip to content

Commit

Permalink
Chore: Simplify results table hooks (pytest-dev#688)
Browse files Browse the repository at this point in the history
  • Loading branch information
BeyondEvil authored Jul 22, 2023
1 parent 316246e commit f6f623d
Show file tree
Hide file tree
Showing 13 changed files with 86 additions and 268 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
72 changes: 50 additions & 22 deletions src/pytest_html/basereport.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import datetime
import json
import math
import os
import re
import warnings
from pathlib import Path

import pytest
from pytest_metadata.plugin import metadata_key

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


Expand Down Expand Up @@ -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]
for key in metadata.keys():
value = metadata[key]
if self._is_redactable_environment_variable(key):
Expand Down Expand Up @@ -145,16 +145,12 @@ 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())
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())
headers = self._report.data["resultsTableHeader"]
session.config.hook.pytest_html_results_table_header(cells=headers)

self._report.set_data("runningState", "Started")
self._generate_report()
Expand All @@ -173,7 +169,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)
Expand All @@ -189,34 +186,60 @@ def pytest_runtest_logreport(self, report):
)

data = {
"duration": report.duration,
"result": _process_outcome(report),
"duration": _format_duration(report.duration),
}

total_duration = self._report.data["totalDuration"]
total_duration["total"] += report.duration
total_duration["formatted"] = _format_duration(total_duration["total"])

test_id = report.nodeid
if report.when != "call":
test_id += f"::{report.when}"
data["testId"] = test_id

row_cells = Row()
self._config.hook.pytest_html_results_table_row(report=report, cells=row_cells)
if row_cells.html is None:
data["extras"] = self._process_extras(report, test_id)
links = [
extra
for extra in data["extras"]
if extra["format_type"] in ["json", "text", "url"]
]
cells = [
f'<td class="col-result">{data["result"]}</td>',
f'<td class="col-name">{data["testId"]}</td>',
f'<td class="col-duration">{data["duration"]}</td>',
f'<td class="col-links">{_process_links(links)}</td>',
]

self._config.hook.pytest_html_results_table_row(report=report, cells=cells)
if not cells:
return
data["resultsTableRow"] = row_cells.html
for sortable, value in row_cells.sortables.items():
data[sortable] = value

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()


def _format_duration(duration):
if duration < 1:
return "{} ms".format(round(duration * 1000))

hours = math.floor(duration / 3600)
remaining_seconds = duration % 3600
minutes = math.floor(remaining_seconds / 60)
remaining_seconds = remaining_seconds % 60
seconds = round(remaining_seconds)

return f"{hours:02d}:{minutes:02d}:{seconds:02d}"


def _is_error(report):
return report.when in ["setup", "teardown"] and report.outcome == "failed"

Expand Down Expand Up @@ -249,3 +272,8 @@ def _process_outcome(report):
return "XFailed"

return report.outcome.capitalize()


def _process_links(links):
a_tag = '<a target="_blank" href="{content}" class="col-links__extra {format_type}">{name}</a>'
return "".join([a_tag.format_map(link) for link in links])
14 changes: 13 additions & 1 deletion src/pytest_html/report_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,26 @@
class ReportData:
def __init__(self, config):
self._config = config

default_headers = [
'<th class="sortable" data-column-type="result">Result</th>',
'<th class="sortable" data-column-type="testId">Test</th>',
'<th class="sortable" data-column-type="duration">Duration</th>',
"<th>Links</th>",
]

self._data = {
"title": "",
"collectedItems": 0,
"totalDuration": {
"total": 0,
"formatted": "",
},
"runningState": "not_started",
"environment": {},
"tests": defaultdict(list),
"resultsTableHeader": {},
"additionalSummary": defaultdict(list),
"resultsTableHeader": default_headers,
}

collapsed = config.getini("render_collapsed")
Expand Down
11 changes: 0 additions & 11 deletions src/pytest_html/resources/index.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,9 @@
<td></td>
</tr>
</template>
<template id="template_a">
<a target="_blank"></a>
</template>
<template id="template_results-table__tbody">
<tbody class="results-table-row">
<tr class="collapsible">
<td class="col-result"></td>
<td class="col-name"></td>
<td class="col-duration"></td>
<td class="col-links"></td>
</tr>
<tr class="extras-row">
<td class="extra" colspan="4">
Expand All @@ -62,10 +55,6 @@
<template id="template_results-table__head">
<thead id="results-table-head">
<tr>
<th class="sortable" data-column-type="result">Result</th>
<th class="sortable" data-column-type="testId">Test</th>
<th class="sortable" data-column-type="duration">Duration</th>
<th>Links</th>
</tr>
</thead>
</template>
Expand Down
4 changes: 2 additions & 2 deletions src/pytest_html/resources/style.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/pytest_html/scripts/datamanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class DataManager {
get isFinished() {
return this.data.runningState === 'Finished'
}
get formattedDuration() {
return this.data.totalDuration.formatted
}
}

module.exports = {
Expand Down
61 changes: 12 additions & 49 deletions src/pytest_html/scripts/dom.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
const storageModule = require('./storage.js')
const { formatDuration, transformTableObj } = require('./utils.js')
const mediaViewer = require('./mediaviewer.js')
const templateEnvRow = document.querySelector('#template_environment_row')
const templateCollGroup = document.querySelector('#template_table-colgroup')
const templateResult = document.querySelector('#template_results-table__tbody')
const aTag = document.querySelector('#template_a')
const listHeader = document.querySelector('#template_results-table__head')
const listHeaderEmpty = document.querySelector('#template_results-table__head--empty')

Expand All @@ -28,12 +26,6 @@ const findAll = (selector, elem) => {
return [...elem.querySelectorAll(selector)]
}

const insertAdditionalHTML = (html, element, selector, position = 'beforebegin') => {
Object.keys(html).map((key) => {
element.querySelectorAll(selector).item(key).insertAdjacentHTML(position, html[key])
})
}

const dom = {
getStaticRow: (key, value) => {
const envRow = templateEnvRow.content.cloneNode(true)
Expand All @@ -53,29 +45,14 @@ const dom = {
const sortAttr = storageModule.getSort()
const sortAsc = JSON.parse(storageModule.getSortDirection())

const regex = /data-column-type="(\w+)/
const cols = Object.values(resultsTableHeader).reduce((result, value) => {
if (value.includes('sortable')) {
const matches = regex.exec(value)
if (matches) {
result.push(matches[1])
}
}
return result
}, [])
const sortables = ['result', 'testId', 'duration', ...cols]

// Add custom html from the pytest_html_results_table_header hook
const headers = transformTableObj(resultsTableHeader)
insertAdditionalHTML(headers.inserts, header, 'th')
insertAdditionalHTML(headers.appends, header, 'tr', 'beforeend')

sortables.forEach((sortCol) => {
if (sortCol === sortAttr) {
header.querySelector(`[data-column-type="${sortCol}"]`).classList.add(sortAsc ? 'desc' : 'asc')
}
resultsTableHeader.forEach((html) => {
const t = document.createElement('template')
t.innerHTML = html
header.querySelector('#results-table-head > tr').appendChild(t.content)
})

header.querySelector(`.sortable[data-column-type="${sortAttr}"]`).classList.add(sortAsc ? 'desc' : 'asc')

return header
},
getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true),
Expand All @@ -86,12 +63,13 @@ const dom = {
resultBody.querySelector('tbody').classList.add(resultLower)
resultBody.querySelector('tbody').id = testId
resultBody.querySelector('.collapsible').dataset.id = id
resultBody.querySelector('.col-result').innerText = result
resultBody.querySelector('.col-result').classList.add(`${collapsed ? 'expander' : 'collapser'}`)
resultBody.querySelector('.col-name').innerText = testId

const formattedDuration = duration < 1 ? formatDuration(duration).ms : formatDuration(duration).formatted
resultBody.querySelector('.col-duration').innerText = formattedDuration
resultsTableRow.forEach((html) => {
const t = document.createElement('template')
t.innerHTML = html
resultBody.querySelector('.collapsible').appendChild(t.content)
})
resultBody.querySelector('.collapsible > td')?.classList.add(`${collapsed ? 'expander' : 'collapser'}`)

if (log) {
// Wrap lines starting with "E" with span.error to color those lines red
Expand All @@ -107,16 +85,6 @@ const dom = {

const media = []
extras?.forEach(({ name, format_type, content }) => {
if (['json', 'text', 'url'].includes(format_type)) {
const extraLink = aTag.content.cloneNode(true)
const extraLinkItem = extraLink.querySelector('a')

extraLinkItem.href = content
extraLinkItem.className = `col-links__extra ${format_type}`
extraLinkItem.innerText = name
resultBody.querySelector('.col-links').appendChild(extraLinkItem)
}

if (['image', 'video'].includes(format_type)) {
media.push({ path: content, name, format_type })
}
Expand All @@ -127,11 +95,6 @@ const dom = {
})
mediaViewer.setUp(resultBody, media)

// Add custom html from the pytest_html_results_table_row hook
const rows = transformTableObj(resultsTableRow)
resultsTableRow && insertAdditionalHTML(rows.inserts, resultBody, 'td')
resultsTableRow && insertAdditionalHTML(rows.appends, resultBody, 'tr', 'beforeend')

// Add custom html from the pytest_html_results_table_html hook
tableHtml?.forEach((item) => {
resultBody.querySelector('td[class="extra"]').insertAdjacentHTML('beforeend', item)
Expand Down
Loading

0 comments on commit f6f623d

Please sign in to comment.