diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 3f4c06c3e22..64906d05188 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -190,6 +190,9 @@ jobs: timeout_minutes: 20 max_attempts: 2 # https://github.com/servo/servo/issues/30683 command: ./mach test-unit --${{ inputs.profile }} + - name: Devtools tests + if: ${{ inputs.unit-tests }} + run: ./mach test-devtools --${{ inputs.profile }} - name: Build libservo with examples if: ${{ inputs.build-libservo }} continue-on-error: true diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 8a83d247fea..9fbc0673eb1 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -174,6 +174,9 @@ jobs: timeout_minutes: 40 # https://github.com/servo/servo/issues/30275 max_attempts: 3 # https://github.com/servo/servo/issues/30683 command: ./mach test-unit --${{ inputs.profile }} + - name: Devtools tests + if: ${{ inputs.unit-tests }} + run: ./mach test-devtools --${{ inputs.profile }} - name: Build libservo with examples if: ${{ inputs.build-libservo }} continue-on-error: true diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c6aa3dc7445..b3bf9122c13 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -187,6 +187,9 @@ jobs: timeout_minutes: 30 max_attempts: 3 # https://github.com/servo/servo/issues/30683 command: .\mach test-unit --${{ inputs.profile }} -- -- --test-threads=1 + - name: Devtools tests + if: ${{ inputs.unit-tests }} + run: .\mach test-devtools --${{ inputs.profile }} - name: Build libservo with examples if: ${{ inputs.build-libservo }} continue-on-error: true diff --git a/components/devtools/protocol.rs b/components/devtools/protocol.rs index d3be1e75bc7..c4729ebfb15 100644 --- a/components/devtools/protocol.rs +++ b/components/devtools/protocol.rs @@ -5,7 +5,7 @@ //! Low-level wire protocol implementation. Currently only supports //! [JSON packets](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#json-packets). -use std::io::{Read, Write}; +use std::io::{ErrorKind, Read, Write}; use std::net::TcpStream; use log::debug; @@ -49,7 +49,8 @@ impl JsonPacketStream for TcpStream { loop { let mut buf = [0]; let byte = match self.read(&mut buf) { - Ok(0) => return Ok(None), // EOF + Ok(0) => return Ok(None), // EOF + Err(e) if e.kind() == ErrorKind::ConnectionReset => return Ok(None), // EOF Ok(1) => buf[0], Ok(_) => unreachable!(), Err(e) => return Err(e.to_string()), diff --git a/python/servo/devtools_tests.py b/python/servo/devtools_tests.py index c1043ce42df..bdecaa3de27 100644 --- a/python/servo/devtools_tests.py +++ b/python/servo/devtools_tests.py @@ -7,9 +7,11 @@ # option. This file may not be copied, modified, or distributed # except according to those terms. +from __future__ import annotations from concurrent.futures import Future from dataclasses import dataclass import logging +import socket from geckordp.actors.root import RootActor from geckordp.actors.descriptors.tab import TabActor from geckordp.actors.watcher import WatcherActor @@ -23,7 +25,7 @@ import socketserver import subprocess import time from threading import Thread -from typing import Optional +from typing import Any, Optional import unittest from servo.command_base import BuildType @@ -43,28 +45,96 @@ class Devtools: client: RDPClient watcher: WatcherActor targets: list + exited: bool = False + + def connect(*, expected_targets: int = 1) -> Devtools: + """ + Connect to the Servo devtools server. + You should use a `with` statement to ensure we disconnect unconditionally. + """ + client = RDPClient() + client.connect("127.0.0.1", 6080) + root = RootActor(client) + tabs = root.list_tabs() + tab_dict = tabs[0] + tab = TabActor(client, tab_dict["actor"]) + watcher = tab.get_watcher() + watcher = WatcherActor(client, watcher["actor"]) + + done = Future() + targets = [] + + def on_target(data): + try: + targets.append(data["target"]) + if len(targets) == expected_targets: + done.set_result(None) + except Exception as e: + # Raising here does nothing, for some reason. + # Send the exception back so it can be raised. + done.set_result(e) + + client.add_event_listener( + watcher.actor_id, + Events.Watcher.TARGET_AVAILABLE_FORM, + on_target, + ) + watcher.watch_targets(WatcherActor.Targets.FRAME) + watcher.watch_targets(WatcherActor.Targets.WORKER) + + result: Optional[Exception] = done.result(1) + if result: + raise result + + return Devtools(client, watcher, targets) + + def __getattribute__(self, name: str) -> Any: + """ + Access a property, raising a ValueError if the instance was previously marked as exited. + """ + if name != "exited" and object.__getattribute__(self, "exited"): + raise ValueError("Devtools instance must not be used after __exit__()") + return object.__getattribute__(self, name) + + def __enter__(self) -> Devtools: + """ + Enter the `with` context for this instance, raising a ValueError if it was previously marked as exited. + """ + if self.exited: + raise ValueError("Devtools instance must not be used after __exit__()") + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + """ + Exit the `with` context for this instance, disconnecting the client and marking it as exited. + Does not raise a ValueError if it was previously marked as exited, so you can nest `with` statements. + """ + if not self.exited: + # Ignore any return value; we never want to return True to suppress exceptions + self.client.__exit__(exc_type, exc_value, traceback) + self.exited = True class DevtoolsTests(unittest.IsolatedAsyncioTestCase): # /path/to/servo/python/servo script_path = None build_type: Optional[BuildType] = None + base_urls = None + web_servers = None + web_server_threads = None def __init__(self, methodName="runTest"): super().__init__(methodName) self.servoshell = None - self.base_urls = None - self.web_servers = None - self.web_server_threads = None # Watcher tests def test_watcher_returns_same_breakpoint_list_actor_every_time(self): self.run_servoshell(url="data:text/html,") - devtools = self._setup_devtools_client() - response1 = devtools.watcher.get_breakpoint_list_actor() - response2 = devtools.watcher.get_breakpoint_list_actor() - self.assertEqual(response1["breakpointList"]["actor"], response2["breakpointList"]["actor"]) + with Devtools.connect() as devtools: + response1 = devtools.watcher.get_breakpoint_list_actor() + response2 = devtools.watcher.get_breakpoint_list_actor() + self.assertEqual(response1["breakpointList"]["actor"], response2["breakpointList"]["actor"]) # Sources list # Classic script vs module script: @@ -77,22 +147,20 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): # Worker script sources can be external or blob. def test_sources_list(self): - self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) - self.run_servoshell() + self.run_servoshell(url=f"{self.base_urls[0]}/sources/test.html") self.assert_sources_list( set( [ - # TODO: update expectations when we fix ES modules tuple( [ - Source("srcScript", f"{self.base_urls[0]}/classic.js"), - Source("inlineScript", f"{self.base_urls[0]}/test.html"), - Source("inlineScript", f"{self.base_urls[0]}/test.html"), - Source("srcScript", f"{self.base_urls[1]}/classic.js"), - Source("importedModule", f"{self.base_urls[0]}/module.js"), + Source("srcScript", f"{self.base_urls[0]}/sources/classic.js"), + Source("inlineScript", f"{self.base_urls[0]}/sources/test.html"), + Source("inlineScript", f"{self.base_urls[0]}/sources/test.html"), + Source("srcScript", f"{self.base_urls[1]}/sources/classic.js"), + Source("importedModule", f"{self.base_urls[0]}/sources/module.js"), ] ), - tuple([Source("Worker", f"{self.base_urls[0]}/classic_worker.js")]), + tuple([Source("Worker", f"{self.base_urls[0]}/sources/classic_worker.js")]), ] ), ) @@ -112,9 +180,8 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): self.assert_sources_list(set([tuple([Source("inlineScript", "data:text/html,")])])) def test_sources_list_with_data_external_classic_script(self): - self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) - self.run_servoshell(url=f'data:text/html,') - self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/classic.js")])])) + self.run_servoshell(url=f'data:text/html,') + self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/sources/classic.js")])])) def test_sources_list_with_data_empty_inline_module_script(self): self.run_servoshell(url="data:text/html,") @@ -127,24 +194,23 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): ) def test_sources_list_with_data_external_module_script(self): - self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) - self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_data_external_module_script.html") - self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/module.js")])])) + self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_data_external_module_script.html") + self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/sources/module.js")])])) # Sources list for `introductionType` = `importedModule` def test_sources_list_with_static_import_module(self): - self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) - self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_static_import_module.html") + self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_static_import_module.html") self.assert_sources_list( set( [ tuple( [ Source( - "inlineScript", f"{self.base_urls[0]}/test_sources_list_with_static_import_module.html" + "inlineScript", + f"{self.base_urls[0]}/sources/test_sources_list_with_static_import_module.html", ), - Source("importedModule", f"{self.base_urls[0]}/module.js"), + Source("importedModule", f"{self.base_urls[0]}/sources/module.js"), ] ) ] @@ -152,17 +218,17 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): ) def test_sources_list_with_dynamic_import_module(self): - self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) - self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_dynamic_import_module.html") + self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_dynamic_import_module.html") self.assert_sources_list( set( [ tuple( [ Source( - "inlineScript", f"{self.base_urls[0]}/test_sources_list_with_dynamic_import_module.html" + "inlineScript", + f"{self.base_urls[0]}/sources/test_sources_list_with_dynamic_import_module.html", ), - Source("importedModule", f"{self.base_urls[0]}/module.js"), + Source("importedModule", f"{self.base_urls[0]}/sources/module.js"), ] ) ] @@ -172,19 +238,21 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): # Sources list for `introductionType` = `Worker` def test_sources_list_with_classic_worker(self): - self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) - self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_classic_worker.html") + self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_classic_worker.html") self.assert_sources_list( set( [ tuple( [ - Source("inlineScript", f"{self.base_urls[0]}/test_sources_list_with_classic_worker.html"), + Source( + "inlineScript", + f"{self.base_urls[0]}/sources/test_sources_list_with_classic_worker.html", + ), ] ), tuple( [ - Source("Worker", f"{self.base_urls[0]}/classic_worker.js"), + Source("Worker", f"{self.base_urls[0]}/sources/classic_worker.js"), ] ), ] @@ -192,19 +260,20 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): ) def test_sources_list_with_module_worker(self): - self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) - self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_module_worker.html") + self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_module_worker.html") self.assert_sources_list( set( [ tuple( [ - Source("inlineScript", f"{self.base_urls[0]}/test_sources_list_with_module_worker.html"), + Source( + "inlineScript", f"{self.base_urls[0]}/sources/test_sources_list_with_module_worker.html" + ), ] ), tuple( [ - Source("Worker", f"{self.base_urls[0]}/module_worker.js"), + Source("Worker", f"{self.base_urls[0]}/sources/module_worker.js"), ] ), ] @@ -310,31 +379,35 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_debugger_eval_and_display_url(self): self.run_servoshell(url="data:text/html,") - devtools = self._setup_devtools_client() - console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"]) - evaluation_result = Future() + with Devtools.connect() as devtools: + console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"]) + evaluation_result = Future() - async def on_evaluation_result(data: dict): - evaluation_result.set_result(data) + async def on_evaluation_result(data: dict): + evaluation_result.set_result(data) - devtools.client.add_event_listener(console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result) - console.evaluate_js_async("//# sourceURL=http://test") - evaluation_result.result(1) - self.assert_sources_list(set([tuple([Source("debugger eval", "http://test/")])])) + devtools.client.add_event_listener( + console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result + ) + console.evaluate_js_async("//# sourceURL=http://test") + evaluation_result.result(1) + self.assert_sources_list(set([tuple([Source("debugger eval", "http://test/")])]), devtools=devtools) def test_sources_list_with_debugger_eval_but_no_display_url(self): self.run_servoshell(url="data:text/html,") - devtools = self._setup_devtools_client() - console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"]) - evaluation_result = Future() + with Devtools.connect() as devtools: + console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"]) + evaluation_result = Future() - async def on_evaluation_result(data: dict): - evaluation_result.set_result(data) + async def on_evaluation_result(data: dict): + evaluation_result.set_result(data) - devtools.client.add_event_listener(console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result) - console.evaluate_js_async("1") - evaluation_result.result(1) - self.assert_sources_list(set([tuple([])])) + devtools.client.add_event_listener( + console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result + ) + console.evaluate_js_async("1") + evaluation_result.result(1) + self.assert_sources_list(set([tuple([])]), devtools=devtools) def test_sources_list_with_function_and_display_url(self): self.run_servoshell(url='data:text/html,') @@ -537,24 +610,20 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): self.assert_source_content(Source("inlineScript", f"data:text/html,{script_tag}"), script_tag) def test_source_content_external_script(self): - self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) - self.run_servoshell(url=f'data:text/html,') + self.run_servoshell(url=f'data:text/html,') expected_content = 'console.log("external classic");\n' - self.assert_source_content(Source("srcScript", f"{self.base_urls[0]}/classic.js"), expected_content) + self.assert_source_content(Source("srcScript", f"{self.base_urls[0]}/sources/classic.js"), expected_content) def test_source_content_html_file(self): - self.start_web_server(test_dir=self.get_test_path("sources")) - self.run_servoshell() + self.run_servoshell(url=f"{self.base_urls[0]}/sources/test.html") expected_content = open(self.get_test_path("sources/test.html")).read() - self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/test.html"), expected_content) + self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/sources/test.html"), expected_content) def test_source_content_with_inline_module_import_external(self): - self.start_web_server(test_dir=self.get_test_path("sources_content_with_inline_module_import_external")) - self.run_servoshell() - expected_content = open( - self.get_test_path("sources_content_with_inline_module_import_external/test.html") - ).read() - self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/test.html"), expected_content) + self.run_servoshell(url=f"{self.base_urls[0]}/sources_content_with_inline_module_import_external/test.html") + path = "sources_content_with_inline_module_import_external/test.html" + expected_content = open(self.get_test_path(path)).read() + self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/{path}"), expected_content) # Test case that uses innerHTML and would actually need the HTML parser # (innerHTML has a fast path for values that don’t contain b'&' | b'\0' | b'<' | b'\r') @@ -580,16 +649,16 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): # Test case that uses XMLHttpRequest#responseXML and would actually need the HTML parser # (innerHTML has a fast path for values that don’t contain b'&' | b'\0' | b'<' | b'\r') def test_source_content_inline_script_with_responsexml(self): - self.start_web_server(test_dir=self.get_test_path("sources_content_with_responsexml")) - self.run_servoshell() + self.run_servoshell(url=f"{self.base_urls[0]}/sources_content_with_responsexml/test.html") expected_content = open(self.get_test_path("sources_content_with_responsexml/test.html")).read() - self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/test.html"), expected_content) + self.assert_source_content( + Source("inlineScript", f"{self.base_urls[0]}/sources_content_with_responsexml/test.html"), expected_content + ) def test_source_breakable_lines_and_positions(self): - self.start_web_server(test_dir=self.get_test_path("sources_breakable_lines_and_positions")) - self.run_servoshell() + self.run_servoshell(url=f"{self.base_urls[0]}/sources_breakable_lines_and_positions/test.html") self.assert_source_breakable_lines_and_positions( - Source("inlineScript", f"{self.base_urls[0]}/test.html"), + Source("inlineScript", f"{self.base_urls[0]}/sources_breakable_lines_and_positions/test.html"), [4, 5, 6, 7], { "4": [4, 12, 20, 28], @@ -600,13 +669,14 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): ) # Sets `base_url` and `web_server` and `web_server_thread`. - def start_web_server(self, *, test_dir=None, num_servers=2): - assert self.base_urls is None and self.web_servers is None and self.web_server_threads is None - if test_dir is None: - test_dir = os.path.join(DevtoolsTests.script_path, "devtools_tests") + @classmethod + def setUpClass(cls): + assert cls.base_urls is None and cls.web_servers is None and cls.web_server_threads is None + test_dir = os.path.join(DevtoolsTests.script_path, "devtools_tests") + num_servers = 2 base_urls = [Future() for i in range(num_servers)] - self.web_servers = [None for i in range(num_servers)] - self.web_server_threads = [None for i in range(num_servers)] + cls.web_servers = [None for i in range(num_servers)] + cls.web_server_threads = [None for i in range(num_servers)] class Handler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): @@ -625,165 +695,142 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): web_server = socketserver.TCPServer(("127.0.0.1", 10000 + index), Handler) base_url = f"http://127.0.0.1:{web_server.server_address[1]}" base_urls[index].set_result(base_url) - self.web_servers[index] = web_server + cls.web_servers[index] = web_server web_server.serve_forever() # Start a web server for the test. for index in range(num_servers): thread = Thread(target=server_thread, args=[index]) - self.web_server_threads[index] = thread + cls.web_server_threads[index] = thread thread.start() - self.base_urls = [base_url.result(1) for base_url in base_urls] + cls.base_urls = [base_url.result(1) for base_url in base_urls] # Sets `servoshell`. - def run_servoshell(self, *, url=None): + def run_servoshell(self, *, url): # Change this setting if you want to debug Servo. os.environ["RUST_LOG"] = "error,devtools=warn" # Run servoshell. - if url is None: - url = f"{self.base_urls[0]}/test.html" - self.servoshell = subprocess.Popen([f"target/{self.build_type.directory_name()}/servo", "--devtools=6080", url]) + self.servoshell = subprocess.Popen( + [f"target/{self.build_type.directory_name()}/servo", "--headless", "--devtools=6080", url] + ) - # FIXME: Don’t do this - time.sleep(1) + sleep_per_try = 1 / 8 # seconds + remaining_tries = 5 / sleep_per_try # 5 seconds + while True: + print(".", end="", flush=True) + stream = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + stream.connect(("127.0.0.1", 6080)) + stream.recv(4096) # FIXME: without this, geckordp RDPClient.connect() may fail + stream.shutdown(socket.SHUT_RDWR) + print("+", end="", flush=True) + break + except Exception: + time.sleep(sleep_per_try) + self.assertGreater(remaining_tries, 0) + remaining_tries -= 1 + continue def tearDown(self): - # Terminate servoshell. + # Terminate servoshell, but do not stop the web servers. if self.servoshell is not None: self.servoshell.terminate() self.servoshell = None + @classmethod + def tearDownClass(cls): # Stop the web servers. - if self.web_servers is not None: - for web_server in self.web_servers: + if cls.web_servers is not None: + for web_server in cls.web_servers: web_server.shutdown() web_server.server_close() - self.web_servers = None - if self.web_server_threads is not None: - for web_server_thread in self.web_server_threads: + cls.web_servers = None + if cls.web_server_threads is not None: + for web_server_thread in cls.web_server_threads: web_server_thread.join() - self.web_server_threads = None - if self.base_urls is not None: - self.base_urls = None - - def _setup_devtools_client(self, *, expected_targets=1) -> Devtools: - client = RDPClient() - client.connect("127.0.0.1", 6080) - root = RootActor(client) - tabs = root.list_tabs() - tab_dict = tabs[0] - tab = TabActor(client, tab_dict["actor"]) - watcher = tab.get_watcher() - watcher = WatcherActor(client, watcher["actor"]) - - done = Future() - targets = [] - - def on_target(data): - try: - targets.append(data["target"]) - if len(targets) == expected_targets: - done.set_result(None) - except Exception as e: - # Raising here does nothing, for some reason. - # Send the exception back so it can be raised. - done.set_result(e) - - client.add_event_listener( - watcher.actor_id, - Events.Watcher.TARGET_AVAILABLE_FORM, - on_target, - ) - watcher.watch_targets(WatcherActor.Targets.FRAME) - watcher.watch_targets(WatcherActor.Targets.WORKER) - - result: Optional[Exception] = done.result(1) - if result: - raise result - - return Devtools(client, watcher, targets) + cls.web_server_threads = None + if cls.base_urls is not None: + cls.base_urls = None def assert_sources_list( self, expected_sources_by_target: set[tuple[Source]], *, devtools: Optional[Devtools] = None ): expected_targets = len(expected_sources_by_target) if devtools is None: - devtools = self._setup_devtools_client(expected_targets=expected_targets) - done = Future() - # NOTE: breaks if two targets have the same list of source urls. - # This should really be a multiset, but Python does not have multisets. - actual_sources_by_target: set[tuple[Source]] = set() + devtools = Devtools.connect(expected_targets=expected_targets) + with devtools: + done = Future() + # NOTE: breaks if two targets have the same list of source urls. + # This should really be a multiset, but Python does not have multisets. + actual_sources_by_target: set[tuple[Source]] = set() - def on_source_resource(data): - for [resource_type, sources] in data["array"]: - try: - self.assertEqual(resource_type, "source") - source_urls = tuple([Source(source["introductionType"], source["url"]) for source in sources]) - self.assertFalse(source_urls in actual_sources_by_target) # See NOTE above - actual_sources_by_target.add(source_urls) - if len(actual_sources_by_target) == expected_targets: - done.set_result(None) - except Exception as e: - # Raising here does nothing, for some reason. - # Send the exception back so it can be raised. - done.set_result(e) + def on_source_resource(data): + for [resource_type, sources] in data["array"]: + try: + self.assertEqual(resource_type, "source") + source_urls = tuple([Source(source["introductionType"], source["url"]) for source in sources]) + self.assertFalse(source_urls in actual_sources_by_target) # See NOTE above + actual_sources_by_target.add(source_urls) + if len(actual_sources_by_target) == expected_targets: + done.set_result(None) + except Exception as e: + # Raising here does nothing, for some reason. + # Send the exception back so it can be raised. + done.set_result(e) - for target in devtools.targets: - devtools.client.add_event_listener( - target["actor"], - Events.Watcher.RESOURCES_AVAILABLE_ARRAY, - on_source_resource, - ) - devtools.watcher.watch_resources([Resources.SOURCE]) + for target in devtools.targets: + devtools.client.add_event_listener( + target["actor"], + Events.Watcher.RESOURCES_AVAILABLE_ARRAY, + on_source_resource, + ) + devtools.watcher.watch_resources([Resources.SOURCE]) - result: Optional[Exception] = done.result(1) - if result: - raise result - self.assertEqual(actual_sources_by_target, expected_sources_by_target) - devtools.client.disconnect() + result: Optional[Exception] = done.result(1) + if result: + raise result + self.assertEqual(actual_sources_by_target, expected_sources_by_target) def assert_source_content( self, expected_source: Source, expected_content: str, *, devtools: Optional[Devtools] = None ): if devtools is None: - devtools = self._setup_devtools_client() + devtools = Devtools.connect() + with devtools: + done = Future() + source_actors = {} - done = Future() - source_actors = {} + def on_source_resource(data): + for [resource_type, sources] in data["array"]: + try: + self.assertEqual(resource_type, "source") + for source in sources: + if Source(source["introductionType"], source["url"]) == expected_source: + source_actors[expected_source] = source["actor"] + done.set_result(None) + except Exception as e: + done.set_result(e) - def on_source_resource(data): - for [resource_type, sources] in data["array"]: - try: - self.assertEqual(resource_type, "source") - for source in sources: - if Source(source["introductionType"], source["url"]) == expected_source: - source_actors[expected_source] = source["actor"] - done.set_result(None) - except Exception as e: - done.set_result(e) + for target in devtools.targets: + devtools.client.add_event_listener( + target["actor"], + Events.Watcher.RESOURCES_AVAILABLE_ARRAY, + on_source_resource, + ) + devtools.watcher.watch_resources([Resources.SOURCE]) - for target in devtools.targets: - devtools.client.add_event_listener( - target["actor"], - Events.Watcher.RESOURCES_AVAILABLE_ARRAY, - on_source_resource, - ) - devtools.watcher.watch_resources([Resources.SOURCE]) + result: Optional[Exception] = done.result(1) + if result: + raise result - result: Optional[Exception] = done.result(1) - if result: - raise result + # We found at least one source with the given url. + self.assertIn(expected_source, source_actors) + source_actor = source_actors[expected_source] - # We found at least one source with the given url. - self.assertIn(expected_source, source_actors) - source_actor = source_actors[expected_source] + response = devtools.client.send_receive({"to": source_actor, "type": "source"}) - response = devtools.client.send_receive({"to": source_actor, "type": "source"}) - - self.assertEqual(response["source"], expected_content) - - devtools.client.disconnect() + self.assertEqual(response["source"], expected_content) def assert_source_breakable_lines_and_positions( self, @@ -794,45 +841,43 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): devtools: Optional[Devtools] = None, ): if devtools is None: - devtools = self._setup_devtools_client() + devtools = Devtools.connect() + with devtools: + done = Future() + source_actors = {} - done = Future() - source_actors = {} + def on_source_resource(data): + for [resource_type, sources] in data["array"]: + try: + self.assertEqual(resource_type, "source") + for source in sources: + if Source(source["introductionType"], source["url"]) == expected_source: + source_actors[expected_source] = source["actor"] + done.set_result(None) + except Exception as e: + done.set_result(e) - def on_source_resource(data): - for [resource_type, sources] in data["array"]: - try: - self.assertEqual(resource_type, "source") - for source in sources: - if Source(source["introductionType"], source["url"]) == expected_source: - source_actors[expected_source] = source["actor"] - done.set_result(None) - except Exception as e: - done.set_result(e) + for target in devtools.targets: + devtools.client.add_event_listener( + target["actor"], + Events.Watcher.RESOURCES_AVAILABLE_ARRAY, + on_source_resource, + ) + devtools.watcher.watch_resources([Resources.SOURCE]) - for target in devtools.targets: - devtools.client.add_event_listener( - target["actor"], - Events.Watcher.RESOURCES_AVAILABLE_ARRAY, - on_source_resource, - ) - devtools.watcher.watch_resources([Resources.SOURCE]) + result: Optional[Exception] = done.result(1) + if result: + raise result - result: Optional[Exception] = done.result(1) - if result: - raise result + # We found at least one source with the given url. + self.assertIn(expected_source, source_actors) + source_actor = source_actors[expected_source] - # We found at least one source with the given url. - self.assertIn(expected_source, source_actors) - source_actor = source_actors[expected_source] + response = devtools.client.send_receive({"to": source_actor, "type": "getBreakableLines"}) + self.assertEqual(response["lines"], expected_breakable_lines) - response = devtools.client.send_receive({"to": source_actor, "type": "getBreakableLines"}) - self.assertEqual(response["lines"], expected_breakable_lines) - - response = devtools.client.send_receive({"to": source_actor, "type": "getBreakpointPositionsCompressed"}) - self.assertEqual(response["positions"], expected_positions) - - devtools.client.disconnect() + response = devtools.client.send_receive({"to": source_actor, "type": "getBreakpointPositionsCompressed"}) + self.assertEqual(response["positions"], expected_positions) def get_test_path(self, path: str) -> str: return os.path.join(DevtoolsTests.script_path, os.path.join("devtools_tests", path)) diff --git a/python/servo/devtools_tests/sources/test.html b/python/servo/devtools_tests/sources/test.html index b43d1667aa4..7825e05391a 100644 --- a/python/servo/devtools_tests/sources/test.html +++ b/python/servo/devtools_tests/sources/test.html @@ -8,4 +8,4 @@ import module from "./module.js"; console.log("inline module"); - +