Skip to content

Commit f139aae

Browse files
committed
fix: collect artifacts on test setup/teardown (#117)
1 parent a22ee09 commit f139aae

File tree

3 files changed

+150
-2
lines changed

3 files changed

+150
-2
lines changed

pytest.ini

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
; [pytest]
2+
; log_cli=true
3+
; log_cli_level=DEBUG
4+
; log_cli_format=%(asctime)s.%(msecs)03d %(levelname)s %(name)s:%(filename)s:%(lineno)s: %(message)s
5+
; log_cli_date_format=%Y-%m-%d %H:%M:%S

pytest_playwright/pytest_playwright.py

+76-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import sys
1919
import warnings
2020
from typing import Any, Callable, Dict, Generator, List, Optional
21-
21+
import logging
2222
import pytest
2323
from playwright.sync_api import (
2424
Browser,
@@ -33,6 +33,8 @@
3333
import tempfile
3434

3535

36+
log = logging.getLogger(__name__)
37+
3638
artifacts_folder = tempfile.TemporaryDirectory(prefix="playwright-pytest-")
3739

3840

@@ -74,6 +76,34 @@ def pytest_configure(config: Any) -> None:
7476
"markers",
7577
"browser_context_args(**kwargs): provide additional arguments to browser.new_context()",
7678
)
79+
log.debug("pytest_configure")
80+
class Teardown:
81+
failed = False
82+
setattr(config, "teardown", Teardown)
83+
84+
85+
def pytest_runtest_teardown(item):
86+
# import faulthandler
87+
# faulthandler.dump_traceback(all_threads=True)
88+
log.debug("pytest_runtest_teardown")
89+
item.config.teardown.failed = True
90+
91+
92+
def pytest_sessionfinish(session, exitstatus):
93+
log.debug("pytest_runtest_teardown")
94+
log.debug(f"{exitstatus=}")
95+
96+
97+
def pytest_exception_interact(node, call, report):
98+
log.debug("1pytest_exception_interact")
99+
log.debug(f"{node.config.teardown.failed=}")
100+
log.debug(f"{node.session.testsfailed=}")
101+
excinfo = call.exc_info if hasattr(call, "exc_info") else None
102+
log.debug(f"{excinfo=}")
103+
104+
if call.when == "teardown":
105+
log.debug("2pytest_exception_interact")
106+
node.config.teardown.failed = True
77107

78108

79109
# Making test result information available in fixtures
@@ -256,10 +286,52 @@ def context(
256286
)
257287

258288
yield context
289+
# import traceback
290+
# log.debug(f"{dir(traceback)=}")
291+
# log.debug(f"{traceback.print_last()=}")
292+
# stack = traceback.extract_stack()
293+
# log.debug(f"{stack=}")
294+
# for frame_summary in stack:
295+
# log.debug(f"{frame_summary.filename=}")
296+
# log.debug(f"{frame_summary.name=}")
297+
# log.debug(f"{frame_summary.colno=}")
298+
299+
300+
# log.debug(f"{traceback.print_stack()=}")
259301

302+
# log.debug(f"{traceback.print_exc()=}")
303+
304+
log.debug(f"{request.session.testscollected=}")
305+
log.debug(f"{request.session.exitstatus=}")
306+
log.debug(f"{request.session.testsfailed=}")
307+
308+
if request.session.testsfailed:
309+
log.debug("Only print if failed")
310+
311+
log.debug("context")
260312
# If request.node is missing rep_call, then some error happened during execution
261313
# that prevented teardown, but should still be counted as a failure
262-
failed = request.node.rep_call.failed if hasattr(request.node, "rep_call") else True
314+
failed_setup = request.node.rep_setup.failed if hasattr(request.node, "rep_setup") else False
315+
failed_call = request.node.rep_call.failed if hasattr(request.node, "rep_call") else False
316+
failed_teardown = request.node.rep_teardown.failed if hasattr(request.node, "rep_teardown") else False
317+
318+
failed_xteardown = request.config.teardown.failed if hasattr(request.config, "teardown") else False
319+
320+
321+
failed = failed_setup or failed_call or failed_xteardown
322+
log.debug(f"{failed=}")
323+
log.debug(f"{failed_setup=}")
324+
log.debug(f"{failed_call=}")
325+
log.debug(f"{failed_teardown=}")
326+
log.debug(f"{failed_xteardown=}")
327+
328+
329+
log.debug(f"{hasattr(request.node, 'rep_setup')=}")
330+
log.debug(f"{hasattr(request.node, 'rep_call')=}")
331+
log.debug(f"{hasattr(request.node, 'rep_teardown')=}")
332+
333+
log.debug(f"{hasattr(request.config, 'teardown')=}")
334+
263335

264336
if capture_trace:
265337
retain_trace = tracing_option == "on" or (
@@ -272,9 +344,11 @@ def context(
272344
context.tracing.stop()
273345

274346
screenshot_option = pytestconfig.getoption("--screenshot")
347+
log.debug(f"{screenshot_option=}")
275348
capture_screenshot = screenshot_option == "on" or (
276349
failed and screenshot_option == "only-on-failure"
277350
)
351+
log.debug(f"{capture_screenshot=}")
278352
if capture_screenshot:
279353
for index, page in enumerate(pages):
280354
human_readable_status = "failed" if failed else "finished"

tests/test_playwright.py

+69
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,73 @@ def test_failing(page):
703703
_assert_folder_tree(test_results_dir, expected)
704704

705705

706+
def test_artifacts_retain_on_setup_failure(testdir: pytest.Testdir) -> None:
707+
testdir.makepyfile(
708+
"""
709+
import pytest
710+
@pytest.fixture
711+
def failed_setup_call(page):
712+
assert 1 == page.evaluate("1 + 1")
713+
yield page
714+
715+
def test_failing(page, failed_setup_call):
716+
assert 2 == page.evaluate("1 + 1")
717+
"""
718+
)
719+
result = testdir.runpytest(
720+
"--screenshot",
721+
"only-on-failure"
722+
)
723+
result.assert_outcomes(errors=1)
724+
test_results_dir = os.path.join(testdir.tmpdir, "test-results")
725+
726+
expected = [
727+
{
728+
"name": "test-artifacts-retain-on-setup-failure-py-test-failing-chromium",
729+
"children": [
730+
{
731+
"name": "test-failed-1.png",
732+
}
733+
],
734+
}
735+
]
736+
_assert_folder_tree(test_results_dir, expected)
737+
738+
# request.config.teardown.failed = False
739+
def test_artifacts_on_teardown(testdir: pytest.Testdir) -> None:
740+
testdir.makepyfile(
741+
"""
742+
import pytest
743+
@pytest.fixture
744+
def failed_teardown_call(page, request):
745+
yield page
746+
assert 1 == page.evaluate("1 + 1")
747+
748+
def test_passing(page, failed_teardown_call):
749+
assert 2 == page.evaluate("1 + 1")
750+
"""
751+
)
752+
result = testdir.runpytest(
753+
"--screenshot",
754+
"only-on-failure"
755+
)
756+
result.assert_outcomes(passed=1, errors=1) # сделать чтобы скрин не сохранялся при успешном тесте (и все успешные тирдауны и сетапы)
757+
test_results_dir = os.path.join(testdir.tmpdir, "test-results")
758+
print(f"{os.listdir(test_results_dir)=}")
759+
print(os.listdir(f"{test_results_dir}/test-artifacts-on-teardown-py-test-passing-chromium"))
760+
expected = [
761+
{
762+
"name": "test-artifacts-on-teardown-py-test-passing-chromium",
763+
"children": [
764+
{
765+
"name": "test-failed-1.png",
766+
}
767+
],
768+
}
769+
]
770+
_assert_folder_tree(test_results_dir, expected)
771+
772+
706773
def test_should_work_with_test_names_which_exceeds_256_characters(
707774
testdir: pytest.Testdir,
708775
) -> None:
@@ -734,8 +801,10 @@ def _assert_folder_tree(root: str, expected_tree: List[Any]) -> None:
734801
for file in expected_tree:
735802
if isinstance(file["name"], str):
736803
if "children" in file:
804+
print(f"{file=}")
737805
assert os.path.isdir(os.path.join(root, file["name"]))
738806
else:
807+
print(f"{file=}")
739808
assert os.path.isfile(os.path.join(root, file["name"]))
740809
if isinstance(file["name"], re.Pattern):
741810
assert any([file["name"].match(item) for item in os.listdir(root)])

0 commit comments

Comments
 (0)