rules.py (17952B)
1 import abc 2 import inspect 3 import os 4 import re 5 from typing import Any, List, Match, Optional, Pattern, Text, Tuple, cast 6 7 8 Error = Tuple[str, str, str, Optional[int]] 9 10 def collapse(text: Text) -> Text: 11 return inspect.cleandoc(str(text)).replace("\n", " ") 12 13 14 class Rule(metaclass=abc.ABCMeta): 15 @abc.abstractproperty 16 def name(self) -> Text: 17 pass 18 19 @abc.abstractproperty 20 def description(self) -> Text: 21 pass 22 23 to_fix: Optional[Text] = None 24 25 @classmethod 26 def error(cls, path: Text, context: Tuple[Any, ...] = (), line_no: Optional[int] = None) -> Error: 27 name = cast(str, cls.name) 28 description = cast(str, cls.description) % context 29 return (name, description, path, line_no) 30 31 32 class MissingLink(Rule): 33 name = "MISSING-LINK" 34 description = "Testcase file must have a link to a spec" 35 to_fix = """ 36 Ensure that there is a `<link rel="help" href="[url]">` for the spec. 37 `MISSING-LINK` is designed to ensure that the CSS build tool can find 38 the tests. Note that the CSS build system is primarily used by 39 [test.csswg.org/](http://test.csswg.org/), which doesn't use 40 `wptserve`, so `*.any.js` and similar tests won't work there; stick 41 with the `.html` equivalent. 42 """ 43 44 45 class PathLength(Rule): 46 name = "PATH LENGTH" 47 description = "/%s longer than maximum path length (%d > 150)" 48 to_fix = "use shorter filename to rename the test file" 49 50 51 class FileType(Rule): 52 name = "FILE TYPE" 53 description = "/%s is an unsupported file type (%s)" 54 55 56 class WorkerCollision(Rule): 57 name = "WORKER COLLISION" 58 description = collapse(""" 59 path ends with %s which collides with generated tests from %s files 60 """) 61 62 63 class GitIgnoreFile(Rule): 64 name = "GITIGNORE" 65 description = ".gitignore found outside the root" 66 67 68 class MojomJSFile(Rule): 69 name = "MOJOM-JS" 70 description = "Don't check *.mojom.js files into WPT" 71 to_fix = """ 72 Check if the file is already included in mojojs.zip: 73 https://source.chromium.org/chromium/chromium/src/+/master:chrome/tools/build/linux/FILES.cfg 74 If yes, use `loadMojoResources` from `resources/test-only-api.js` to load 75 it; if not, contact ecosystem-infra@chromium.org for adding new files 76 to mojojs.zip. 77 """ 78 79 80 class AhemCopy(Rule): 81 name = "AHEM COPY" 82 description = "Don't add extra copies of Ahem, use /fonts/Ahem.ttf" 83 84 85 class AhemSystemFont(Rule): 86 name = "AHEM SYSTEM FONT" 87 description = "Don't use Ahem as a system font, use /fonts/ahem.css" 88 89 90 # TODO: Add tests for this rule 91 class IgnoredPath(Rule): 92 name = "IGNORED PATH" 93 description = collapse(""" 94 %s matches an ignore filter in .gitignore - please add a .gitignore 95 exception 96 """) 97 98 99 class ParseFailed(Rule): 100 name = "PARSE-FAILED" 101 description = "Unable to parse file" 102 to_fix = """ 103 examine the file to find the causes of any parse errors, and fix them. 104 """ 105 106 107 class ContentManual(Rule): 108 name = "CONTENT-MANUAL" 109 description = "Manual test whose filename doesn't end in '-manual'" 110 111 112 class ContentVisual(Rule): 113 name = "CONTENT-VISUAL" 114 description = "Visual test whose filename doesn't end in '-visual'" 115 116 117 class AbsoluteUrlRef(Rule): 118 name = "ABSOLUTE-URL-REF" 119 description = collapse(""" 120 Reference test with a reference file specified via an absolute URL: 121 '%s' 122 """) 123 124 125 class SameFileRef(Rule): 126 name = "SAME-FILE-REF" 127 description = "Reference test which points at itself as a reference" 128 129 130 class NonexistentRef(Rule): 131 name = "NON-EXISTENT-REF" 132 description = collapse(""" 133 Reference test with a non-existent '%s' relationship reference: '%s' 134 """) 135 136 137 class MultipleTimeout(Rule): 138 name = "MULTIPLE-TIMEOUT" 139 description = "More than one meta name='timeout'" 140 to_fix = """ 141 ensure each test file has only one instance of a `<meta 142 name="timeout"...>` element 143 """ 144 145 146 class InvalidTimeout(Rule): 147 name = "INVALID-TIMEOUT" 148 description = collapse(""" 149 Test file with `<meta name='timeout'...>` element that has a `content` 150 attribute whose value is not `long`: %s 151 """) 152 to_fix = "replace the value of the `content` attribute with `long`" 153 154 155 class MultipleTestharness(Rule): 156 name = "MULTIPLE-TESTHARNESS" 157 description = "More than one `<script src='/resources/testharness.js'>`" 158 to_fix = """ 159 Ensure each test has only one `<script 160 src='/resources/testharness.js'>` instance. 161 For `.js` tests, remove `// META: script=/resources/testharness.js`, 162 which wptserve already adds to the boilerplate markup. 163 """ 164 165 166 class MissingReftestWait(Rule): 167 name = "MISSING-REFTESTWAIT" 168 description = "Missing `class=reftest-wait`" 169 to_fix = """ 170 ensure tests that include reftest-wait.js also use class=reftest-wait on the root element. 171 """ 172 173 174 class MissingTestharnessReport(Rule): 175 name = "MISSING-TESTHARNESSREPORT" 176 description = "Missing `<script src='/resources/testharnessreport.js'>`" 177 to_fix = """ 178 ensure each test file contains `<script 179 src='/resources/testharnessreport.js'>` 180 """ 181 182 183 class MultipleTestharnessReport(Rule): 184 name = "MULTIPLE-TESTHARNESSREPORT" 185 description = "More than one `<script src='/resources/testharnessreport.js'>`" 186 to_fix = """ 187 Ensure each test has only one `<script 188 src='/resources/testharnessreport.js'>` instance. 189 For `.js` tests, remove `// META: script=/resources/testharnessreport.js`, 190 which wptserve already adds to the boilerplate markup. 191 """ 192 193 194 class VariantMissing(Rule): 195 name = "VARIANT-MISSING" 196 description = collapse(""" 197 Test file with a `<meta name='variant'...>` element that's missing a 198 `content` attribute 199 """) 200 to_fix = """ 201 add a `content` attribute with an appropriate value to the `<meta 202 name='variant'...>` element 203 """ 204 205 206 class MalformedVariant(Rule): 207 name = "MALFORMED-VARIANT" 208 description = collapse(""" 209 %s must be a non empty string 210 and start with '?' or '#' 211 """) 212 213 214 class LateTimeout(Rule): 215 name = "LATE-TIMEOUT" 216 description = "`<meta name=timeout>` seen after testharness.js script" 217 description = collapse(""" 218 Test file with `<meta name='timeout'...>` element after `<script 219 src='/resources/testharnessreport.js'>` element 220 """) 221 to_fix = """ 222 move the `<meta name="timeout"...>` element to precede the `script` 223 element. 224 """ 225 226 227 class EarlyTestharnessReport(Rule): 228 name = "EARLY-TESTHARNESSREPORT" 229 description = collapse(""" 230 Test file has an instance of 231 `<script src='/resources/testharnessreport.js'>` prior to 232 `<script src='/resources/testharness.js'>` 233 """) 234 to_fix = "flip the order" 235 236 237 class EarlyTestdriverVendor(Rule): 238 name = "EARLY-TESTDRIVER-VENDOR" 239 description = collapse(""" 240 Test file has an instance of 241 `<script src='/resources/testdriver-vendor.js'>` prior to 242 `<script src='/resources/testdriver.js'>` 243 """) 244 to_fix = "flip the order" 245 246 247 class MultipleTestdriver(Rule): 248 name = "MULTIPLE-TESTDRIVER" 249 description = "More than one `<script src='/resources/testdriver.js'>`" 250 251 252 class MissingTestdriverVendor(Rule): 253 name = "MISSING-TESTDRIVER-VENDOR" 254 description = "Missing `<script src='/resources/testdriver-vendor.js'>`" 255 256 257 class MultipleTestdriverVendor(Rule): 258 name = "MULTIPLE-TESTDRIVER-VENDOR" 259 description = "More than one `<script src='/resources/testdriver-vendor.js'>`" 260 261 262 class TestharnessPath(Rule): 263 name = "TESTHARNESS-PATH" 264 description = "testharness.js script seen with incorrect path" 265 266 267 class TestharnessReportPath(Rule): 268 name = "TESTHARNESSREPORT-PATH" 269 description = "testharnessreport.js script seen with incorrect path" 270 271 272 class TestdriverPath(Rule): 273 name = "TESTDRIVER-PATH" 274 description = "testdriver.js script seen with incorrect path" 275 276 277 class TestdriverUnsupportedQueryParameter(Rule): 278 name = "TESTDRIVER-UNSUPPORTED-QUERY-PARAMETER" 279 description = "testdriver.js script seen with unsupported query parameters" 280 281 282 class TestdriverVendorPath(Rule): 283 name = "TESTDRIVER-VENDOR-PATH" 284 description = "testdriver-vendor.js script seen with incorrect path" 285 286 287 class TestdriverInUnsupportedType(Rule): 288 name = "TESTDRIVER-IN-UNSUPPORTED-TYPE" 289 description = "testdriver.js included in a %s test, which doesn't support testdriver.js" 290 291 292 class OpenNoMode(Rule): 293 name = "OPEN-NO-MODE" 294 description = "File opened without providing an explicit mode (note: binary files must be read with 'b' in the mode flags)" 295 296 297 class UnknownGlobalMetadata(Rule): 298 name = "UNKNOWN-GLOBAL-METADATA" 299 description = "Unexpected value for global metadata" 300 301 302 class BrokenGlobalMetadata(Rule): 303 name = "BROKEN-GLOBAL-METADATA" 304 description = "Invalid global metadata: %s" 305 306 307 class UnknownTimeoutMetadata(Rule): 308 name = "UNKNOWN-TIMEOUT-METADATA" 309 description = "Unexpected value for timeout metadata" 310 311 312 class UnknownMetadata(Rule): 313 name = "UNKNOWN-METADATA" 314 description = "Unexpected kind of metadata" 315 316 317 class StrayMetadata(Rule): 318 name = "STRAY-METADATA" 319 description = "Metadata comments should start the file" 320 321 322 class IndentedMetadata(Rule): 323 name = "INDENTED-METADATA" 324 description = "Metadata comments should start the line" 325 326 327 class BrokenMetadata(Rule): 328 name = "BROKEN-METADATA" 329 description = "Metadata comment is not formatted correctly" 330 331 332 class TestharnessInOtherType(Rule): 333 name = "TESTHARNESS-IN-OTHER-TYPE" 334 description = "testharness.js included in a %s test" 335 336 337 class ReferenceInOtherType(Rule): 338 name = "REFERENCE-IN-OTHER-TYPE" 339 description = "Reference link included in a %s test" 340 341 342 class DuplicateBasenamePath(Rule): 343 name = "DUPLICATE-BASENAME-PATH" 344 description = collapse(""" 345 File has identical basename path (path excluding extension) as 346 other file(s) (found extensions: %s) 347 """) 348 to_fix = "rename files so they have unique basename paths" 349 350 351 class DuplicatePathCaseInsensitive(Rule): 352 name = "DUPLICATE-CASE-INSENSITIVE-PATH" 353 description = collapse(""" 354 Path differs from path %s only in case 355 """) 356 to_fix = "rename files so they are unique irrespective of case" 357 358 359 class TentativeDirectoryName(Rule): 360 name = "TENTATIVE-DIRECTORY-NAME" 361 description = "Directories for tentative tests must be named exactly 'tentative'" 362 to_fix = "rename directory to be called 'tentative'" 363 364 365 class InvalidMetaFile(Rule): 366 name = "INVALID-META-FILE" 367 description = "The META.yml is not a YAML file with the expected structure" 368 369 370 class InvalidWebFeaturesFile(Rule): 371 name = "INVALID-WEB-FEATURES-FILE" 372 description = "The WEB_FEATURES.yml file contains an invalid structure" 373 374 375 class MissingTestInWebFeaturesFile(Rule): 376 name = "MISSING-WEB-FEATURES-FILE" 377 description = collapse(""" 378 The WEB_FEATURES.yml file references a test that does not exist: '%s' 379 """) 380 381 382 EXTENSIONS = { 383 "html": [".html", ".htm"], 384 "xhtml": [".xht", ".xhtml"], 385 "svg": [".svg"], 386 "js": [".js", ".mjs"], 387 "python": [".py"] 388 } 389 EXTENSIONS["markup"] = EXTENSIONS["html"] + EXTENSIONS["xhtml"] + EXTENSIONS["svg"] 390 EXTENSIONS["js_all"] = EXTENSIONS["markup"] + EXTENSIONS["js"] 391 392 393 class Regexp(metaclass=abc.ABCMeta): 394 @abc.abstractproperty 395 def pattern(self) -> bytes: 396 pass 397 398 @abc.abstractproperty 399 def name(self) -> Text: 400 pass 401 402 @abc.abstractproperty 403 def description(self) -> Text: 404 pass 405 406 file_extensions: Optional[List[Text]] = None 407 408 def __init__(self) -> None: 409 self._re: Pattern[bytes] = re.compile(self.pattern) 410 411 def applies(self, path: Text) -> bool: 412 return (self.file_extensions is None or 413 os.path.splitext(path)[1] in self.file_extensions) 414 415 def search(self, line: bytes) -> Optional[Match[bytes]]: 416 return self._re.search(line) 417 418 419 class TabsRegexp(Regexp): 420 pattern = b"^\t" 421 name = "INDENT TABS" 422 description = "Test-file line starts with one or more tab characters" 423 to_fix = "use spaces to replace any tab characters at beginning of lines" 424 425 426 class CRRegexp(Regexp): 427 pattern = b"\r$" 428 name = "CR AT EOL" 429 description = "Test-file line ends with CR (U+000D) character" 430 to_fix = """ 431 reformat file so each line just has LF (U+000A) line ending (standard, 432 cross-platform "Unix" line endings instead of, e.g., DOS line endings). 433 """ 434 435 436 class SetTimeoutRegexp(Regexp): 437 pattern = br"setTimeout\s*\(" 438 name = "SET TIMEOUT" 439 file_extensions = EXTENSIONS["js_all"] 440 description = "setTimeout used" 441 to_fix = """ 442 replace all `setTimeout(...)` calls with `step_timeout(...)` calls 443 """ 444 445 446 class W3CTestOrgRegexp(Regexp): 447 pattern = br"w3c\-test\.org" 448 name = "W3C-TEST.ORG" 449 description = "Test-file line has the string `w3c-test.org`" 450 to_fix = """ 451 either replace the `w3c-test.org` string with the expression 452 `{{host}}:{{ports[http][0]}}` or a generic hostname like `example.org` 453 """ 454 455 456 class WebPlatformTestRegexp(Regexp): 457 pattern = br"web\-platform\.test" 458 name = "WEB-PLATFORM.TEST" 459 description = "Internal web-platform.test domain used" 460 to_fix = """ 461 use [server-side substitution](https://web-platform-tests.org/writing-tests/server-pipes.html#sub), 462 along with the [`.sub` filename-flag](https://web-platform-tests.org/writing-tests/file-names.html#test-features), 463 to replace web-platform.test with `{{domains[]}}` 464 """ 465 466 467 class Webidl2Regexp(Regexp): 468 pattern = br"webidl2\.js" 469 name = "WEBIDL2.JS" 470 description = "Legacy webidl2.js script used" 471 472 473 class ConsoleRegexp(Regexp): 474 pattern = br"console\.[a-zA-Z]+\s*\(" 475 name = "CONSOLE" 476 file_extensions = EXTENSIONS["js_all"] 477 description = "Test-file line has a `console.*(...)` call" 478 to_fix = """ 479 remove the `console.*(...)` call (and in some cases, consider adding an 480 `assert_*` of some kind in place of it) 481 """ 482 483 484 class GenerateTestsRegexp(Regexp): 485 pattern = br"generate_tests\s*\(" 486 name = "GENERATE_TESTS" 487 file_extensions = EXTENSIONS["js_all"] 488 description = "Test file line has a generate_tests call" 489 to_fix = "remove the call and call `test()` a number of times instead" 490 491 492 class PrintRegexp(Regexp): 493 pattern = br"print(?:\s|\s*\()" 494 name = "PRINT STATEMENT" 495 file_extensions = EXTENSIONS["python"] 496 description = collapse(""" 497 A server-side python support file contains a `print` statement 498 """) 499 to_fix = """ 500 remove the `print` statement or replace it with something else that 501 achieves the intended effect (e.g., a logging call) 502 """ 503 504 505 class LayoutTestsRegexp(Regexp): 506 pattern = br"(eventSender|testRunner|internals)\." 507 name = "LAYOUTTESTS APIS" 508 file_extensions = EXTENSIONS["js_all"] 509 description = "eventSender/testRunner/internals used; these are LayoutTests-specific APIs (WebKit/Blink)" 510 511 512 class MissingDepsRegexp(Regexp): 513 pattern = br"[^\w]/gen/" 514 name = "MISSING DEPENDENCY" 515 file_extensions = EXTENSIONS["js_all"] 516 description = "Chromium-specific content referenced" 517 to_fix = "Reimplement the test to use well-documented testing interfaces" 518 519 520 class SpecialPowersRegexp(Regexp): 521 pattern = b"SpecialPowers" 522 name = "SPECIALPOWERS API" 523 file_extensions = EXTENSIONS["js_all"] 524 description = "SpecialPowers used; this is gecko-specific and not supported in wpt" 525 526 527 class TrailingWhitespaceRegexp(Regexp): 528 name = "TRAILING WHITESPACE" 529 description = "Whitespace at EOL" 530 pattern = b"[ \t\f\v]$" 531 to_fix = """Remove trailing whitespace from all lines in the file.""" 532 533 534 class AssertThrowsRegexp(Regexp): 535 pattern = br"[^.]assert_throws\(" 536 name = "ASSERT_THROWS" 537 file_extensions = EXTENSIONS["js_all"] 538 description = "Test-file line has an `assert_throws(...)` call" 539 to_fix = """Replace with `assert_throws_dom` or `assert_throws_js` or `assert_throws_exactly`""" 540 541 542 class PromiseRejectsRegexp(Regexp): 543 pattern = br"promise_rejects\(" 544 name = "PROMISE_REJECTS" 545 file_extensions = EXTENSIONS["js_all"] 546 description = "Test-file line has a `promise_rejects(...)` call" 547 to_fix = """Replace with promise_rejects_dom or promise_rejects_js or `promise_rejects_exactly`""" 548 549 550 class AssertPreconditionRegexp(Regexp): 551 pattern = br"[^.]assert_precondition\(" 552 name = "ASSERT-PRECONDITION" 553 file_extensions = EXTENSIONS["js_all"] 554 description = "Test-file line has an `assert_precondition(...)` call" 555 to_fix = """Replace with `assert_implements` or `assert_implements_optional`""" 556 557 558 class HTMLInvalidSyntaxRegexp(Regexp): 559 pattern = (br"<(a|abbr|article|audio|b|bdi|bdo|blockquote|body|button|canvas|caption|cite|code|colgroup|data|datalist|dd|del|details|" 560 br"dfn|dialog|div|dl|dt|em|fieldset|figcaption|figure|footer|form|h[1-6]|head|header|html|i|iframe|ins|kbd|label|legend|li|" 561 br"main|map|mark|menu|meter|nav|noscript|object|ol|optgroup|option|output|p|picture|pre|progress|q|rp|rt|ruby|s|samp|script|" 562 br"search|section|select|slot|small|span|strong|style|sub|summary|sup|table|tbody|td|template|textarea|tfoot|th|thead|time|" 563 br"title|tr|u|ul|var|video)(\s+[^>]+)?\s*/>") 564 name = "HTML INVALID SYNTAX" 565 file_extensions = EXTENSIONS["html"] 566 description = "Test-file line has a non-void HTML tag with /> syntax" 567 to_fix = """Replace with start tag and end tag""" 568 569 570 class TestDriverInternalRegexp(Regexp): 571 pattern = br"test_driver_internal" 572 name = "TEST DRIVER INTERNAL" 573 file_extensions = EXTENSIONS["js_all"] 574 description = "Test-file uses test_driver_internal API" 575 to_fix = """Only use test_driver public API"""