1414
1515REPORT_SECTION_TITLE = "pytest-httpserver report"
1616
17- SERVERS_STASH_KEY = pytest .StashKey [list [HTTPServerBase ]]()
17+
18+ class _ServerRegistry :
19+ """
20+ Registry of the servers used by a test.
21+
22+ It keeps track of the server findings (assertions and handler errors):
23+
24+ * which findings were already attached to a test report, so a finding is
25+ never added to more than one report, even if the test fails in multiple
26+ phases (setup, call and teardown),
27+
28+ * a snapshot of the findings taken when the fixtures of the test are torn
29+ down (see :py:meth:`snapshot`), so the findings collected during the
30+ teardown phase can be attached to the teardown report even if the
31+ server's state is cleared by another (for example session scoped)
32+ fixture finalizer before the report is made.
33+ """
34+
35+ def __init__ (self ) -> None :
36+ self ._servers : list [HTTPServerBase ] = []
37+ self ._reported_findings : list [object ] = []
38+ self ._snapshotted_findings : list [tuple [HTTPServerBase , str , object ]] = []
39+
40+ def register (self , server : HTTPServerBase ) -> bool :
41+ """
42+ Registers the server.
43+
44+ :param server: the server object
45+ :return: ``True`` if the server was not registered before, ``False`` otherwise
46+ """
47+ if server in self ._servers :
48+ return False
49+ self ._servers .append (server )
50+ return True
51+
52+ def snapshot (self , server : HTTPServerBase ) -> None :
53+ """
54+ Copies the not yet reported findings of the server to the registry.
55+
56+ This is called from a finalizer of the fixture which registered the
57+ server, so the findings collected during the teardown phase of the test
58+ are copied before any other (typically session scoped) fixture
59+ finalizer clears the state of the server.
60+
61+ :param server: the server object
62+ """
63+ for kind , finding in self ._iter_findings (server ):
64+ if not self ._is_reported (finding ):
65+ self ._snapshotted_findings .append ((server , kind , finding ))
66+
67+ def _is_reported (self , finding : object ) -> bool :
68+ return any (finding is reported for reported in self ._reported_findings )
69+
70+ def _iter_findings (self , server : HTTPServerBase ) -> Generator [tuple [str , object ], None , None ]:
71+ for assertion in server .assertions :
72+ yield ("assertion" , assertion )
73+ for handler_error in server .handler_errors :
74+ yield ("error" , handler_error )
75+
76+ def collect_findings (self ) -> list [str ]:
77+ """
78+ Returns the report lines of the findings which were not reported yet.
79+
80+ The returned findings are marked as reported so subsequent calls won't
81+ include them again.
82+
83+ :return: list of the report lines, empty list if there's nothing to report
84+ """
85+
86+ section_lines : list [str ] = []
87+
88+ for server in self ._servers :
89+ server_lines : list [str ] = []
90+ new_assertions : list [object ] = []
91+ new_handler_errors : list [object ] = []
92+
93+ candidates : list [tuple [str , object ]] = list (self ._iter_findings (server ))
94+ candidates .extend (
95+ (kind , finding )
96+ for snapshotted_server , kind , finding in self ._snapshotted_findings
97+ if snapshotted_server is server
98+ )
99+
100+ for kind , finding in candidates :
101+ if self ._is_reported (finding ):
102+ continue
103+ if kind == "assertion" :
104+ new_assertions .append (finding )
105+ else :
106+ new_handler_errors .append (finding )
107+ self ._reported_findings .append (finding )
108+
109+ if new_assertions :
110+ server_lines .append ("Pending server-side assertions:" )
111+ server_lines .append ("" )
112+ for index , assertion in enumerate (new_assertions , start = 1 ):
113+ server_lines .append ("{}. {}" .format (index , assertion ))
114+ server_lines .append ("" )
115+
116+ if new_handler_errors :
117+ server_lines .append ("Errors raised by request handlers:" )
118+ server_lines .append ("" )
119+ for index , error in enumerate (new_handler_errors , start = 1 ):
120+ server_lines .append ("{}. {!r}" .format (index , error ))
121+ server_lines .append ("" )
122+
123+ if server_lines :
124+ section_lines .append ("--- {}" .format (server ))
125+ section_lines .extend (server_lines )
126+
127+ return section_lines
128+
129+
130+ SERVERS_STASH_KEY = pytest .StashKey [_ServerRegistry ]()
18131
19132
20133class Plugin :
@@ -50,12 +163,20 @@ def register_server(request: pytest.FixtureRequest, server: HTTPServerBase) -> N
50163 :param server: the server object to register
51164 """
52165 node = request .node
53- servers = node .stash .get (SERVERS_STASH_KEY , None )
54- if servers is None :
55- servers = []
56- node .stash [SERVERS_STASH_KEY ] = servers
57- if server not in servers :
58- servers .append (server )
166+ registry = node .stash .get (SERVERS_STASH_KEY , None )
167+ if registry is None :
168+ registry = _ServerRegistry ()
169+ node .stash [SERVERS_STASH_KEY ] = registry
170+
171+ def snapshot () -> None :
172+ registry .snapshot (server )
173+
174+ if registry .register (server ):
175+ # copy the findings of the server before the state of the server is
176+ # cleared (which may happen in another fixture finalizer), so the
177+ # findings collected during the teardown phase of the test can be
178+ # attached to the report of the teardown phase
179+ request .addfinalizer (snapshot )
59180
60181
61182def get_httpserver_listen_address () -> tuple [str | None , int | None ]:
@@ -150,39 +271,19 @@ def httpserver_ipv6(make_httpserver_ipv6: HTTPServer, request: pytest.FixtureReq
150271 return server
151272
152273
153- def _format_server_findings (server : HTTPServerBase ) -> list [str ]:
154- """
155- Formats the pending (not yet raised) errors of the specified server.
156-
157- :param server: the server object
158- :return: list of the report lines, empty list if there's nothing to report
159- """
160-
161- lines : list [str ] = []
162-
163- assertions = list (server .assertions )
164- if assertions :
165- lines .append ("Pending server-side assertions:" )
166- lines .append ("" )
167- for index , assertion in enumerate (assertions , start = 1 ):
168- lines .append ("{}. {}" .format (index , assertion ))
169- lines .append ("" )
170-
171- handler_errors = list (server .handler_errors )
172- if handler_errors :
173- lines .append ("Errors raised by request handlers:" )
174- lines .append ("" )
175- for index , error in enumerate (handler_errors , start = 1 ):
176- lines .append ("{}. {!r}" .format (index , error ))
177- lines .append ("" )
178-
179- return lines
274+ def _format_report_section (section_lines : list [str ]) -> str :
275+ section_lines .insert (0 , "Server-side errors collected by pytest-httpserver (not raised during the test):" )
276+ section_lines .append (
277+ "Note: these errors were not raised because the test did not call the check "
278+ "methods of the server (for example HTTPServer.check())."
279+ )
280+ return "\n " .join (section_lines )
180281
181282
182283@pytest .hookimpl (wrapper = True )
183284def pytest_runtest_makereport (
184285 item : pytest .Item ,
185- call : pytest .CallInfo [None ], # noqa: ARG001
286+ call : pytest .CallInfo [None ],
186287) -> Generator [None , object , object ]:
187288 """
188289 Adds the pending server-side errors to the report of the failed test.
@@ -197,24 +298,26 @@ def pytest_runtest_makereport(
197298 To make the debugging easier in these cases, the errors are attached to
198299 the report of the failed test (as a report section), as they may explain
199300 why the test failed.
301+
302+ Each error is attached to one report only, so if the test fails in
303+ multiple phases (such as setup and teardown), the errors are not
304+ duplicated - errors collected after a failed phase are added to the
305+ report of the next failed phase.
306+
307+ The registry of the servers is removed from the test item when the report
308+ of the teardown phase is made, so neither the servers nor their state
309+ (such as the log of the requests) outlives the test.
200310 """
201311 report = yield
202- if isinstance (report , pytest .TestReport ) and report .failed :
203- servers = item .stash .get (SERVERS_STASH_KEY , None )
204- if servers :
205- section_lines : list [str ] = []
206- for server in servers :
207- server_lines = _format_server_findings (server )
208- if server_lines :
209- section_lines .append ("--- {}" .format (server ))
210- section_lines .extend (server_lines )
211- if section_lines :
212- section_lines .insert (
213- 0 , "Server-side errors collected by pytest-httpserver (not raised during the test):"
214- )
215- section_lines .append (
216- "Note: these errors were not raised because the test did not call the check "
217- "methods of the server (for example HTTPServer.check())."
218- )
219- report .sections .append ((REPORT_SECTION_TITLE , "\n " .join (section_lines )))
312+ if isinstance (report , pytest .TestReport ):
313+ registry = item .stash .get (SERVERS_STASH_KEY , None )
314+ if registry is not None :
315+ if report .failed :
316+ section_lines = registry .collect_findings ()
317+ if section_lines :
318+ report .sections .append ((REPORT_SECTION_TITLE , _format_report_section (section_lines )))
319+ if call .when == "teardown" :
320+ # this is the last report of the test, drop the registry to
321+ # not keep the server objects (and their state) alive
322+ del item .stash [SERVERS_STASH_KEY ]
220323 return report
0 commit comments