12
12
from collections import defaultdict
13
13
from functools import lru_cache , reduce
14
14
from itertools import groupby
15
+ from os import sep
15
16
from pathlib import Path
16
17
17
18
from hypothesis ._settings import Phase , Verbosity
@@ -45,6 +46,20 @@ def trace(self, frame, event, arg):
45
46
self ._previous_location = current_location
46
47
47
48
49
+ UNHELPFUL_LOCATIONS = (
50
+ # There's a branch which is only taken when an exception is active while exiting
51
+ # a contextmanager; this is probably after the fault has been triggered.
52
+ # Similar reasoning applies to a few other standard-library modules: even
53
+ # if the fault was later, these still aren't useful locations to report!
54
+ f"{ sep } contextlib.py" ,
55
+ f"{ sep } inspect.py" ,
56
+ f"{ sep } re.py" ,
57
+ f"{ sep } re{ sep } __init__.py" , # refactored in Python 3.11
58
+ # Quite rarely, the first AFNP line is in Pytest's assertion-rewriting module.
59
+ f"{ sep } _pytest{ sep } assertion{ sep } rewrite.py" ,
60
+ )
61
+
62
+
48
63
def get_explaining_locations (traces ):
49
64
# Traces is a dict[interesting_origin | None, set[frozenset[tuple[str, int]]]]
50
65
# Each trace in the set might later become a Counter instead of frozenset.
@@ -84,21 +99,25 @@ def get_explaining_locations(traces):
84
99
else :
85
100
queue .update (cf_graphs [origin ][src ] - seen )
86
101
87
- return explanations
102
+ # The last step is to filter out explanations that we know would be uninformative.
103
+ # When this is the first AFNP location, we conclude that Scrutineer missed the
104
+ # real divergence (earlier in the trace) and drop that unhelpful explanation.
105
+ return {
106
+ origin : {loc for loc in afnp_locs if not loc [0 ].endswith (UNHELPFUL_LOCATIONS )}
107
+ for origin , afnp_locs in explanations .items ()
108
+ }
88
109
89
110
90
111
LIB_DIR = str (Path (sys .executable ).parent / "lib" )
91
112
EXPLANATION_STUB = (
92
113
"Explanation:" ,
93
114
" These lines were always and only run by failing examples:" ,
94
115
)
95
- HAD_TRACE = " We didn't try to explain this, because sys.gettrace()="
96
116
97
117
98
118
def make_report (explanations , cap_lines_at = 5 ):
99
119
report = defaultdict (list )
100
120
for origin , locations in explanations .items ():
101
- assert locations # or else we wouldn't have stored the key, above.
102
121
report_lines = [
103
122
" {}:{}" .format (k , ", " .join (map (str , sorted (l for _ , l in v ))))
104
123
for k , v in groupby (locations , lambda kv : kv [0 ])
@@ -107,15 +126,14 @@ def make_report(explanations, cap_lines_at=5):
107
126
if len (report_lines ) > cap_lines_at + 1 :
108
127
msg = " (and {} more with settings.verbosity >= verbose)"
109
128
report_lines [cap_lines_at :] = [msg .format (len (report_lines [cap_lines_at :]))]
110
- report [origin ] = list (EXPLANATION_STUB ) + report_lines
129
+ if report_lines : # We might have filtered out every location as uninformative.
130
+ report [origin ] = list (EXPLANATION_STUB ) + report_lines
111
131
return report
112
132
113
133
114
134
def explanatory_lines (traces , settings ):
115
135
if Phase .explain in settings .phases and sys .gettrace () and not traces :
116
- return defaultdict (
117
- lambda : [EXPLANATION_STUB [0 ], HAD_TRACE + repr (sys .gettrace ())]
118
- )
136
+ return defaultdict (list )
119
137
# Return human-readable report lines summarising the traces
120
138
explanations = get_explaining_locations (traces )
121
139
max_lines = 5 if settings .verbosity <= Verbosity .normal else 100
0 commit comments