Created
July 24, 2023 18:12
-
-
Save minrk/59e190ea7a582f5ea1ab632d8d3f3235 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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