mirror of
https://github.com/servo/servo.git
synced 2025-09-19 11:20:09 +01:00
devtools: Create source actors from Debugger API notifications (#38334)
currently our devtools impl creates source actors in script, when executing scripts in HTMLScriptElement or DedicatedWorkerGlobalScope. this approach is cumbersome, and it means that many pathways to running scripts are missed, such as imported ES modules. with the [SpiderMonkey Debugger API](https://firefox-source-docs.mozilla.org/js/Debugger/), we can pick up all of the scripts and all of their sources without any extra code, as long as we tell it about every global we create (#38333, #38551). this patch adds a [Debugger#onNewScript() hook](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.html#onnewscript-script-global) to the debugger script, which calls DebuggerGlobalScope#notifyNewSource() to notify our script system when a new script runs. if the source is relevant to the file tree in the Sources tab, script tells devtools to create a source actor. Testing: adds several new automated devtools tests Fixes: part of #36027 Signed-off-by: Delan Azabani <dazabani@igalia.com> Co-authored-by: atbrakhi <atbrakhi@igalia.com>
This commit is contained in:
parent
de73d4a25c
commit
4784668fa9
10 changed files with 522 additions and 118 deletions
|
@ -13,6 +13,7 @@ import logging
|
|||
from geckordp.actors.root import RootActor
|
||||
from geckordp.actors.descriptors.tab import TabActor
|
||||
from geckordp.actors.watcher import WatcherActor
|
||||
from geckordp.actors.web_console import WebConsoleActor
|
||||
from geckordp.actors.resources import Resources
|
||||
from geckordp.actors.events import Events
|
||||
from geckordp.rdp_client import RDPClient
|
||||
|
@ -37,6 +38,13 @@ class Source:
|
|||
url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Devtools:
|
||||
client: RDPClient
|
||||
watcher: WatcherActor
|
||||
targets: list
|
||||
|
||||
|
||||
class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
||||
# /path/to/servo/python/servo
|
||||
script_path = None
|
||||
|
@ -71,8 +79,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
[
|
||||
Source("srcScript", f"{self.base_urls[0]}/classic.js"),
|
||||
Source("inlineScript", f"{self.base_urls[0]}/test.html"),
|
||||
Source("srcScript", f"{self.base_urls[1]}/classic.js"),
|
||||
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"),
|
||||
]
|
||||
),
|
||||
tuple([Source("Worker", f"{self.base_urls[0]}/classic_worker.js")]),
|
||||
|
@ -116,7 +125,6 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
|
||||
# Sources list for `introductionType` = `importedModule`
|
||||
|
||||
@unittest.expectedFailure
|
||||
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")
|
||||
|
@ -135,7 +143,6 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
),
|
||||
)
|
||||
|
||||
@unittest.expectedFailure
|
||||
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")
|
||||
|
@ -196,6 +203,324 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
),
|
||||
)
|
||||
|
||||
# Sources list for `introductionType` set to values that require `displayURL` (`//# sourceURL`)
|
||||
|
||||
def test_sources_list_with_injected_script_write_and_display_url(self):
|
||||
self.run_servoshell(
|
||||
url='data:text/html,<script>document.write("<script>//%23 sourceURL=http://test</scr"+"ipt>")</script>'
|
||||
)
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source(
|
||||
"inlineScript",
|
||||
'data:text/html,<script>document.write("<script>//%23 sourceURL=http://test</scr"+"ipt>")</script>',
|
||||
),
|
||||
Source("injectedScript", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_injected_script_write_but_no_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<script>document.write("<script>1</scr"+"ipt>")</script>')
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source(
|
||||
"inlineScript",
|
||||
'data:text/html,<script>document.write("<script>1</scr"+"ipt>")</script>',
|
||||
),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_injected_script_append_and_display_url(self):
|
||||
script = 's=document.createElement("script");s.append("//%23 sourceURL=http://test");document.body.append(s)'
|
||||
self.run_servoshell(url=f"data:text/html,<body><script>{script}</script>")
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source(
|
||||
"inlineScript",
|
||||
f"data:text/html,<body><script>{script}</script>",
|
||||
),
|
||||
Source("injectedScript", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_injected_script_append_but_no_display_url(self):
|
||||
script = 's=document.createElement("script");s.append("1");document.body.append(s)'
|
||||
self.run_servoshell(url=f"data:text/html,<body><script>{script}</script>")
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source(
|
||||
"inlineScript",
|
||||
f"data:text/html,<body><script>{script}</script>",
|
||||
),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_eval_and_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<script>eval("//%23 sourceURL=http://test")</script>')
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source(
|
||||
"inlineScript", 'data:text/html,<script>eval("//%23 sourceURL=http://test")</script>'
|
||||
),
|
||||
Source("eval", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_eval_but_no_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<script>eval("1")</script>')
|
||||
self.assert_sources_list(set([tuple([Source("inlineScript", 'data:text/html,<script>eval("1")</script>')])]))
|
||||
|
||||
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()
|
||||
|
||||
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/")])]))
|
||||
|
||||
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()
|
||||
|
||||
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([])]))
|
||||
|
||||
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.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source(
|
||||
"inlineScript",
|
||||
'data:text/html,<script>new Function("//%23 sourceURL=http://test")</script>',
|
||||
),
|
||||
Source("Function", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_function_but_no_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<script>new Function("1")</script>')
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source("inlineScript", 'data:text/html,<script>new Function("1")</script>'),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_javascript_url_and_display_url(self):
|
||||
# “1” prefix is a workaround for <https://github.com/servo/servo/issues/38547>
|
||||
self.run_servoshell(
|
||||
url='data:text/html,<a href="javascript:1//%23 sourceURL=http://test"></a><script>document.querySelector("a").click()</script>'
|
||||
)
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source(
|
||||
"inlineScript",
|
||||
'data:text/html,<a href="javascript:1//%23 sourceURL=http://test"></a><script>document.querySelector("a").click()</script>',
|
||||
),
|
||||
Source("javascriptURL", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_javascript_url_but_no_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<a href="javascript:1"></a>')
|
||||
self.assert_sources_list(set([tuple([])]))
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_sources_list_with_event_handler_and_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<a onclick="//%23 sourceURL=http://test"></a>')
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source("eventHandler", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_event_handler_but_no_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<a onclick="1"></a>')
|
||||
self.assert_sources_list(set([tuple([])]))
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_sources_list_with_dom_timer_and_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<script>setTimeout("//%23 sourceURL=http://test",0)</script>')
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source("domTimer", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_sources_list_with_dom_timer_but_no_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<script>setTimeout("1",0)</script>')
|
||||
self.assert_sources_list(set([tuple([])]))
|
||||
|
||||
# Sources list for scripts with `displayURL` (`//# sourceURL`), despite not being required by `introductionType`
|
||||
|
||||
def test_sources_list_with_inline_script_and_display_url(self):
|
||||
self.run_servoshell(url="data:text/html,<script>//%23 sourceURL=http://test</script>")
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source("inlineScript", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Extra test case for situation where `//# sourceURL` can’t be parsed with page url as base.
|
||||
def test_sources_list_with_inline_script_but_invalid_display_url(self):
|
||||
self.run_servoshell(url="data:text/html,<script>//%23 sourceURL=test</script>")
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source("inlineScript", "data:text/html,<script>//%23 sourceURL=test</script>"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_sources_list_with_inline_script_but_no_display_url(self):
|
||||
self.run_servoshell(url="data:text/html,<script>1</script>")
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source("inlineScript", "data:text/html,<script>1</script>"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Sources list for inline scripts in `<iframe srcdoc>`
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_sources_list_with_iframe_srcdoc_and_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<iframe srcdoc="<script>//%23 sourceURL=http://test</script>">')
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source("inlineScript", "http://test/"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_sources_list_with_iframe_srcdoc_but_no_display_url(self):
|
||||
self.run_servoshell(url='data:text/html,<iframe srcdoc="<script>1</script>">')
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
# FIXME: it’s not really gonna be 0
|
||||
Source("inlineScript", "about:srcdoc#0"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_sources_list_with_iframe_srcdoc_multiple_inline_scripts(self):
|
||||
self.run_servoshell(
|
||||
url='data:text/html,<iframe srcdoc="<script>//%23 sourceURL=http://test</script><script>2</script>">'
|
||||
)
|
||||
self.assert_sources_list(
|
||||
set(
|
||||
[
|
||||
tuple(
|
||||
[
|
||||
Source("inlineScript", "http://test/"),
|
||||
# FIXME: it’s not really gonna be 0
|
||||
Source("inlineScript", "about:srcdoc#0"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Source contents
|
||||
|
||||
def test_source_content_inline_script(self):
|
||||
|
@ -320,7 +645,7 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
if self.base_urls is not None:
|
||||
self.base_urls = None
|
||||
|
||||
def _setup_devtools_client(self, expected_targets=1):
|
||||
def _setup_devtools_client(self, *, expected_targets=1) -> Devtools:
|
||||
client = RDPClient()
|
||||
client.connect("127.0.0.1", 6080)
|
||||
root = RootActor(client)
|
||||
|
@ -355,11 +680,14 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
if result:
|
||||
raise result
|
||||
|
||||
return client, watcher, targets
|
||||
return Devtools(client, watcher, targets)
|
||||
|
||||
def assert_sources_list(self, expected_sources_by_target: set[tuple[Source]]):
|
||||
def assert_sources_list(
|
||||
self, expected_sources_by_target: set[tuple[Source]], *, devtools: Optional[Devtools] = None
|
||||
):
|
||||
expected_targets = len(expected_sources_by_target)
|
||||
client, watcher, targets = self._setup_devtools_client(expected_targets)
|
||||
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.
|
||||
|
@ -379,22 +707,25 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
# Send the exception back so it can be raised.
|
||||
done.set_result(e)
|
||||
|
||||
for target in targets:
|
||||
client.add_event_listener(
|
||||
for target in devtools.targets:
|
||||
devtools.client.add_event_listener(
|
||||
target["actor"],
|
||||
Events.Watcher.RESOURCES_AVAILABLE_ARRAY,
|
||||
on_source_resource,
|
||||
)
|
||||
watcher.watch_resources([Resources.SOURCE])
|
||||
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)
|
||||
client.disconnect()
|
||||
devtools.client.disconnect()
|
||||
|
||||
def assert_source_content(self, expected_source: Source, expected_content: str):
|
||||
client, watcher, targets = self._setup_devtools_client()
|
||||
def assert_source_content(
|
||||
self, expected_source: Source, expected_content: str, *, devtools: Optional[Devtools] = None
|
||||
):
|
||||
if devtools is None:
|
||||
devtools = self._setup_devtools_client()
|
||||
|
||||
done = Future()
|
||||
source_actors = {}
|
||||
|
@ -410,13 +741,13 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
except Exception as e:
|
||||
done.set_result(e)
|
||||
|
||||
for target in targets:
|
||||
client.add_event_listener(
|
||||
for target in devtools.targets:
|
||||
devtools.client.add_event_listener(
|
||||
target["actor"],
|
||||
Events.Watcher.RESOURCES_AVAILABLE_ARRAY,
|
||||
on_source_resource,
|
||||
)
|
||||
watcher.watch_resources([Resources.SOURCE])
|
||||
devtools.watcher.watch_resources([Resources.SOURCE])
|
||||
|
||||
result: Optional[Exception] = done.result(1)
|
||||
if result:
|
||||
|
@ -426,11 +757,11 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertIn(expected_source, source_actors)
|
||||
source_actor = source_actors[expected_source]
|
||||
|
||||
response = client.send_receive({"to": source_actor, "type": "source"})
|
||||
response = devtools.client.send_receive({"to": source_actor, "type": "source"})
|
||||
|
||||
self.assertEqual(response["source"], expected_content)
|
||||
|
||||
client.disconnect()
|
||||
devtools.client.disconnect()
|
||||
|
||||
def get_test_path(self, path: str) -> str:
|
||||
return os.path.join(DevtoolsTests.script_path, os.path.join("devtools_tests", path))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue