From eaab71d335ddcdae14cd969744be5965c54c97f1 Mon Sep 17 00:00:00 2001 From: atbrakhi Date: Wed, 27 Aug 2025 15:19:58 +0200 Subject: [PATCH] devtools: Fix flaky source list test assertions (#38969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the sources list tests, we assert that the sources for each target are given to us in the same order as we specified in the test case, but this is only true for classic <script> and <script src>. ES module scripts and async/defer scripts are loaded asynchronously, so we can’t rely on the order being the same every time. this patch changes the test assertions to use a frozen multiset for each target’s sources, rather than a frozen list (tuple), so the sources can appear in any order but must still appear the expected number of times. we also change the test assertions to use a multiset ([Counter](https://docs.python.org/3/library/collections.html#counter-objects)) of frozen multisets, rather than a set of multisets, so now two targets can have the same set of sources without breaking tests. Testing: this patch improves existing tests, but does not change coverage Fixes: part of #38658 --------- Signed-off-by: atbrakhi Co-authored-by: delan azabani --- python/servo/devtools_tests.py | 164 ++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 66 deletions(-) diff --git a/python/servo/devtools_tests.py b/python/servo/devtools_tests.py index 159bd21c3ba..d1281ead688 100644 --- a/python/servo/devtools_tests.py +++ b/python/servo/devtools_tests.py @@ -25,14 +25,16 @@ import socketserver import subprocess import time from threading import Thread -from typing import Any, Optional +from typing import Any, Iterable, Optional, TypeVar import unittest +from collections import Counter + # Set this to true to log requests in the internal web servers. LOG_REQUESTS = False -@dataclass(frozen=True) +@dataclass(frozen=True, order=True) class Source: introduction_type: str url: str @@ -113,6 +115,26 @@ class Devtools: self.exited = True +# TODO: Use new syntax in python 3.12 +# +# +T = TypeVar("T") +FrozenMultiset = tuple[tuple[T, int], ...] + + +def frozen_multiset(items: Iterable[T] = []) -> FrozenMultiset[T]: + """ + Simulate a frozen multiset using a tuple of tuples. + Python does not have one yet: + + + """ + # First make a mutable multiset + result = Counter(items) + # then convert it to a tuple with a stable order + return tuple(sorted(result.items())) + + class DevtoolsTests(unittest.IsolatedAsyncioTestCase): # /path/to/servo/python/servo script_path = None @@ -147,9 +169,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list(self): self.run_servoshell(url=f"{self.base_urls[0]}/sources/test.html") self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source("srcScript", f"{self.base_urls[0]}/sources/classic.js"), Source("inlineScript", f"{self.base_urls[0]}/sources/test.html"), @@ -158,51 +180,57 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): Source("importedModule", f"{self.base_urls[0]}/sources/module.js"), ] ), - tuple([Source("Worker", f"{self.base_urls[0]}/sources/classic_worker.js")]), + frozen_multiset([Source("Worker", f"{self.base_urls[0]}/sources/classic_worker.js")]), ] ), ) def test_sources_list_with_data_no_scripts(self): self.run_servoshell(url="data:text/html,") - self.assert_sources_list(set([tuple()])) + self.assert_sources_list(Counter([frozen_multiset()])) # Sources list for `introductionType` = `inlineScript` and `srcScript` def test_sources_list_with_data_empty_inline_classic_script(self): self.run_servoshell(url="data:text/html,") - self.assert_sources_list(set([tuple()])) + self.assert_sources_list(Counter([frozen_multiset()])) def test_sources_list_with_data_inline_classic_script(self): self.run_servoshell(url="data:text/html,") - self.assert_sources_list(set([tuple([Source("inlineScript", "data:text/html,")])])) + self.assert_sources_list( + Counter([frozen_multiset([Source("inlineScript", "data:text/html,")])]) + ) def test_sources_list_with_data_external_classic_script(self): self.run_servoshell(url=f'data:text/html,') - self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/sources/classic.js")])])) + self.assert_sources_list( + Counter([frozen_multiset([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,") - self.assert_sources_list(set([tuple()])) + self.assert_sources_list(Counter([frozen_multiset()])) def test_sources_list_with_data_inline_module_script(self): self.run_servoshell(url="data:text/html,") self.assert_sources_list( - set([tuple([Source("inlineScript", "data:text/html,")])]) + Counter([frozen_multiset([Source("inlineScript", "data:text/html,")])]) ) 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.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/sources/module.js")])])) + self.assert_sources_list( + Counter([frozen_multiset([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.run_servoshell(url=f"{self.base_urls[0]}/sources/test_sources_list_with_static_import_module.html") self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -218,9 +246,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): 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.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -238,9 +266,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): 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.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -248,7 +276,7 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): ), ] ), - tuple( + frozen_multiset( [ Source("Worker", f"{self.base_urls[0]}/sources/classic_worker.js"), ] @@ -260,16 +288,16 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): 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.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", f"{self.base_urls[0]}/sources/test_sources_list_with_module_worker.html" ), ] ), - tuple( + frozen_multiset( [ Source("Worker", f"{self.base_urls[0]}/sources/module_worker.js"), ] @@ -285,9 +313,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): url='data:text/html,' ) self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -303,9 +331,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_injected_script_write_but_no_display_url(self): self.run_servoshell(url='data:text/html,') self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -321,9 +349,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): script = 's=document.createElement("script");s.append("//%23 sourceURL=http://test");document.body.append(s)' self.run_servoshell(url=f"data:text/html,") self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -340,9 +368,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): script = 's=document.createElement("script");s.append("1");document.body.append(s)' self.run_servoshell(url=f"data:text/html,") self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -357,9 +385,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_eval_and_display_url(self): self.run_servoshell(url='data:text/html,') self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", 'data:text/html,' @@ -373,7 +401,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_eval_but_no_display_url(self): self.run_servoshell(url='data:text/html,') - self.assert_sources_list(set([tuple([Source("inlineScript", 'data:text/html,')])])) + self.assert_sources_list( + Counter([frozen_multiset([Source("inlineScript", 'data:text/html,')])]) + ) def test_sources_list_with_debugger_eval_and_display_url(self): self.run_servoshell(url="data:text/html,") @@ -389,7 +419,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): ) console.evaluate_js_async("//# sourceURL=http://test") evaluation_result.result(1) - self.assert_sources_list(set([tuple([Source("debugger eval", "http://test/")])]), devtools=devtools) + self.assert_sources_list( + Counter([frozen_multiset([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,") @@ -405,14 +437,14 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): ) console.evaluate_js_async("1") evaluation_result.result(1) - self.assert_sources_list(set([tuple([])]), devtools=devtools) + self.assert_sources_list(Counter([frozen_multiset([])]), devtools=devtools) def test_sources_list_with_function_and_display_url(self): self.run_servoshell(url='data:text/html,') self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -428,9 +460,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_function_but_no_display_url(self): self.run_servoshell(url='data:text/html,') self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source("inlineScript", 'data:text/html,'), ] @@ -445,9 +477,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): url='data:text/html,' ) self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source( "inlineScript", @@ -462,15 +494,15 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_javascript_url_but_no_display_url(self): self.run_servoshell(url='data:text/html,') - self.assert_sources_list(set([tuple([])])) + self.assert_sources_list(Counter([frozen_multiset([])])) @unittest.expectedFailure def test_sources_list_with_event_handler_and_display_url(self): self.run_servoshell(url='data:text/html,') self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source("eventHandler", "http://test/"), ] @@ -481,15 +513,15 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_event_handler_but_no_display_url(self): self.run_servoshell(url='data:text/html,') - self.assert_sources_list(set([tuple([])])) + self.assert_sources_list(Counter([frozen_multiset([])])) @unittest.expectedFailure def test_sources_list_with_dom_timer_and_display_url(self): self.run_servoshell(url='data:text/html,') self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source("domTimer", "http://test/"), ] @@ -501,16 +533,16 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): @unittest.expectedFailure def test_sources_list_with_dom_timer_but_no_display_url(self): self.run_servoshell(url='data:text/html,') - self.assert_sources_list(set([tuple([])])) + self.assert_sources_list(Counter([frozen_multiset([])])) # 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,") self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source("inlineScript", "http://test/"), ] @@ -523,9 +555,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_inline_script_but_invalid_display_url(self): self.run_servoshell(url="data:text/html,") self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source("inlineScript", "data:text/html,"), ] @@ -537,9 +569,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_inline_script_but_no_display_url(self): self.run_servoshell(url="data:text/html,") self.assert_sources_list( - set( + Counter( [ - tuple( + frozen_multiset( [ Source("inlineScript", "data:text/html,"), ] @@ -554,9 +586,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase): def test_sources_list_with_iframe_srcdoc_and_display_url(self): self.run_servoshell(url='data:text/html,