|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
| 15 | +import contextlib |
15 | 16 | import multiprocessing |
16 | 17 | import os |
| 18 | +import signal |
17 | 19 | import subprocess |
18 | 20 | import sys |
19 | 21 | import textwrap |
| 22 | +import time |
20 | 23 | from datetime import timedelta |
21 | 24 | from pathlib import Path |
22 | | -from typing import Any, Callable, Dict |
| 25 | +from typing import Any, Callable, Dict, Generator, List |
23 | 26 |
|
24 | 27 | import pytest |
25 | 28 |
|
26 | 29 | from playwright.sync_api import ( |
27 | 30 | Browser, |
28 | 31 | BrowserContext, |
| 32 | + ConsoleMessage, |
29 | 33 | Dialog, |
30 | 34 | Error, |
31 | 35 | Page, |
@@ -448,6 +452,77 @@ def dispose_route(): |
448 | 452 | assert result.returncode == 0, result.stderr |
449 | 453 |
|
450 | 454 |
|
| 455 | +class Interrupted(BaseException): |
| 456 | + """Raised from a signal handler, like pytest-timeout's Failed.""" |
| 457 | + |
| 458 | + |
| 459 | +@contextlib.contextmanager |
| 460 | +def interrupt_after(seconds: float) -> Generator[None, None, None]: |
| 461 | + # Python runs the handler in whatever code executes when the alarm fires. |
| 462 | + # During a blocking sync call that is the dispatcher's event loop, not the |
| 463 | + # test. The previous handler and timer belong to pytest-timeout. |
| 464 | + def handler(signum: int, frame: Any) -> None: |
| 465 | + raise Interrupted() |
| 466 | + |
| 467 | + previous_handler = signal.signal(signal.SIGALRM, handler) |
| 468 | + previous_timer = signal.setitimer(signal.ITIMER_REAL, seconds) |
| 469 | + try: |
| 470 | + yield |
| 471 | + finally: |
| 472 | + signal.setitimer(signal.ITIMER_REAL, *previous_timer) |
| 473 | + signal.signal(signal.SIGALRM, previous_handler) |
| 474 | + |
| 475 | + |
| 476 | +@pytest.mark.skipif(sys.platform == "win32", reason="SIGALRM is not available") |
| 477 | +def test_sync_should_survive_exception_raised_by_signal_handler(page: Page) -> None: |
| 478 | + with interrupt_after(0.2): |
| 479 | + with pytest.raises(Interrupted): |
| 480 | + page.wait_for_timeout(10_000) |
| 481 | + # The connection is still usable, and the next call does not wait for the |
| 482 | + # abandoned one to finish. |
| 483 | + start = time.monotonic() |
| 484 | + assert page.evaluate("1 + 1") == 2 |
| 485 | + assert time.monotonic() - start < 5 |
| 486 | + assert page.title() == "" |
| 487 | + |
| 488 | + |
| 489 | +@pytest.mark.skipif(sys.platform == "win32", reason="SIGALRM is not available") |
| 490 | +def test_sync_expect_event_should_survive_exception_raised_by_signal_handler( |
| 491 | + page: Page, |
| 492 | +) -> None: |
| 493 | + with interrupt_after(0.2): |
| 494 | + with pytest.raises(Interrupted): |
| 495 | + with page.expect_event("console"): |
| 496 | + pass |
| 497 | + with page.expect_console_message() as console_info: |
| 498 | + page.evaluate("console.log('hello')") |
| 499 | + assert console_info.value.text == "hello" |
| 500 | + |
| 501 | + |
| 502 | +@pytest.mark.skipif(sys.platform == "win32", reason="SIGALRM is not available") |
| 503 | +def test_sync_should_survive_exception_raised_by_signal_handler_while_event_handler_blocks( |
| 504 | + page: Page, |
| 505 | +) -> None: |
| 506 | + handled: List[str] = [] |
| 507 | + |
| 508 | + def handler(message: ConsoleMessage) -> None: |
| 509 | + page.wait_for_timeout(500) |
| 510 | + handled.append(message.text) |
| 511 | + |
| 512 | + page.on("console", handler) |
| 513 | + with interrupt_after(0.2): |
| 514 | + with pytest.raises(Interrupted): |
| 515 | + page.evaluate("console.log('hello')") |
| 516 | + page.wait_for_timeout(10_000) |
| 517 | + assert page.evaluate("1 + 1") == 2 |
| 518 | + # The handler was suspended in its own sync call. It finishes once the |
| 519 | + # dispatcher runs the event loop again. |
| 520 | + deadline = time.monotonic() + 10 |
| 521 | + while not handled and time.monotonic() < deadline: |
| 522 | + page.wait_for_timeout(100) |
| 523 | + assert handled == ["hello"] |
| 524 | + |
| 525 | + |
451 | 526 | def test_click_should_accept_timedelta_for_timeout(page: Page) -> None: |
452 | 527 | with pytest.raises(TimeoutError, match="Timeout 1ms exceeded"): |
453 | 528 | page.click("does-not-exist", timeout=timedelta(milliseconds=1)) |
0 commit comments