import-wpt-test.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. #!/usr/bin/env python3
  2. from collections import namedtuple
  3. from dataclasses import dataclass
  4. from enum import Enum
  5. from html.parser import HTMLParser
  6. from pathlib import Path
  7. from urllib.parse import urljoin, urlparse
  8. from urllib.request import urlopen
  9. import re
  10. import os
  11. import sys
  12. wpt_base_url = 'https://wpt.live/'
  13. class TestType(Enum):
  14. TEXT = 1, 'Tests/LibWeb/Text/input/wpt-import', 'Tests/LibWeb/Text/expected/wpt-import'
  15. REF = 2, 'Tests/LibWeb/Ref/input/wpt-import', 'Tests/LibWeb/Ref/expected/wpt-import'
  16. CRASH = 3, 'Tests/LibWeb/Crash/wpt-import', ''
  17. def __new__(cls, *args, **kwds):
  18. obj = object.__new__(cls)
  19. obj._value_ = args[0]
  20. return obj
  21. def __init__(self, _: str, input_path: str, expected_path: str):
  22. self.input_path = input_path
  23. self.expected_path = expected_path
  24. PathMapping = namedtuple('PathMapping', ['source', 'destination'])
  25. class ResourceType(Enum):
  26. INPUT = 1
  27. EXPECTED = 2
  28. @dataclass
  29. class ResourceAndType:
  30. resource: str
  31. type: ResourceType
  32. test_type = TestType.TEXT
  33. raw_reference_path = None # As specified in the test HTML
  34. reference_path = None # With parent directories
  35. class LinkedResourceFinder(HTMLParser):
  36. def __init__(self):
  37. super().__init__()
  38. self._tag_stack_ = []
  39. self._match_css_url_ = re.compile(r"url\(['\"]?(?P<url>[^'\")]+)['\"]?\)")
  40. self._match_css_import_string_ = re.compile(r"@import\s+\"(?P<url>[^\")]+)\"")
  41. self._resources = []
  42. @property
  43. def resources(self):
  44. return self._resources
  45. def handle_starttag(self, tag, attrs):
  46. self._tag_stack_.append(tag)
  47. if tag in ["script", "img", "iframe"]:
  48. attr_dict = dict(attrs)
  49. if "src" in attr_dict:
  50. self._resources.append(attr_dict["src"])
  51. if tag == "link":
  52. attr_dict = dict(attrs)
  53. if "rel" in attr_dict and attr_dict["rel"] == "stylesheet":
  54. self._resources.append(attr_dict["href"])
  55. def handle_endtag(self, tag):
  56. self._tag_stack_.pop()
  57. def handle_data(self, data):
  58. if self._tag_stack_ and self._tag_stack_[-1] == "style":
  59. # Look for uses of url()
  60. url_iterator = self._match_css_url_.finditer(data)
  61. for match in url_iterator:
  62. self._resources.append(match.group("url"))
  63. # Look for @imports that use plain strings - we already found the url() ones
  64. import_iterator = self._match_css_import_string_.finditer(data)
  65. for match in import_iterator:
  66. self._resources.append(match.group("url"))
  67. class TestTypeIdentifier(HTMLParser):
  68. """Identifies what kind of test the page is, and stores it in self.test_type
  69. For reference tests, the URL of the reference page is saved as self.reference_path
  70. """
  71. def __init__(self, url):
  72. super().__init__()
  73. self.url = url
  74. self.test_type = TestType.TEXT
  75. self.reference_path = None
  76. self.ref_test_link_found = False
  77. def handle_starttag(self, tag, attrs):
  78. if tag == "link":
  79. attr_dict = dict(attrs)
  80. if "rel" in attr_dict and (attr_dict["rel"] == "match" or attr_dict["rel"] == "mismatch"):
  81. if self.ref_test_link_found:
  82. raise RuntimeError("Ref tests with multiple match or mismatch links are not currently supported")
  83. self.test_type = TestType.REF
  84. self.reference_path = attr_dict["href"]
  85. self.ref_test_link_found = True
  86. def map_to_path(sources: list[ResourceAndType], is_resource=True, resource_path=None) -> list[PathMapping]:
  87. filepaths: list[PathMapping] = []
  88. for source in sources:
  89. base_directory = test_type.input_path if source.type == ResourceType.INPUT else test_type.expected_path
  90. if source.resource.startswith('/') or not is_resource:
  91. file_path = Path(base_directory, source.resource.lstrip('/'))
  92. else:
  93. # Add it as a sibling path if it's a relative resource
  94. sibling_location = Path(resource_path).parent
  95. parent_directory = Path(base_directory, sibling_location)
  96. file_path = Path(parent_directory, source.resource)
  97. # Map to source and destination
  98. output_path = wpt_base_url + str(file_path).replace(base_directory, '')
  99. filepaths.append(PathMapping(output_path, file_path.absolute()))
  100. return filepaths
  101. def is_crash_test(url_string):
  102. # https://web-platform-tests.org/writing-tests/crashtest.html
  103. # A test file is treated as a crash test if they have -crash in their name before the file extension, or they are
  104. # located in a folder named crashtests
  105. parsed_url = urlparse(url_string)
  106. path_segments = parsed_url.path.strip('/').split('/')
  107. if len(path_segments) > 1 and "crashtests" in path_segments[::-1]:
  108. return True
  109. file_name = path_segments[-1]
  110. file_name_parts = file_name.split('.')
  111. if len(file_name_parts) > 1 and any([part.endswith('-crash') for part in file_name_parts[:-1]]):
  112. return True
  113. return False
  114. def modify_sources(files, resources: list[ResourceAndType]) -> None:
  115. for file in files:
  116. # Get the distance to the wpt-imports folder
  117. folder_index = str(file).find(test_type.input_path)
  118. if folder_index == -1:
  119. folder_index = str(file).find(test_type.expected_path)
  120. non_prefixed_path = str(file)[folder_index + len(test_type.expected_path):]
  121. else:
  122. non_prefixed_path = str(file)[folder_index + len(test_type.input_path):]
  123. parent_folder_count = len(Path(non_prefixed_path).parent.parts) - 1
  124. parent_folder_path = '../' * parent_folder_count
  125. with open(file, 'r') as f:
  126. page_source = f.read()
  127. # Iterate all scripts and overwrite the src attribute
  128. for i, resource in enumerate(map(lambda r: r.resource, resources)):
  129. if resource.startswith('/'):
  130. new_src_value = parent_folder_path + resource[1::]
  131. page_source = page_source.replace(resource, new_src_value)
  132. # Look for mentions of the reference page, and update their href
  133. if raw_reference_path is not None:
  134. new_reference_path = parent_folder_path + '../../expected/wpt-import/' + reference_path[::]
  135. page_source = page_source.replace(raw_reference_path, new_reference_path)
  136. with open(file, 'w') as f:
  137. f.write(str(page_source))
  138. def download_files(filepaths):
  139. downloaded_files = []
  140. for file in filepaths:
  141. source = urljoin(file.source, "/".join(file.source.split('/')[3:]))
  142. destination = Path(os.path.normpath(file.destination))
  143. if destination.exists():
  144. print(f"Skipping {destination} as it already exists")
  145. continue
  146. print(f"Downloading {source} to {destination}")
  147. connection = urlopen(source)
  148. if connection.status != 200:
  149. print(f"Failed to download {file.source}")
  150. continue
  151. os.makedirs(destination.parent, exist_ok=True)
  152. with open(destination, 'wb') as f:
  153. f.write(connection.read())
  154. downloaded_files.append(destination)
  155. return downloaded_files
  156. def create_expectation_files(files):
  157. # Ref tests don't have an expectation text file
  158. if test_type in [TestType.REF, TestType.CRASH]:
  159. return
  160. for file in files:
  161. new_path = str(file.destination).replace(test_type.input_path, test_type.expected_path)
  162. new_path = new_path.rsplit(".", 1)[0] + '.txt'
  163. expected_file = Path(new_path)
  164. if expected_file.exists():
  165. print(f"Skipping {expected_file} as it already exists")
  166. continue
  167. os.makedirs(expected_file.parent, exist_ok=True)
  168. expected_file.touch()
  169. def main():
  170. if len(sys.argv) != 2:
  171. print("Usage: import-wpt-test.py <url>")
  172. return
  173. url_to_import = sys.argv[1]
  174. resource_path = '/'.join(Path(url_to_import).parts[2::])
  175. with urlopen(url_to_import) as response:
  176. page = response.read().decode("utf-8")
  177. global test_type, reference_path, raw_reference_path
  178. if is_crash_test(url_to_import):
  179. test_type = TestType.CRASH
  180. else:
  181. identifier = TestTypeIdentifier(url_to_import)
  182. identifier.feed(page)
  183. test_type = identifier.test_type
  184. raw_reference_path = identifier.reference_path
  185. print(f"Identified {url_to_import} as type {test_type}, ref {raw_reference_path}")
  186. main_file = [ResourceAndType(resource_path, ResourceType.INPUT)]
  187. main_paths = map_to_path(main_file, False)
  188. if test_type == TestType.REF and raw_reference_path is None:
  189. raise RuntimeError('Failed to file reference path in ref test')
  190. if raw_reference_path is not None:
  191. if raw_reference_path.startswith('/'):
  192. reference_path = raw_reference_path
  193. main_paths.append(PathMapping(
  194. wpt_base_url + raw_reference_path,
  195. Path(test_type.expected_path + raw_reference_path).absolute()
  196. ))
  197. else:
  198. reference_path = Path(resource_path).parent.joinpath(raw_reference_path).__str__()
  199. main_paths.append(PathMapping(
  200. wpt_base_url + '/' + reference_path,
  201. Path(test_type.expected_path + '/' + reference_path).absolute()
  202. ))
  203. files_to_modify = download_files(main_paths)
  204. create_expectation_files(main_paths)
  205. input_parser = LinkedResourceFinder()
  206. input_parser.feed(page)
  207. additional_resources = list(map(lambda s: ResourceAndType(s, ResourceType.INPUT), input_parser.resources))
  208. expected_parser = LinkedResourceFinder()
  209. for path in main_paths[1:]:
  210. with urlopen(path.source) as response:
  211. page = response.read().decode("utf-8")
  212. expected_parser.feed(page)
  213. additional_resources.extend(
  214. list(map(lambda s: ResourceAndType(s, ResourceType.EXPECTED), expected_parser.resources))
  215. )
  216. modify_sources(files_to_modify, additional_resources)
  217. script_paths = map_to_path(additional_resources, True, resource_path)
  218. download_files(script_paths)
  219. if __name__ == "__main__":
  220. main()