item.py (11630B)
1 import os.path 2 from abc import ABCMeta, abstractproperty 3 from inspect import isabstract 4 from typing import (Any, Dict, Hashable, List, Optional, Sequence, Text, Tuple, Type, 5 TYPE_CHECKING, Union, cast) 6 from urllib.parse import urljoin, urlparse, parse_qs 7 8 from .utils import to_os_path 9 10 if TYPE_CHECKING: 11 from .manifest import Manifest 12 13 Fuzzy = Dict[Optional[Tuple[str, str, str]], List[int]] 14 PageRanges = Dict[str, List[int]] 15 item_types: Dict[str, Type["ManifestItem"]] = {} 16 17 18 class ManifestItemMeta(ABCMeta): 19 """Custom metaclass that registers all the subclasses in the 20 item_types dictionary according to the value of their item_type 21 attribute, and otherwise behaves like an ABCMeta.""" 22 23 def __new__(cls: Type["ManifestItemMeta"], name: str, bases: Tuple[type], attrs: Dict[str, Any]) -> "ManifestItemMeta": 24 inst = super().__new__(cls, name, bases, attrs) 25 if isabstract(inst): 26 return inst 27 28 assert issubclass(inst, ManifestItem) 29 item_type = cast(str, inst.item_type) 30 31 item_types[item_type] = inst 32 33 return inst 34 35 36 class ManifestItem(metaclass=ManifestItemMeta): 37 __slots__ = ("_tests_root", "path") 38 39 def __init__(self, tests_root: Text, path: Text) -> None: 40 self._tests_root = tests_root 41 self.path = path 42 43 @abstractproperty 44 def id(self) -> Text: 45 """The test's id (usually its url)""" 46 pass 47 48 @abstractproperty 49 def item_type(self) -> str: 50 """The item's type""" 51 pass 52 53 @property 54 def path_parts(self) -> Tuple[Text, ...]: 55 return tuple(self.path.split(os.path.sep)) 56 57 def key(self) -> Hashable: 58 """A unique identifier for the test""" 59 return (self.item_type, self.id) 60 61 def __eq__(self, other: Any) -> bool: 62 if not hasattr(other, "key"): 63 return False 64 return bool(self.key() == other.key()) 65 66 def __hash__(self) -> int: 67 return hash(self.key()) 68 69 def __repr__(self) -> str: 70 return f"<{self.__module__}.{self.__class__.__name__} id={self.id!r}, path={self.path!r}>" 71 72 def to_json(self) -> Tuple[Any, ...]: 73 return () 74 75 @classmethod 76 def from_json(cls, 77 manifest: "Manifest", 78 path: Text, 79 obj: Any 80 ) -> "ManifestItem": 81 path = to_os_path(path) 82 tests_root = manifest.tests_root 83 assert tests_root is not None 84 return cls(tests_root, path) 85 86 87 class URLManifestItem(ManifestItem): 88 __slots__ = ("url_base", "_url", "_extras", "_flags") 89 90 def __init__(self, 91 tests_root: Text, 92 path: Text, 93 url_base: Text, 94 url: Optional[Text], 95 **extras: Any 96 ) -> None: 97 super().__init__(tests_root, path) 98 assert url_base[0] == "/" 99 self.url_base = url_base 100 assert url is None or url[0] != "/" 101 self._url = url 102 self._extras = extras 103 parsed_url = urlparse(self.url) 104 self._flags = (set(parsed_url.path.rsplit("/", 1)[1].split(".")[1:-1]) | 105 set(parse_qs(parsed_url.query).get("wpt_flags", []))) 106 107 @property 108 def id(self) -> Text: 109 return self.url 110 111 @property 112 def url(self) -> Text: 113 rel_url = self._url or self.path.replace(os.path.sep, "/") 114 # we can outperform urljoin, because we know we just have path relative URLs 115 if self.url_base == "/": 116 return "/" + rel_url 117 return urljoin(self.url_base, rel_url) 118 119 @property 120 def https(self) -> bool: 121 return "https" in self._flags or "serviceworker" in self._flags or "serviceworker-module" in self._flags 122 123 @property 124 def h2(self) -> bool: 125 return "h2" in self._flags 126 127 @property 128 def subdomain(self) -> bool: 129 # Note: this is currently hard-coded to check for `www`, rather than 130 # all possible valid subdomains. It can be extended if needed. 131 return "www" in self._flags 132 133 def to_json(self) -> Tuple[Optional[Text], Dict[Any, Any]]: 134 rel_url = None if self._url == self.path.replace(os.path.sep, "/") else self._url 135 rv: Tuple[Optional[Text], Dict[Any, Any]] = (rel_url, {}) 136 return rv 137 138 @classmethod 139 def from_json(cls, 140 manifest: "Manifest", 141 path: Text, 142 obj: Tuple[Text, Dict[Any, Any]] 143 ) -> "URLManifestItem": 144 path = to_os_path(path) 145 url, extras = obj 146 tests_root = manifest.tests_root 147 assert tests_root is not None 148 return cls(tests_root, 149 path, 150 manifest.url_base, 151 url, 152 **extras) 153 154 155 class TestharnessTest(URLManifestItem): 156 __slots__ = () 157 158 item_type = "testharness" 159 160 @property 161 def timeout(self) -> Optional[Text]: 162 return self._extras.get("timeout") 163 164 @property 165 def pac(self) -> Optional[Text]: 166 return self._extras.get("pac") 167 168 @property 169 def testdriver_features(self) -> Optional[List[Text]]: 170 return self._extras.get("testdriver_features") 171 172 @property 173 def testdriver(self) -> Optional[bool]: 174 return self._extras.get("testdriver") 175 176 @property 177 def jsshell(self) -> Optional[Text]: 178 return self._extras.get("jsshell") 179 180 @property 181 def script_metadata(self) -> Optional[List[Tuple[Text, Text]]]: 182 return self._extras.get("script_metadata") 183 184 def to_json(self) -> Tuple[Optional[Text], Dict[Text, Any]]: 185 rv = super().to_json() 186 if self.timeout is not None: 187 rv[-1]["timeout"] = self.timeout 188 if self.pac is not None: 189 rv[-1]["pac"] = self.pac 190 if self.testdriver_features is not None: 191 rv[-1]["testdriver_features"] = self.testdriver_features 192 if self.testdriver: 193 rv[-1]["testdriver"] = self.testdriver 194 if self.jsshell: 195 rv[-1]["jsshell"] = True 196 if self.script_metadata: 197 rv[-1]["script_metadata"] = [(k, v) for (k,v) in self.script_metadata] 198 return rv 199 200 201 class RefTest(URLManifestItem): 202 __slots__ = ("references",) 203 204 item_type = "reftest" 205 206 def __init__(self, 207 tests_root: Text, 208 path: Text, 209 url_base: Text, 210 url: Optional[Text], 211 references: Optional[List[Tuple[Text, Text]]] = None, 212 **extras: Any 213 ): 214 super().__init__(tests_root, path, url_base, url, **extras) 215 if references is None: 216 self.references: List[Tuple[Text, Text]] = [] 217 else: 218 self.references = references 219 220 @property 221 def timeout(self) -> Optional[Text]: 222 return self._extras.get("timeout") 223 224 @property 225 def viewport_size(self) -> Optional[Text]: 226 return self._extras.get("viewport_size") 227 228 @property 229 def dpi(self) -> Optional[Text]: 230 return self._extras.get("dpi") 231 232 @property 233 def fuzzy(self) -> Fuzzy: 234 fuzzy: Union[Fuzzy, List[Tuple[Optional[Sequence[Text]], List[int]]]] = self._extras.get("fuzzy", {}) 235 if not isinstance(fuzzy, list): 236 return fuzzy 237 238 rv: Fuzzy = {} 239 for k, v in fuzzy: # type: Tuple[Optional[Sequence[Text]], List[int]] 240 if k is None: 241 key: Optional[Tuple[Text, Text, Text]] = None 242 else: 243 # mypy types this as Tuple[Text, ...] 244 assert len(k) == 3 245 key = tuple(k) # type: ignore 246 rv[key] = v 247 return rv 248 249 @property 250 def testdriver(self) -> Optional[bool]: 251 return self._extras.get("testdriver") 252 253 def to_json(self) -> Tuple[Optional[Text], List[Tuple[Text, Text]], Dict[Text, Any]]: # type: ignore 254 rel_url = None if self._url == self.path else self._url 255 rv: Tuple[Optional[Text], List[Tuple[Text, Text]], Dict[Text, Any]] = (rel_url, self.references, {}) 256 extras = rv[-1] 257 if self.timeout is not None: 258 extras["timeout"] = self.timeout 259 if self.viewport_size is not None: 260 extras["viewport_size"] = self.viewport_size 261 if self.dpi is not None: 262 extras["dpi"] = self.dpi 263 if self.fuzzy: 264 extras["fuzzy"] = list(self.fuzzy.items()) 265 if self.testdriver: 266 extras["testdriver"] = self.testdriver 267 return rv 268 269 @classmethod 270 def from_json(cls, # type: ignore 271 manifest: "Manifest", 272 path: Text, 273 obj: Tuple[Text, List[Tuple[Text, Text]], Dict[Any, Any]] 274 ) -> "RefTest": 275 tests_root = manifest.tests_root 276 assert tests_root is not None 277 path = to_os_path(path) 278 url, references, extras = obj 279 return cls(tests_root, 280 path, 281 manifest.url_base, 282 url, 283 references, 284 **extras) 285 286 287 class PrintRefTest(RefTest): 288 __slots__ = ("references",) 289 290 item_type = "print-reftest" 291 292 @property 293 def page_ranges(self) -> PageRanges: 294 return cast(PageRanges, self._extras.get("page_ranges", {})) 295 296 def to_json(self): # type: ignore 297 rv = super().to_json() 298 if self.page_ranges: 299 rv[-1]["page_ranges"] = self.page_ranges 300 return rv 301 302 303 class ManualTest(URLManifestItem): 304 __slots__ = () 305 306 item_type = "manual" 307 308 309 class ConformanceCheckerTest(URLManifestItem): 310 __slots__ = () 311 312 item_type = "conformancechecker" 313 314 315 class VisualTest(URLManifestItem): 316 __slots__ = () 317 318 item_type = "visual" 319 320 321 class CrashTest(URLManifestItem): 322 __slots__ = () 323 324 item_type = "crashtest" 325 326 @property 327 def timeout(self) -> Optional[Text]: 328 return None 329 330 @property 331 def testdriver(self) -> Optional[bool]: 332 return self._extras.get("testdriver") 333 334 def to_json(self): # type: ignore 335 rel_url, extras = super().to_json() 336 if self.testdriver: 337 extras["testdriver"] = self.testdriver 338 return rel_url, extras 339 340 341 class WebDriverSpecTest(URLManifestItem): 342 __slots__ = () 343 344 item_type = "wdspec" 345 346 @property 347 def timeout(self) -> Optional[Text]: 348 return self._extras.get("timeout") 349 350 def to_json(self) -> Tuple[Optional[Text], Dict[Text, Any]]: 351 rv = super().to_json() 352 if self.timeout is not None: 353 rv[-1]["timeout"] = self.timeout 354 return rv 355 356 357 class SupportFile(ManifestItem): 358 __slots__ = () 359 360 item_type = "support" 361 362 @property 363 def id(self) -> Text: 364 return self.path 365 366 367 class SpecItem(ManifestItem): 368 __slots__ = ("specs") 369 370 item_type = "spec" 371 372 def __init__(self, 373 tests_root: Text, 374 path: Text, 375 specs: List[Text] 376 ) -> None: 377 super().__init__(tests_root, path) 378 self.specs = specs 379 380 @property 381 def id(self) -> Text: 382 return self.path 383 384 def to_json(self) -> Tuple[Optional[Text], Dict[Text, Any]]: 385 rv: Tuple[Optional[Text], Dict[Any, Any]] = (None, {}) 386 for i in range(len(self.specs)): 387 spec_key = f"spec_link{i+1}" 388 rv[-1][spec_key] = self.specs[i] 389 return rv 390 391 @classmethod 392 def from_json(cls, 393 manifest: "Manifest", 394 path: Text, 395 obj: Any 396 ) -> "ManifestItem": 397 """Not properly implemented and is not used.""" 398 return cls("/", "", [])