Skip to content

Commit c60d192

Browse files
committed
test: use upstream's per-test timeout, and fix two races of the test infrastructure
A test times out after 30 seconds, as upstream, and the fixtures are not counted (pyproject.toml). A test marked slow gets three times that, as test.slow() gives upstream: the ports of the tests that call it (IF-009, IF-011, FC-001, CC-040, BT-077, BT-091, all of screencast.spec.ts and signals.spec.ts), and the Python-only tests that wait longer by design (the pause tests, the subprocess and multiprocessing tests). The wheel installation test gets its own bound. A test that times out fails with its stack (the signal method); the thread method killed the xdist worker. CI and test.sh drop their --timeout flags. Two races that the full suites on 28 workers showed: - find_free_port() reserves a port with a dual-stack bind, and a listener that gets 127.0.0.1 but loses ::1 to another process takes a new port instead of going on with IPv4 only. The IPv6-only server of the socks tests could take a port that the test server of another worker held on 127.0.0.1, and the browser, which resolves localhost to ::1 first, reached the wrong server. - The child of the signal tests closes the browser on SIGHUP and ends when it has closed, as the Node.js child does. The Python client has no SIGHUP handler, so the signal ended the child at once, and the driver, left without a client, killed the browser and could crash on its closed pipe before it logged the exit; SI-006 failed one time in two on WebKit.
1 parent 36d698e commit c60d192

28 files changed

Lines changed: 191 additions & 73 deletions

‎.github/workflows/ci.yml‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,23 @@ jobs:
107107
python -m build --wheel
108108
python -m playwright install --with-deps ${{ matrix.browser }}
109109
- name: Common Tests
110-
run: pytest tests/common --browser=${{ matrix.browser }} --timeout 90
110+
run: pytest tests/common --browser=${{ matrix.browser }}
111111
- name: Test Reference count
112112
run: pytest tests/test_reference_count_async.py --browser=${{ matrix.browser }}
113113
- name: Test Wheel Installation
114114
run: pytest tests/test_installation.py --browser=${{ matrix.browser }}
115115
- name: Test Sync API
116116
if: matrix.os != 'ubuntu-latest'
117-
run: pytest tests/sync --browser=${{ matrix.browser }} --timeout 90 --timeout-method thread -n auto
117+
run: pytest tests/sync --browser=${{ matrix.browser }} -n auto
118118
- name: Test Sync API
119119
if: matrix.os == 'ubuntu-latest'
120-
run: xvfb-run pytest tests/sync --browser=${{ matrix.browser }} --timeout 90 --timeout-method thread -n auto
120+
run: xvfb-run pytest tests/sync --browser=${{ matrix.browser }} -n auto
121121
- name: Test Async API
122122
if: matrix.os != 'ubuntu-latest'
123-
run: pytest tests/async --browser=${{ matrix.browser }} --timeout 90 --timeout-method thread -n auto
123+
run: pytest tests/async --browser=${{ matrix.browser }} -n auto
124124
- name: Test Async API
125125
if: matrix.os == 'ubuntu-latest'
126-
run: xvfb-run pytest tests/async --browser=${{ matrix.browser }} --timeout 90 --timeout-method thread -n auto
126+
run: xvfb-run pytest tests/async --browser=${{ matrix.browser }} -n auto
127127

128128
test-stable:
129129
name: Stable
@@ -154,19 +154,19 @@ jobs:
154154
python -m build --wheel
155155
python -m playwright install ${{ matrix.browser-channel }} --with-deps
156156
- name: Common Tests
157-
run: pytest tests/common --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90
157+
run: pytest tests/common --browser=chromium --browser-channel=${{ matrix.browser-channel }}
158158
- name: Test Sync API
159159
if: matrix.os != 'ubuntu-latest'
160-
run: pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 --timeout-method thread -n auto
160+
run: pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n auto
161161
- name: Test Sync API
162162
if: matrix.os == 'ubuntu-latest'
163-
run: xvfb-run pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 --timeout-method thread -n auto
163+
run: xvfb-run pytest tests/sync --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n auto
164164
- name: Test Async API
165165
if: matrix.os != 'ubuntu-latest'
166-
run: pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 --timeout-method thread -n auto
166+
run: pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n auto
167167
- name: Test Async API
168168
if: matrix.os == 'ubuntu-latest'
169-
run: xvfb-run pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} --timeout 90 --timeout-method thread -n auto
169+
run: xvfb-run pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n auto
170170

171171
test_examples:
172172
name: Examples

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ The tests are a port of the upstream tests in `microsoft/playwright` (`tests/pag
5656

5757
- Port each upstream test twice, to `tests/async/` and to `tests/sync/`, in the file that matches the upstream file: `tests/page/page-check.spec.ts` becomes `tests/async/page/test_page_check.py` and `tests/sync/page/test_page_check.py`; `tests/library/chromium/oopif.spec.ts` becomes `tests/async/library/chromium/test_oopif.py`.
5858
- Name the test `test_<ID>_<title>`: the identifier with `_`, then the `describe` titles and the title in snake_case. `test('EX-181 toHaveCount pass')` in `test.describe('toHaveCount')` becomes `test_EX_181_to_have_count_to_have_count_pass`. A test that a loop declares several times is one function with `@pytest.mark.parametrize`.
59-
- Port faithfully: the same steps, assertions and skip conditions. A test that fails because of a bug in the Python client stays, marked `@pytest.mark.xfail(reason="python: ...", strict=True)`.
59+
- Port faithfully: the same steps, assertions and skip conditions. The timeout of a test is 30 seconds, as upstream (`pyproject.toml`); where upstream calls `test.slow()`, mark the port `@pytest.mark.slow`, which triples it. A Python-only test that waits longer than 30 seconds by design gets the same mark. A test that fails because of a bug in the Python client stays, marked `@pytest.mark.xfail(reason="python: ...", strict=True)`.
6060
- A test that can not exist in Python (a JavaScript-only API, Node.js client internals, tools that run in the driver, `@playwright/test` features) goes in `tests/upstream_not_ported.txt`, with the reason.
6161
- `python scripts/check_test_registry.py` checks all of this. `--missing <code or file glob>` prints the expected names of the tests still to port; `--report` prints the coverage per subsystem.
6262
- Do not write a new test when an upstream test exists. A Python-only test goes in `tests/async/python/` or `tests/sync/python/`, in the file named after the upstream file of the feature.

‎pyproject.toml‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,21 @@ version_file = "playwright/_repo_version.py"
6060

6161
[tool.pytest.ini_options]
6262
addopts = "-Wall -rsx -vv -s"
63+
# The timeout of a test is 30 seconds, as upstream; a test marked slow gets three
64+
# times that, as test.slow() gives upstream (tests/conftest.py). The timeout covers
65+
# the test function; the fixtures (a browser launch) are not counted. The signal
66+
# method fails the test with its stack and keeps the worker; the thread method
67+
# kills the process (on Windows, pytest-timeout falls back to it).
68+
timeout = 30
69+
timeout_method = "signal"
70+
timeout_func_only = true
6371
markers = [
6472
"skip_browser",
6573
"only_browser",
6674
"skip_platform",
6775
"only_platform",
68-
"skip_protocol_check"
76+
"skip_protocol_check",
77+
"slow: the test gets three times the timeout, as test.slow() upstream",
6978
]
7079
junit_family = "xunit2"
7180
asyncio_mode = "auto"

‎test.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ python=python
1414
export PYTHONPATH="scripts${PYTHONPATH:+:$PYTHONPATH}"
1515
# --maxschedchunk=1: a worker gets one test at a time, as soon as it is free, so
1616
# that the slow tests at the end of the collection spread over all workers.
17-
args=(--browser "$browser" -n "$workers" --maxschedchunk=1 --timeout 120 --timeout-method thread -p pytest_line_reporter -rfE --disable-warnings)
17+
args=(--browser "$browser" -n "$workers" --maxschedchunk=1 -p pytest_line_reporter -rfE --disable-warnings)
1818
status=0
1919
xvfb-run -a "$python" -m pytest tests/sync "${args[@]}" "$@" || status=$?
2020
xvfb-run -a "$python" -m pytest tests/async "${args[@]}" "$@" || status=$?

‎tests/async/library/test_browsertype_connect.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,7 @@ def handle_upload(request: TestServerRequest) -> None:
12981298
upload_file.unlink()
12991299

13001300

1301+
@pytest.mark.slow
13011302
@pytest.mark.parametrize("kind", KINDS)
13021303
async def test_BT_077_should_upload_a_folder(
13031304
kind: str,
@@ -1783,6 +1784,7 @@ async def wait_for_navigation() -> Error:
17831784
assert TARGET_CLOSED_ERROR_MESSAGE in (await wait_for_navigation()).message
17841785

17851786

1787+
@pytest.mark.slow
17861788
async def test_BT_091_launch_server_only_should_be_able_to_reconnect_to_a_browser_12_times_without_warnings(
17871789
connect: Callable[..., Awaitable[Browser]],
17881790
start_remote_server: RemoteServerStarter,

‎tests/async/library/test_client_certificates.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,7 @@ def respond(peer: PeerInfo) -> Response:
10991099
server.close()
11001100

11011101

1102+
@pytest.mark.slow
11021103
async def test_CC_040_browser_persistent_context_validate_input(
11031104
launch_persistent: LaunchPersistent,
11041105
) -> None:

‎tests/async/library/test_screencast.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343

4444
from ..utils import utils
4545

46+
# All the tests of the file are slow upstream (test.slow() at the top of the file).
47+
pytestmark = pytest.mark.slow
48+
4649

4750
async def ensure_some_frames(page: Page) -> None:
4851
"""Port of ensureSomeFrames() in tests/config/utils.ts."""

‎tests/async/library/test_signals.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
from tests.helpers.client_process import ClientProcess
3636
from tests.server import Server
3737

38+
# All the tests of the file are slow upstream (test.slow() at the top of the file).
39+
pytestmark = pytest.mark.slow
40+
3841
FLAVOR = "async"
3942

4043
skip_on_windows = pytest.mark.skipif(
@@ -169,9 +172,9 @@ def test_SI_006_signals_should_close_the_browser_on_sighup(
169172
client.kill(signal.SIGHUP)
170173
assert client.out("exitCode") == "0"
171174
assert client.out("signal") == "null"
172-
# Node.js handles SIGHUP and exits with 0. Python does not handle SIGHUP, so the
173-
# signal ends the process.
174-
assert client.child_exit_code() == -signal.SIGHUP
175+
# The child ignores SIGHUP and ends when the browser has closed, as the Node.js
176+
# child does; see client_process_impl.py.
177+
assert client.child_exit_code() == 0
175178

176179

177180
@skip_on_windows

‎tests/async/page/test_page_filechooser.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ..utils import Utils
2929

3030

31+
@pytest.mark.slow
3132
async def test_FC_001_should_upload_multiple_large_files(
3233
page: Page, server: Server, tmp_path: Path
3334
) -> None:

‎tests/async/page/test_page_set_input_files.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ async def test_IF_008_should_upload_a_file_after_popup(
199199
assert await page.evaluate("e => e.files[0].name", input) == "file-to-upload.txt"
200200

201201

202+
@pytest.mark.slow
202203
async def test_IF_009_should_upload_large_file(
203204
page: Page, server: Server, tmp_path: Path
204205
) -> None:
@@ -239,6 +240,7 @@ async def test_IF_010_should_throw_an_error_if_the_file_does_not_exist(
239240
assert "i actually do not exist.txt" in str(exc_info.value)
240241

241242

243+
@pytest.mark.slow
242244
async def test_IF_011_should_upload_large_file_with_relative_path(
243245
page: Page, server: Server, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
244246
) -> None:

0 commit comments

Comments
 (0)