Revert "ci: Run devtools tests whenever we run unit tests (#38614)"

This reverts commit 47aa9ea8cf.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-08-13 16:03:17 +02:00 committed by Oriol Brufau
parent 18f0d92e99
commit 20ad1ce84e
6 changed files with 233 additions and 288 deletions

View file

@ -190,9 +190,6 @@ jobs:
timeout_minutes: 20 timeout_minutes: 20
max_attempts: 2 # https://github.com/servo/servo/issues/30683 max_attempts: 2 # https://github.com/servo/servo/issues/30683
command: ./mach test-unit --${{ inputs.profile }} command: ./mach test-unit --${{ inputs.profile }}
- name: Devtools tests
if: ${{ inputs.unit-tests }}
run: ./mach test-devtools --${{ inputs.profile }}
- name: Archive build timing - name: Archive build timing
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:

View file

@ -174,9 +174,6 @@ jobs:
timeout_minutes: 40 # https://github.com/servo/servo/issues/30275 timeout_minutes: 40 # https://github.com/servo/servo/issues/30275
max_attempts: 3 # https://github.com/servo/servo/issues/30683 max_attempts: 3 # https://github.com/servo/servo/issues/30683
command: ./mach test-unit --${{ inputs.profile }} command: ./mach test-unit --${{ inputs.profile }}
- name: Devtools tests
if: ${{ inputs.unit-tests }}
run: ./mach test-devtools --${{ inputs.profile }}
- name: Build mach package - name: Build mach package
run: ./mach package --${{ inputs.profile }} run: ./mach package --${{ inputs.profile }}
- name: Run DMG smoketest - name: Run DMG smoketest

View file

@ -187,9 +187,6 @@ jobs:
timeout_minutes: 30 timeout_minutes: 30
max_attempts: 3 # https://github.com/servo/servo/issues/30683 max_attempts: 3 # https://github.com/servo/servo/issues/30683
command: .\mach test-unit --${{ inputs.profile }} -- -- --test-threads=1 command: .\mach test-unit --${{ inputs.profile }} -- -- --test-threads=1
- name: Devtools tests
if: ${{ inputs.unit-tests }}
run: .\mach test-devtools --${{ inputs.profile }}
- name: Archive build timing - name: Archive build timing
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:

View file

@ -5,7 +5,7 @@
//! Low-level wire protocol implementation. Currently only supports //! Low-level wire protocol implementation. Currently only supports
//! [JSON packets](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#json-packets). //! [JSON packets](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#json-packets).
use std::io::{ErrorKind, Read, Write}; use std::io::{Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
use log::debug; use log::debug;
@ -49,8 +49,7 @@ impl JsonPacketStream for TcpStream {
loop { loop {
let mut buf = [0]; let mut buf = [0];
let byte = match self.read(&mut buf) { 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(1) => buf[0],
Ok(_) => unreachable!(), Ok(_) => unreachable!(),
Err(e) => return Err(e.to_string()), Err(e) => return Err(e.to_string()),

View file

@ -7,11 +7,9 @@
# option. This file may not be copied, modified, or distributed # option. This file may not be copied, modified, or distributed
# except according to those terms. # except according to those terms.
from __future__ import annotations
from concurrent.futures import Future from concurrent.futures import Future
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
import socket
from geckordp.actors.root import RootActor from geckordp.actors.root import RootActor
from geckordp.actors.descriptors.tab import TabActor from geckordp.actors.descriptors.tab import TabActor
from geckordp.actors.watcher import WatcherActor from geckordp.actors.watcher import WatcherActor
@ -25,7 +23,7 @@ import socketserver
import subprocess import subprocess
import time import time
from threading import Thread from threading import Thread
from typing import Any, Optional from typing import Optional
import unittest import unittest
from servo.command_base import BuildType from servo.command_base import BuildType
@ -45,96 +43,28 @@ class Devtools:
client: RDPClient client: RDPClient
watcher: WatcherActor watcher: WatcherActor
targets: list 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): class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# /path/to/servo/python/servo # /path/to/servo/python/servo
script_path = None script_path = None
build_type: Optional[BuildType] = None build_type: Optional[BuildType] = None
base_urls = None
web_servers = None
web_server_threads = None
def __init__(self, methodName="runTest"): def __init__(self, methodName="runTest"):
super().__init__(methodName) super().__init__(methodName)
self.servoshell = None self.servoshell = None
self.base_urls = None
self.web_servers = None
self.web_server_threads = None
# Watcher tests # Watcher tests
def test_watcher_returns_same_breakpoint_list_actor_every_time(self): def test_watcher_returns_same_breakpoint_list_actor_every_time(self):
self.run_servoshell(url="data:text/html,") self.run_servoshell(url="data:text/html,")
with Devtools.connect() as devtools: devtools = self._setup_devtools_client()
response1 = devtools.watcher.get_breakpoint_list_actor() response1 = devtools.watcher.get_breakpoint_list_actor()
response2 = devtools.watcher.get_breakpoint_list_actor() response2 = devtools.watcher.get_breakpoint_list_actor()
self.assertEqual(response1["breakpointList"]["actor"], response2["breakpointList"]["actor"]) self.assertEqual(response1["breakpointList"]["actor"], response2["breakpointList"]["actor"])
# Sources list # Sources list
# Classic script vs module script: # Classic script vs module script:
@ -147,20 +77,22 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# Worker script sources can be external or blob. # Worker script sources can be external or blob.
def test_sources_list(self): def test_sources_list(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources/test.html") self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell()
self.assert_sources_list( self.assert_sources_list(
set( set(
[ [
# TODO: update expectations when we fix ES modules
tuple( tuple(
[ [
Source("srcScript", f"{self.base_urls[0]}/sources/classic.js"), Source("srcScript", f"{self.base_urls[0]}/classic.js"),
Source("inlineScript", f"{self.base_urls[0]}/sources/test.html"), Source("inlineScript", f"{self.base_urls[0]}/test.html"),
Source("inlineScript", f"{self.base_urls[0]}/sources/test.html"), Source("inlineScript", f"{self.base_urls[0]}/test.html"),
Source("srcScript", f"{self.base_urls[1]}/sources/classic.js"), Source("srcScript", f"{self.base_urls[1]}/classic.js"),
Source("importedModule", f"{self.base_urls[0]}/sources/module.js"), Source("importedModule", f"{self.base_urls[0]}/module.js"),
] ]
), ),
tuple([Source("Worker", f"{self.base_urls[0]}/sources/classic_worker.js")]), tuple([Source("Worker", f"{self.base_urls[0]}/classic_worker.js")]),
] ]
), ),
) )
@ -180,8 +112,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
self.assert_sources_list(set([tuple([Source("inlineScript", "data:text/html,<script>;</script>")])])) self.assert_sources_list(set([tuple([Source("inlineScript", "data:text/html,<script>;</script>")])]))
def test_sources_list_with_data_external_classic_script(self): def test_sources_list_with_data_external_classic_script(self):
self.run_servoshell(url=f'data:text/html,<script src="{self.base_urls[0]}/sources/classic.js"></script>') self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/sources/classic.js")])])) self.run_servoshell(url=f'data:text/html,<script src="{self.base_urls[0]}/classic.js"></script>')
self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/classic.js")])]))
def test_sources_list_with_data_empty_inline_module_script(self): def test_sources_list_with_data_empty_inline_module_script(self):
self.run_servoshell(url="data:text/html,<script type=module></script>") self.run_servoshell(url="data:text/html,<script type=module></script>")
@ -194,23 +127,24 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
) )
def test_sources_list_with_data_external_module_script(self): def test_sources_list_with_data_external_module_script(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_data_external_module_script.html") self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/sources/module.js")])])) 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")])]))
# Sources list for `introductionType` = `importedModule` # Sources list for `introductionType` = `importedModule`
def test_sources_list_with_static_import_module(self): def test_sources_list_with_static_import_module(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_static_import_module.html") 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.assert_sources_list( self.assert_sources_list(
set( set(
[ [
tuple( tuple(
[ [
Source( Source(
"inlineScript", "inlineScript", f"{self.base_urls[0]}/test_sources_list_with_static_import_module.html"
f"{self.base_urls[0]}/sources/test_sources_list_with_static_import_module.html",
), ),
Source("importedModule", f"{self.base_urls[0]}/sources/module.js"), Source("importedModule", f"{self.base_urls[0]}/module.js"),
] ]
) )
] ]
@ -218,17 +152,17 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
) )
def test_sources_list_with_dynamic_import_module(self): def test_sources_list_with_dynamic_import_module(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_dynamic_import_module.html") 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.assert_sources_list( self.assert_sources_list(
set( set(
[ [
tuple( tuple(
[ [
Source( Source(
"inlineScript", "inlineScript", f"{self.base_urls[0]}/test_sources_list_with_dynamic_import_module.html"
f"{self.base_urls[0]}/sources/test_sources_list_with_dynamic_import_module.html",
), ),
Source("importedModule", f"{self.base_urls[0]}/sources/module.js"), Source("importedModule", f"{self.base_urls[0]}/module.js"),
] ]
) )
] ]
@ -238,21 +172,19 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# Sources list for `introductionType` = `Worker` # Sources list for `introductionType` = `Worker`
def test_sources_list_with_classic_worker(self): def test_sources_list_with_classic_worker(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_classic_worker.html") 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.assert_sources_list( self.assert_sources_list(
set( set(
[ [
tuple( tuple(
[ [
Source( Source("inlineScript", f"{self.base_urls[0]}/test_sources_list_with_classic_worker.html"),
"inlineScript",
f"{self.base_urls[0]}/sources/test_sources_list_with_classic_worker.html",
),
] ]
), ),
tuple( tuple(
[ [
Source("Worker", f"{self.base_urls[0]}/sources/classic_worker.js"), Source("Worker", f"{self.base_urls[0]}/classic_worker.js"),
] ]
), ),
] ]
@ -260,20 +192,19 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
) )
def test_sources_list_with_module_worker(self): def test_sources_list_with_module_worker(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_module_worker.html") 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.assert_sources_list( self.assert_sources_list(
set( set(
[ [
tuple( tuple(
[ [
Source( Source("inlineScript", f"{self.base_urls[0]}/test_sources_list_with_module_worker.html"),
"inlineScript", f"{self.base_urls[0]}/sources/test_sources_list_with_module_worker.html"
),
] ]
), ),
tuple( tuple(
[ [
Source("Worker", f"{self.base_urls[0]}/sources/module_worker.js"), Source("Worker", f"{self.base_urls[0]}/module_worker.js"),
] ]
), ),
] ]
@ -379,35 +310,31 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
def test_sources_list_with_debugger_eval_and_display_url(self): def test_sources_list_with_debugger_eval_and_display_url(self):
self.run_servoshell(url="data:text/html,") self.run_servoshell(url="data:text/html,")
with Devtools.connect() as devtools: devtools = self._setup_devtools_client()
console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"]) console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"])
evaluation_result = Future() evaluation_result = Future()
async def on_evaluation_result(data: dict): async def on_evaluation_result(data: dict):
evaluation_result.set_result(data) evaluation_result.set_result(data)
devtools.client.add_event_listener( devtools.client.add_event_listener(console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result)
console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result console.evaluate_js_async("//# sourceURL=http://test")
) evaluation_result.result(1)
console.evaluate_js_async("//# sourceURL=http://test") self.assert_sources_list(set([tuple([Source("debugger eval", "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): def test_sources_list_with_debugger_eval_but_no_display_url(self):
self.run_servoshell(url="data:text/html,") self.run_servoshell(url="data:text/html,")
with Devtools.connect() as devtools: devtools = self._setup_devtools_client()
console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"]) console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"])
evaluation_result = Future() evaluation_result = Future()
async def on_evaluation_result(data: dict): async def on_evaluation_result(data: dict):
evaluation_result.set_result(data) evaluation_result.set_result(data)
devtools.client.add_event_listener( devtools.client.add_event_listener(console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result)
console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result console.evaluate_js_async("1")
) evaluation_result.result(1)
console.evaluate_js_async("1") self.assert_sources_list(set([tuple([])]))
evaluation_result.result(1)
self.assert_sources_list(set([tuple([])]), devtools=devtools)
def test_sources_list_with_function_and_display_url(self): def test_sources_list_with_function_and_display_url(self):
self.run_servoshell(url='data:text/html,<script>new Function("//%23 sourceURL=http://test")</script>') self.run_servoshell(url='data:text/html,<script>new Function("//%23 sourceURL=http://test")</script>')
@ -610,20 +537,24 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
self.assert_source_content(Source("inlineScript", f"data:text/html,{script_tag}"), script_tag) self.assert_source_content(Source("inlineScript", f"data:text/html,{script_tag}"), script_tag)
def test_source_content_external_script(self): def test_source_content_external_script(self):
self.run_servoshell(url=f'data:text/html,<script src="{self.base_urls[0]}/sources/classic.js"></script>') self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f'data:text/html,<script src="{self.base_urls[0]}/classic.js"></script>')
expected_content = 'console.log("external classic");\n' expected_content = 'console.log("external classic");\n'
self.assert_source_content(Source("srcScript", f"{self.base_urls[0]}/sources/classic.js"), expected_content) self.assert_source_content(Source("srcScript", f"{self.base_urls[0]}/classic.js"), expected_content)
def test_source_content_html_file(self): def test_source_content_html_file(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources/test.html") self.start_web_server(test_dir=self.get_test_path("sources"))
self.run_servoshell()
expected_content = open(self.get_test_path("sources/test.html")).read() expected_content = open(self.get_test_path("sources/test.html")).read()
self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/sources/test.html"), expected_content) self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/test.html"), expected_content)
def test_source_content_with_inline_module_import_external(self): def test_source_content_with_inline_module_import_external(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources_content_with_inline_module_import_external/test.html") self.start_web_server(test_dir=self.get_test_path("sources_content_with_inline_module_import_external"))
path = "sources_content_with_inline_module_import_external/test.html" self.run_servoshell()
expected_content = open(self.get_test_path(path)).read() expected_content = open(
self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/{path}"), expected_content) 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)
# Test case that uses innerHTML and would actually need the HTML parser # Test case that uses innerHTML and would actually need the HTML parser
# (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r') # (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r')
@ -649,16 +580,16 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# Test case that uses XMLHttpRequest#responseXML and would actually need the HTML parser # Test case that uses XMLHttpRequest#responseXML and would actually need the HTML parser
# (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r') # (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r')
def test_source_content_inline_script_with_responsexml(self): def test_source_content_inline_script_with_responsexml(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources_content_with_responsexml/test.html") self.start_web_server(test_dir=self.get_test_path("sources_content_with_responsexml"))
self.run_servoshell()
expected_content = open(self.get_test_path("sources_content_with_responsexml/test.html")).read() expected_content = open(self.get_test_path("sources_content_with_responsexml/test.html")).read()
self.assert_source_content( self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/test.html"), expected_content)
Source("inlineScript", f"{self.base_urls[0]}/sources_content_with_responsexml/test.html"), expected_content
)
def test_source_breakable_lines_and_positions(self): def test_source_breakable_lines_and_positions(self):
self.run_servoshell(url=f"{self.base_urls[0]}/sources_breakable_lines_and_positions/test.html") self.start_web_server(test_dir=self.get_test_path("sources_breakable_lines_and_positions"))
self.run_servoshell()
self.assert_source_breakable_lines_and_positions( self.assert_source_breakable_lines_and_positions(
Source("inlineScript", f"{self.base_urls[0]}/sources_breakable_lines_and_positions/test.html"), Source("inlineScript", f"{self.base_urls[0]}/test.html"),
[4, 5, 6, 7], [4, 5, 6, 7],
{ {
"4": [4, 12, 20, 28], "4": [4, 12, 20, 28],
@ -669,14 +600,13 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
) )
# Sets `base_url` and `web_server` and `web_server_thread`. # Sets `base_url` and `web_server` and `web_server_thread`.
@classmethod def start_web_server(self, *, test_dir=None, num_servers=2):
def setUpClass(cls): assert self.base_urls is None and self.web_servers is None and self.web_server_threads is None
assert cls.base_urls is None and cls.web_servers is None and cls.web_server_threads is None if test_dir is None:
test_dir = os.path.join(DevtoolsTests.script_path, "devtools_tests") test_dir = os.path.join(DevtoolsTests.script_path, "devtools_tests")
num_servers = 2
base_urls = [Future() for i in range(num_servers)] base_urls = [Future() for i in range(num_servers)]
cls.web_servers = [None for i in range(num_servers)] self.web_servers = [None for i in range(num_servers)]
cls.web_server_threads = [None for i in range(num_servers)] self.web_server_threads = [None for i in range(num_servers)]
class Handler(http.server.SimpleHTTPRequestHandler): class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -695,142 +625,165 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
web_server = socketserver.TCPServer(("127.0.0.1", 10000 + index), Handler) 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_url = f"http://127.0.0.1:{web_server.server_address[1]}"
base_urls[index].set_result(base_url) base_urls[index].set_result(base_url)
cls.web_servers[index] = web_server self.web_servers[index] = web_server
web_server.serve_forever() web_server.serve_forever()
# Start a web server for the test. # Start a web server for the test.
for index in range(num_servers): for index in range(num_servers):
thread = Thread(target=server_thread, args=[index]) thread = Thread(target=server_thread, args=[index])
cls.web_server_threads[index] = thread self.web_server_threads[index] = thread
thread.start() thread.start()
cls.base_urls = [base_url.result(1) for base_url in base_urls] self.base_urls = [base_url.result(1) for base_url in base_urls]
# Sets `servoshell`. # Sets `servoshell`.
def run_servoshell(self, *, url): def run_servoshell(self, *, url=None):
# Change this setting if you want to debug Servo. # Change this setting if you want to debug Servo.
os.environ["RUST_LOG"] = "error,devtools=warn" os.environ["RUST_LOG"] = "error,devtools=warn"
# Run servoshell. # Run servoshell.
self.servoshell = subprocess.Popen( if url is None:
[f"target/{self.build_type.directory_name()}/servo", "--headless", "--devtools=6080", url] url = f"{self.base_urls[0]}/test.html"
) self.servoshell = subprocess.Popen([f"target/{self.build_type.directory_name()}/servo", "--devtools=6080", url])
sleep_per_try = 1 / 8 # seconds # FIXME: Dont do this
remaining_tries = 5 / sleep_per_try # 5 seconds time.sleep(1)
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): def tearDown(self):
# Terminate servoshell, but do not stop the web servers. # Terminate servoshell.
if self.servoshell is not None: if self.servoshell is not None:
self.servoshell.terminate() self.servoshell.terminate()
self.servoshell = None self.servoshell = None
@classmethod
def tearDownClass(cls):
# Stop the web servers. # Stop the web servers.
if cls.web_servers is not None: if self.web_servers is not None:
for web_server in cls.web_servers: for web_server in self.web_servers:
web_server.shutdown() web_server.shutdown()
web_server.server_close() web_server.server_close()
cls.web_servers = None self.web_servers = None
if cls.web_server_threads is not None: if self.web_server_threads is not None:
for web_server_thread in cls.web_server_threads: for web_server_thread in self.web_server_threads:
web_server_thread.join() web_server_thread.join()
cls.web_server_threads = None self.web_server_threads = None
if cls.base_urls is not None: if self.base_urls is not None:
cls.base_urls = 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)
def assert_sources_list( def assert_sources_list(
self, expected_sources_by_target: set[tuple[Source]], *, devtools: Optional[Devtools] = None self, expected_sources_by_target: set[tuple[Source]], *, devtools: Optional[Devtools] = None
): ):
expected_targets = len(expected_sources_by_target) expected_targets = len(expected_sources_by_target)
if devtools is None: if devtools is None:
devtools = Devtools.connect(expected_targets=expected_targets) devtools = self._setup_devtools_client(expected_targets=expected_targets)
with devtools: done = Future()
done = Future() # NOTE: breaks if two targets have the same list of source urls.
# NOTE: breaks if two targets have the same list of source urls. # This should really be a multiset, but Python does not have multisets.
# This should really be a multiset, but Python does not have multisets. actual_sources_by_target: set[tuple[Source]] = set()
actual_sources_by_target: set[tuple[Source]] = set()
def on_source_resource(data): def on_source_resource(data):
for [resource_type, sources] in data["array"]: for [resource_type, sources] in data["array"]:
try: try:
self.assertEqual(resource_type, "source") self.assertEqual(resource_type, "source")
source_urls = tuple([Source(source["introductionType"], source["url"]) for source in sources]) source_urls = tuple([Source(source["introductionType"], source["url"]) for source in sources])
self.assertFalse(source_urls in actual_sources_by_target) # See NOTE above self.assertFalse(source_urls in actual_sources_by_target) # See NOTE above
actual_sources_by_target.add(source_urls) actual_sources_by_target.add(source_urls)
if len(actual_sources_by_target) == expected_targets: if len(actual_sources_by_target) == expected_targets:
done.set_result(None) done.set_result(None)
except Exception as e: except Exception as e:
# Raising here does nothing, for some reason. # Raising here does nothing, for some reason.
# Send the exception back so it can be raised. # Send the exception back so it can be raised.
done.set_result(e) done.set_result(e)
for target in devtools.targets: for target in devtools.targets:
devtools.client.add_event_listener( devtools.client.add_event_listener(
target["actor"], target["actor"],
Events.Watcher.RESOURCES_AVAILABLE_ARRAY, Events.Watcher.RESOURCES_AVAILABLE_ARRAY,
on_source_resource, on_source_resource,
) )
devtools.watcher.watch_resources([Resources.SOURCE]) devtools.watcher.watch_resources([Resources.SOURCE])
result: Optional[Exception] = done.result(1) result: Optional[Exception] = done.result(1)
if result: if result:
raise result raise result
self.assertEqual(actual_sources_by_target, expected_sources_by_target) self.assertEqual(actual_sources_by_target, expected_sources_by_target)
devtools.client.disconnect()
def assert_source_content( def assert_source_content(
self, expected_source: Source, expected_content: str, *, devtools: Optional[Devtools] = None self, expected_source: Source, expected_content: str, *, devtools: Optional[Devtools] = None
): ):
if devtools is None: if devtools is None:
devtools = Devtools.connect() devtools = self._setup_devtools_client()
with devtools:
done = Future()
source_actors = {}
def on_source_resource(data): done = Future()
for [resource_type, sources] in data["array"]: source_actors = {}
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: def on_source_resource(data):
devtools.client.add_event_listener( for [resource_type, sources] in data["array"]:
target["actor"], try:
Events.Watcher.RESOURCES_AVAILABLE_ARRAY, self.assertEqual(resource_type, "source")
on_source_resource, for source in sources:
) if Source(source["introductionType"], source["url"]) == expected_source:
devtools.watcher.watch_resources([Resources.SOURCE]) source_actors[expected_source] = source["actor"]
done.set_result(None)
except Exception as e:
done.set_result(e)
result: Optional[Exception] = done.result(1) for target in devtools.targets:
if result: devtools.client.add_event_listener(
raise result target["actor"],
Events.Watcher.RESOURCES_AVAILABLE_ARRAY,
on_source_resource,
)
devtools.watcher.watch_resources([Resources.SOURCE])
# We found at least one source with the given url. result: Optional[Exception] = done.result(1)
self.assertIn(expected_source, source_actors) if result:
source_actor = source_actors[expected_source] raise result
response = devtools.client.send_receive({"to": source_actor, "type": "source"}) # We found at least one source with the given url.
self.assertIn(expected_source, source_actors)
source_actor = source_actors[expected_source]
self.assertEqual(response["source"], expected_content) response = devtools.client.send_receive({"to": source_actor, "type": "source"})
self.assertEqual(response["source"], expected_content)
devtools.client.disconnect()
def assert_source_breakable_lines_and_positions( def assert_source_breakable_lines_and_positions(
self, self,
@ -841,43 +794,45 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
devtools: Optional[Devtools] = None, devtools: Optional[Devtools] = None,
): ):
if devtools is None: if devtools is None:
devtools = Devtools.connect() devtools = self._setup_devtools_client()
with devtools:
done = Future()
source_actors = {}
def on_source_resource(data): done = Future()
for [resource_type, sources] in data["array"]: source_actors = {}
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: def on_source_resource(data):
devtools.client.add_event_listener( for [resource_type, sources] in data["array"]:
target["actor"], try:
Events.Watcher.RESOURCES_AVAILABLE_ARRAY, self.assertEqual(resource_type, "source")
on_source_resource, for source in sources:
) if Source(source["introductionType"], source["url"]) == expected_source:
devtools.watcher.watch_resources([Resources.SOURCE]) source_actors[expected_source] = source["actor"]
done.set_result(None)
except Exception as e:
done.set_result(e)
result: Optional[Exception] = done.result(1) for target in devtools.targets:
if result: devtools.client.add_event_listener(
raise result target["actor"],
Events.Watcher.RESOURCES_AVAILABLE_ARRAY,
on_source_resource,
)
devtools.watcher.watch_resources([Resources.SOURCE])
# We found at least one source with the given url. result: Optional[Exception] = done.result(1)
self.assertIn(expected_source, source_actors) if result:
source_actor = source_actors[expected_source] raise result
response = devtools.client.send_receive({"to": source_actor, "type": "getBreakableLines"}) # We found at least one source with the given url.
self.assertEqual(response["lines"], expected_breakable_lines) self.assertIn(expected_source, source_actors)
source_actor = source_actors[expected_source]
response = devtools.client.send_receive({"to": source_actor, "type": "getBreakpointPositionsCompressed"}) response = devtools.client.send_receive({"to": source_actor, "type": "getBreakableLines"})
self.assertEqual(response["positions"], expected_positions) 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()
def get_test_path(self, path: str) -> str: def get_test_path(self, path: str) -> str:
return os.path.join(DevtoolsTests.script_path, os.path.join("devtools_tests", path)) return os.path.join(DevtoolsTests.script_path, os.path.join("devtools_tests", path))

View file

@ -8,4 +8,4 @@
import module from "./module.js"; import module from "./module.js";
console.log("inline module"); console.log("inline module");
</script> </script>
<script src="http://127.0.0.1:10001/sources/classic.js"></script> <script src="http://127.0.0.1:10001/classic.js"></script>