Skip to content

Instantly share code, notes, and snippets.

@minrk
Created July 24, 2023 18:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save minrk/59e190ea7a582f5ea1ab632d8d3f3235 to your computer and use it in GitHub Desktop.
Save minrk/59e190ea7a582f5ea1ab632d8d3f3235 to your computer and use it in GitHub Desktop.
from dataclasses import dataclass, field
def pytest_configure(config):
# register our markers to avoid warnings
config.addinivalue_line(
"markers", "task(task): label task associated with test"
)
config.addinivalue_line(
"markers", "points(points): assign maximum point value to test results"
)
@dataclass
class Task:
name: str # the task id, e.g. '1.1'
max_points: int = 0 # from points marker
points: int = 0 # from test results
test_ids: list[str] = field(default_factory=list)
@dataclass
class TestInfo:
nodeid: str # the test id
task: Task # link back to task
max_points: int = 0 # from points marker
points: int = 0 # from user_properties
outcome: str = '' # 'passed' or 'failed'
# collect task and test info
tasks: dict[str, Task] = {}
tests: dict[str, TestInfo] = {}
def pytest_report_collectionfinish(config, start_path, startdir, items):
"""
collect info from test markers
this populates 'tasks' and 'tests'
"""
for item in items:
task_marker = item.get_closest_marker("task")
if not task_marker:
continue
task_id = task_marker.args[0]
if task_id not in tasks:
tasks[task_id] = Task(name=task_id)
task = tasks[task_id]
task.test_ids.append(item.nodeid)
point_marker = item.get_closest_marker("points")
if point_marker:
points = point_marker.args[0]
else:
points = 0
tests[item.nodeid] = TestInfo(nodeid=item.nodeid, task=task, max_points=points)
task.max_points += points
def pytest_report_teststatus(report, config):
"""A test has completed. Collect its point info and outcome"""
if report.when != 'call':
# only check the 'call' which is when the test runs
# not the 'setup' or 'teardown' reports
return
if report.nodeid not in tests:
# test doesn't have task marker
return
test = tests[report.nodeid]
task = test.task
properties = dict(report.user_properties)
points = properties.get("points", 0)
test.points += points
task.points += points
test.outcome = report.outcome
def pytest_terminal_summary(terminalreporter):
"""Report the points info we have collected"""
terminalreporter.section("point summary")
total_points = 0
total_max_points = 0
# sort by task id (parse x.x number strings)
def _sort_key(item):
task_id = item[0]
return tuple(int(part) for part in task_id.split("."))
for _, task in sorted(tasks.items(), key=_sort_key):
# report each task
terminalreporter.line(f"Task {task.name}: {task.points} / {task.max_points}")
# collect running point totals
total_points += task.points
total_max_points += task.max_points
# indented report for each test within the task
for test_id in task.test_ids:
test = tests[test_id]
line = f" {test.nodeid}: {test.outcome}"
if test.max_points:
# not all tests have points
line = f"{line} ({test.points} / {test.max_points})"
terminalreporter.line(line)
# final point total summary
terminalreporter.line("")
terminalreporter.line(f"Total points: {total_points} / {total_max_points}")
import pytest
# register marks to avoid warnings
def test_nopoints():
# a test that doesn't directly contribute to points
pass
@pytest.mark.task("1.1")
@pytest.mark.points(1)
def test_pass_11(record_property):
# check 1
record_property("points", 0.5)
# check 2
record_property("points", 1)
@pytest.mark.task("1.1")
@pytest.mark.points(1)
def test_fail_12(record_property):
assert False
record_property("points", 1)
@pytest.mark.task("1.2")
@pytest.mark.points(2)
def test_12(record_property):
record_property("points", 1)
@pytest.mark.task("1.2")
def test_task_no_points(record_property):
"""We can still have tests associated with tasks that don't map to points"""
1/0
record_property("points", 1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment