-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconftest.py
194 lines (161 loc) · 5.85 KB
/
conftest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# conftest.py
import re
from rich.console import Console
from rich.table import Table
from rich import box
from collections import defaultdict
from rich.terminal_theme import MONOKAI
from rich.align import Align
# Constants for test statuses
PASSED = "✅"
FAILED = "❌"
TIMEOUT = "⏳"
TIMEOUT_SECONDS = "[yellow]>2s[/yellow]"
NA = "[red]NA[/red]"
test_results = defaultdict(
lambda: {
"part1_status": None,
"part1_time": None,
"part2_status": None,
"part2_time": None,
}
)
def parse_year_day_part(nodeid: str):
"""
Given a nodeid like '2024/day24/test_day24.py::test_part1[param0]',
return (year='2024', day='24', part=1 or 2).
"""
# Split nodeid at '::' to separate file-part from function-part
file_part, func_part = nodeid.split("::", 1)
# print("----")
# print(file_part)
# print("##########")
# file_part might be '2024/day24/test_day24.py'
segments = file_part.split("/")
# Expect segments = ['2024', 'day24', 'test_day24.py']
year = segments[0] # e.g. '2024'
day_folder = segments[1] # e.g. 'day24'
# Extract the numeric part from 'day24'
match = re.search(r"\d+", day_folder)
day = match.group(0) if match else day_folder # '24'
# Decide whether it's test_part1 or test_part2
# (func_part could be 'test_part1[param0]' or 'test_part2')
if "test_part1" in func_part:
part = 1
elif "test_part2" in func_part:
part = 2
else:
part = 0 # Not recognized as part1/part2
return year, day, part
def pytest_runtest_logreport(report):
"""
Called after each test phase (setup, call, teardown).
We only care about the 'call' phase to see pass/fail/timeout and duration.
"""
if report.when != "call":
return
# Parse (year, day, part) from this test’s nodeid
year, day, part = parse_year_day_part(report.nodeid)
# Not a part1 or part2 test—ignore
if part not in (1, 2):
return
status, duration = determine_status_and_duration(report)
key = (year, day)
if part == 1:
test_results[key]["part1_status"] = status
test_results[key]["part1_time"] = duration
elif part == 2:
test_results[key]["part2_status"] = status
test_results[key]["part2_time"] = duration
def determine_status_and_duration(report):
"""
Determine the status and duration of a test report.
"""
if report.outcome == "passed":
return PASSED, report.duration
if report.outcome == "failed":
if "Failed: Timeout >" in str(report.longrepr):
return TIMEOUT, ">2s"
return FAILED, report.duration
return NA, None
def pytest_terminal_summary(terminalreporter):
"""
At the end of the test run, print a custom summary table using Rich.
"""
if not test_results:
return # No data collected
# We can write a "title" above the table if desired
terminalreporter.write_sep("=", "Advent of Code Summary")
# Create a console that writes to the same output file as Pytest
# so the table appears in the normal Pytest output stream.
console = Console(file=terminalreporter._tw._file, record=True)
table = create_summary_table()
# Sort results by (year, numeric day) for consistent ordering
sorted_items = sorted(test_results.items(), key=lambda x: (x[0][0], int(x[0][1])))
# Populate table rows
for (year, day), data in sorted_items:
table.add_row(
str(year),
str(day),
data["part1_status"] or NA,
data["part2_status"] or NA,
format_time(data["part1_time"]),
format_time(data["part2_time"]),
)
table = Align.center(table, vertical="middle")
# Print the table via Rich
console.print(table)
# Export to SVG
console.save_svg(f"results_{year}.svg", theme=MONOKAI)
# Optional: Still print any TIMEOUTED TESTS section, if you like:
timeouts = terminalreporter.stats.get("timeout", [])
if timeouts:
terminalreporter.write_sep("=", "TIMEOUTED TESTS", yellow=True)
for rep in timeouts:
terminalreporter.write_line(f"{rep.nodeid}")
terminalreporter.write_line("")
def create_summary_table():
"""
Create a Rich table for the summary.
"""
table = Table(title="Advent of Code Results", show_lines=True)
table.box = box.ROUNDED
table.add_column("Year", justify="center", style="bold cyan")
table.add_column("Day", justify="center", style="bold magenta")
table.add_column("Part 1", justify="center", style="bold green")
table.add_column("Part 2", justify="center", style="bold green")
table.add_column("Exec. Time Part 1", justify="center", style="yellow")
table.add_column("Exec. Time Part 2", justify="center", style="yellow")
return table
def format_time(time):
"""
Format time for display.
"""
if time not in (None, ">2s"):
time_str = f"[green]{time*1000:.0f}ms[/green]"
elif time is not None:
time_str = "[yellow]>2s[/yellow]"
else:
time_str = NA
return time_str
def pytest_report_teststatus(report, config):
"""
Hook to modify how pytest reports test results in the terminal.
If it's a timeout, we return ("timeout", "T", "TIMEOUT").
"""
if report.when == "call" and report.failed:
if "Failed: Timeout >" in str(report.longrepr):
# Use "⏳" in the progress line, and "TIMEOUT" in the verbose summary
return ("timeout", "T", ("TIMEOUT", {"yellow": True}))
return default_report_teststatus(report)
def default_report_teststatus(report):
"""
Helper function that returns the default pytest behavior for the status tuple.
"""
outcome = report.outcome
short_letter = {
"passed": ".",
"failed": "F",
"skipped": "s",
}.get(outcome, "?")
return (outcome, short_letter, outcome.upper())