3
3
import functools
4
4
import json
5
5
import pathlib
6
+ import re
6
7
import textwrap
7
8
from dataclasses import dataclass
8
9
10
+ import more_itertools
9
11
from pytest import CollectReport , TestReport
10
12
11
13
@@ -33,6 +35,14 @@ def _from_json(cls, json):
33
35
return cls (** json_ )
34
36
35
37
38
+ @dataclass
39
+ class PreformattedReport :
40
+ filepath : str
41
+ name : str
42
+ variant : str | None
43
+ message : str
44
+
45
+
36
46
def parse_record (record ):
37
47
report_types = {
38
48
"TestReport" : TestReport ,
@@ -47,27 +57,46 @@ def parse_record(record):
47
57
return cls ._from_json (record )
48
58
49
59
60
+ nodeid_re = re .compile (r"(?P<filepath>.+)::(?P<name>.+?)(?:\[(?P<variant>.+)\])?" )
61
+
62
+
63
+ def parse_nodeid (nodeid ):
64
+ match = nodeid_re .fullmatch (nodeid )
65
+ if match is None :
66
+ raise ValueError (f"unknown test id: { nodeid } " )
67
+
68
+ return match .groupdict ()
69
+
70
+
50
71
@functools .singledispatch
51
- def format_summary (report ):
52
- return f"{ report .nodeid } : { report } "
72
+ def preformat_report (report ):
73
+ parsed = parse_nodeid (report .nodeid )
74
+ return PreformattedReport (message = str (report ), ** parsed )
53
75
54
76
55
- @format_summary .register
77
+ @preformat_report .register
56
78
def _ (report : TestReport ):
79
+ parsed = parse_nodeid (report .nodeid )
57
80
message = report .longrepr .chain [0 ][1 ].message
58
- return f" { report . nodeid } : { message } "
81
+ return PreformattedReport ( message = message , ** parsed )
59
82
60
83
61
- @format_summary .register
84
+ @preformat_report .register
62
85
def _ (report : CollectReport ):
86
+ parsed = parse_nodeid (report .nodeid )
63
87
message = report .longrepr .split ("\n " )[- 1 ].removeprefix ("E" ).lstrip ()
64
- return f" { report . nodeid } : { message } "
88
+ return PreformattedReport ( message = message , ** parsed )
65
89
66
90
67
- def format_report (reports , py_version ):
68
- newline = "\n "
69
- summaries = newline .join (format_summary (r ) for r in reports )
70
- message = textwrap .dedent (
91
+ def format_summary (report ):
92
+ if report .variant is not None :
93
+ return f"{ report .filepath } ::{ report .name } [{ report .variant } ]: { report .message } "
94
+ else :
95
+ return f"{ report .filepath } ::{ report .name } : { report .message } "
96
+
97
+
98
+ def format_report (summaries , py_version ):
99
+ template = textwrap .dedent (
71
100
"""\
72
101
<details><summary>Python {py_version} Test Summary</summary>
73
102
@@ -77,10 +106,70 @@ def format_report(reports, py_version):
77
106
78
107
</details>
79
108
"""
80
- ).format (summaries = summaries , py_version = py_version )
109
+ )
110
+ # can't use f-strings because that would format *before* the dedenting
111
+ message = template .format (summaries = "\n " .join (summaries ), py_version = py_version )
81
112
return message
82
113
83
114
115
+ def merge_variants (reports , max_chars , ** formatter_kwargs ):
116
+ def format_variant_group (name , group ):
117
+ filepath , test_name , message = name
118
+
119
+ n_variants = len (group )
120
+ if n_variants != 0 :
121
+ return f"{ filepath } ::{ test_name } [{ n_variants } failing variants]: { message } "
122
+ else :
123
+ return f"{ filepath } ::{ test_name } : { message } "
124
+
125
+ bucket = more_itertools .bucket (reports , lambda r : (r .filepath , r .name , r .message ))
126
+
127
+ summaries = [format_variant_group (name , list (bucket [name ])) for name in bucket ]
128
+ formatted = format_report (summaries , ** formatter_kwargs )
129
+
130
+ return formatted
131
+
132
+
133
+ def truncate (reports , max_chars , ** formatter_kwargs ):
134
+ fractions = [0.95 , 0.75 , 0.5 , 0.25 , 0.1 , 0.01 ]
135
+
136
+ n_reports = len (reports )
137
+ for fraction in fractions :
138
+ n_selected = int (n_reports * fraction )
139
+ selected_reports = reports [: int (n_reports * fraction )]
140
+ report_messages = [format_summary (report ) for report in selected_reports ]
141
+ summary = report_messages + [f"+ { n_reports - n_selected } failing tests" ]
142
+ formatted = format_report (summary , ** formatter_kwargs )
143
+ if len (formatted ) <= max_chars :
144
+ return formatted
145
+
146
+ return None
147
+
148
+
149
+ def summarize (reports ):
150
+ return f"{ len (reports )} failing tests"
151
+
152
+
153
+ def compressed_report (reports , max_chars , ** formatter_kwargs ):
154
+ strategies = [
155
+ merge_variants ,
156
+ # merge_test_files,
157
+ # merge_tests,
158
+ truncate ,
159
+ ]
160
+ summaries = [format_summary (report ) for report in reports ]
161
+ formatted = format_report (summaries , ** formatter_kwargs )
162
+ if len (formatted ) <= max_chars :
163
+ return formatted
164
+
165
+ for strategy in strategies :
166
+ formatted = strategy (reports , max_chars = max_chars , ** formatter_kwargs )
167
+ if formatted is not None and len (formatted ) <= max_chars :
168
+ return formatted
169
+
170
+ return summarize (reports )
171
+
172
+
84
173
if __name__ == "__main__" :
85
174
parser = argparse .ArgumentParser ()
86
175
parser .add_argument ("filepath" , type = pathlib .Path )
@@ -94,8 +183,9 @@ def format_report(reports, py_version):
94
183
reports = [parse_record (json .loads (line )) for line in lines ]
95
184
96
185
failed = [report for report in reports if report .outcome == "failed" ]
186
+ preformatted = [preformat_report (report ) for report in failed ]
97
187
98
- message = format_report ( failed , py_version = py_version )
188
+ message = compressed_report ( preformatted , max_chars = 65535 , py_version = py_version )
99
189
100
190
output_file = pathlib .Path ("pytest-logs.txt" )
101
191
print (f"Writing output file to: { output_file .absolute ()} " )
0 commit comments