|
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, List |
| 25 | +from typing import Any, Callable, Dict, Generator, List |
23 | 26 |
|
24 | 27 | import pytest |
25 | 28 | from greenlet import greenlet |
26 | 29 |
|
27 | 30 | from playwright.sync_api import ( |
28 | 31 | Browser, |
29 | 32 | BrowserContext, |
| 33 | + ConsoleMessage, |
30 | 34 | Dialog, |
31 | 35 | Error, |
32 | 36 | Page, |
@@ -471,6 +475,77 @@ def dispose_route(): |
471 | 475 | assert result.returncode == 0, result.stderr |
472 | 476 |
|
473 | 477 |
|
| 478 | +class Interrupted(BaseException): |
| 479 | + """Raised from a signal handler, like pytest-timeout's Failed.""" |
| 480 | + |
| 481 | + |
| 482 | +@contextlib.contextmanager |
| 483 | +def interrupt_after(seconds: float) -> Generator[None, None, None]: |
| 484 | + # Python runs the handler in whatever code executes when the alarm fires. |
| 485 | + # During a blocking sync call that is the dispatcher's event loop, not the |
| 486 | + # test. The previous handler and timer belong to pytest-timeout. |
| 487 | + def handler(signum: int, frame: Any) -> None: |
| 488 | + raise Interrupted() |
| 489 | + |
| 490 | + previous_handler = signal.signal(signal.SIGALRM, handler) |
| 491 | + previous_timer = signal.setitimer(signal.ITIMER_REAL, seconds) |
| 492 | + try: |
| 493 | + yield |
| 494 | + finally: |
| 495 | + signal.setitimer(signal.ITIMER_REAL, *previous_timer) |
| 496 | + signal.signal(signal.SIGALRM, previous_handler) |
| 497 | + |
| 498 | + |
| 499 | +@pytest.mark.skipif(sys.platform == "win32", reason="SIGALRM is not available") |
| 500 | +def test_sync_should_survive_exception_raised_by_signal_handler(page: Page) -> None: |
| 501 | + with interrupt_after(0.2): |
| 502 | + with pytest.raises(Interrupted): |
| 503 | + page.wait_for_timeout(10_000) |
| 504 | + # The connection is still usable, and the next call does not wait for the |
| 505 | + # abandoned one to finish. |
| 506 | + start = time.monotonic() |
| 507 | + assert page.evaluate("1 + 1") == 2 |
| 508 | + assert time.monotonic() - start < 5 |
| 509 | + assert page.title() == "" |
| 510 | + |
| 511 | + |
| 512 | +@pytest.mark.skipif(sys.platform == "win32", reason="SIGALRM is not available") |
| 513 | +def test_sync_expect_event_should_survive_exception_raised_by_signal_handler( |
| 514 | + page: Page, |
| 515 | +) -> None: |
| 516 | + with interrupt_after(0.2): |
| 517 | + with pytest.raises(Interrupted): |
| 518 | + with page.expect_event("console"): |
| 519 | + pass |
| 520 | + with page.expect_console_message() as console_info: |
| 521 | + page.evaluate("console.log('hello')") |
| 522 | + assert console_info.value.text == "hello" |
| 523 | + |
| 524 | + |
| 525 | +@pytest.mark.skipif(sys.platform == "win32", reason="SIGALRM is not available") |
| 526 | +def test_sync_should_survive_exception_raised_by_signal_handler_while_event_handler_blocks( |
| 527 | + page: Page, |
| 528 | +) -> None: |
| 529 | + handled: List[str] = [] |
| 530 | + |
| 531 | + def handler(message: ConsoleMessage) -> None: |
| 532 | + page.wait_for_timeout(500) |
| 533 | + handled.append(message.text) |
| 534 | + |
| 535 | + page.on("console", handler) |
| 536 | + with interrupt_after(0.2): |
| 537 | + with pytest.raises(Interrupted): |
| 538 | + page.evaluate("console.log('hello')") |
| 539 | + page.wait_for_timeout(10_000) |
| 540 | + assert page.evaluate("1 + 1") == 2 |
| 541 | + # The handler was suspended in its own sync call. It finishes once the |
| 542 | + # dispatcher runs the event loop again. |
| 543 | + deadline = time.monotonic() + 10 |
| 544 | + while not handled and time.monotonic() < deadline: |
| 545 | + page.wait_for_timeout(100) |
| 546 | + assert handled == ["hello"] |
| 547 | + |
| 548 | + |
474 | 549 | def test_click_should_accept_timedelta_for_timeout(page: Page) -> None: |
475 | 550 | with pytest.raises(TimeoutError, match="Timeout 1ms exceeded"): |
476 | 551 | page.click("does-not-exist", timeout=timedelta(milliseconds=1)) |
0 commit comments