test262-update.py (33671B)
1 #!/usr/bin/env python 2 # 3 # This Source Code Form is subject to the terms of the Mozilla Public 4 # License, v. 2.0. If a copy of the MPL was not distributed with this 5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7 import contextlib 8 import os 9 import shutil 10 import sys 11 import tempfile 12 from functools import partial 13 from itertools import chain 14 from operator import itemgetter 15 16 # Skip all tests which use features not supported in SpiderMonkey. 17 UNSUPPORTED_FEATURES = set([ 18 "tail-call-optimization", 19 "Intl.Locale-info", # Bug 1693576 20 "source-phase-imports", 21 "source-phase-imports-module-source", 22 "import-defer", 23 "nonextensible-applies-to-private", # Bug 1991478 24 ]) 25 FEATURE_CHECK_NEEDED = { 26 "Atomics": "!this.hasOwnProperty('Atomics')", 27 "SharedArrayBuffer": "!this.hasOwnProperty('SharedArrayBuffer')", 28 "Temporal": "!this.hasOwnProperty('Temporal')", 29 "decorators": "!(this.hasOwnProperty('getBuildConfiguration')&&getBuildConfiguration('decorators'))", # Bug 1435869 30 "uint8array-base64": "!Uint8Array.fromBase64", # Bug 1985120 31 "explicit-resource-management": "!(this.hasOwnProperty('getBuildConfiguration')&&getBuildConfiguration('explicit-resource-management'))", # Bug 1569081 32 "Atomics.pause": "!this.hasOwnProperty('Atomics')||!Atomics.pause", # Bug 1918717 33 "Error.isError": "!Error.isError", # Bug 1923733 34 "iterator-sequencing": "!Iterator.concat", # Bug 1923732 35 "Math.sumPrecise": "!Math.sumPrecise", # Bug 1985121 36 "upsert": "!Map.prototype.getOrInsertComputed", # Bug 1986668 37 "immutable-arraybuffer": "!ArrayBuffer.prototype.sliceToImmutable", # Bug 1952253 38 } 39 RELEASE_OR_BETA = set(["legacy-regexp"]) 40 SHELL_OPTIONS = { 41 "ShadowRealm": "--enable-shadow-realms", 42 "symbols-as-weakmap-keys": "--enable-symbols-as-weakmap-keys", 43 "explicit-resource-management": "--enable-explicit-resource-management", 44 "iterator-sequencing": "--enable-iterator-sequencing", 45 "Atomics.waitAsync": "--setpref=atomics_wait_async", 46 "immutable-arraybuffer": "--enable-arraybuffer-immutable", 47 } 48 49 INCLUDE_FEATURE_DETECTED_OPTIONAL_SHELL_OPTIONS = {} 50 51 52 @contextlib.contextmanager 53 def TemporaryDirectory(): 54 tmpDir = tempfile.mkdtemp() 55 try: 56 yield tmpDir 57 finally: 58 shutil.rmtree(tmpDir) 59 60 61 def loadTest262Parser(test262Dir): 62 """ 63 Loads the test262 test record parser. 64 """ 65 import importlib.machinery 66 import importlib.util 67 68 packagingDir = os.path.join(test262Dir, "tools", "packaging") 69 moduleName = "parseTestRecord" 70 71 # Create a FileFinder to load Python source files. 72 loader_details = ( 73 importlib.machinery.SourceFileLoader, 74 importlib.machinery.SOURCE_SUFFIXES, 75 ) 76 finder = importlib.machinery.FileFinder(packagingDir, loader_details) 77 78 # Find the module spec. 79 spec = finder.find_spec(moduleName) 80 if spec is None: 81 raise RuntimeError("Can't find parseTestRecord module") 82 83 # Create and execute the module. 84 module = importlib.util.module_from_spec(spec) 85 spec.loader.exec_module(module) 86 87 # Return the executed module 88 return module 89 90 91 def tryParseTestFile(test262parser, source, testName): 92 """ 93 Returns the result of test262parser.parseTestRecord() or None if a parser 94 error occured. 95 96 See <https://github.com/tc39/test262/blob/main/INTERPRETING.md> for an 97 overview of the returned test attributes. 98 """ 99 try: 100 return test262parser.parseTestRecord(source, testName) 101 except Exception as err: 102 print("Error '%s' in file: %s" % (err, testName), file=sys.stderr) 103 print("Please report this error to the test262 GitHub repository!") 104 return None 105 106 107 def createRefTestEntry(options, skip, skipIf, error, isModule, isAsync): 108 """ 109 Returns the |reftest| tuple (terms, comments) from the input arguments. Or a 110 tuple of empty strings if no reftest entry is required. 111 """ 112 113 terms = [] 114 comments = [] 115 116 if options: 117 terms.extend(options) 118 119 if skip: 120 terms.append("skip") 121 comments.extend(skip) 122 123 if skipIf: 124 terms.append("skip-if(" + "||".join([cond for (cond, _) in skipIf]) + ")") 125 comments.extend([comment for (_, comment) in skipIf]) 126 127 if error: 128 terms.append("error:" + error) 129 130 if isModule: 131 terms.append("module") 132 133 if isAsync: 134 terms.append("async") 135 136 return (" ".join(terms), ", ".join(comments)) 137 138 139 def createRefTestLine(terms, comments): 140 """ 141 Creates the |reftest| line using the given terms and comments. 142 """ 143 144 refTest = terms 145 if comments: 146 refTest += " -- " + comments 147 return refTest 148 149 150 def createSource(testSource, refTest, prologue, epilogue): 151 """ 152 Returns the post-processed source for |testSource|. 153 """ 154 155 source = [] 156 157 # Add the |reftest| line. 158 if refTest: 159 source.append(b"// |reftest| " + refTest.encode("utf-8")) 160 161 # Prepend any directives if present. 162 if prologue: 163 source.append(prologue.encode("utf-8")) 164 165 source.append(testSource) 166 167 # Append the test epilogue, i.e. the call to "reportCompare". 168 # TODO: Does this conflict with raw tests? 169 if epilogue: 170 source.append(epilogue.encode("utf-8")) 171 source.append(b"") 172 173 return b"\n".join(source) 174 175 176 def writeTestFile(test262OutDir, testFileName, source): 177 """ 178 Writes the test source to |test262OutDir|. 179 """ 180 181 with open(os.path.join(test262OutDir, testFileName), "wb") as output: 182 output.write(source) 183 184 185 def addSuffixToFileName(fileName, suffix): 186 (filePath, ext) = os.path.splitext(fileName) 187 return filePath + suffix + ext 188 189 190 def writeShellAndBrowserFiles( 191 test262OutDir, harnessDir, includesMap, localIncludesMap, relPath 192 ): 193 """ 194 Generate the shell.js and browser.js files for the test harness. 195 """ 196 197 # Find all includes from parent directories. 198 def findParentIncludes(): 199 parentIncludes = set() 200 current = relPath 201 while current: 202 (parent, child) = os.path.split(current) 203 if parent in includesMap: 204 parentIncludes.update(includesMap[parent]) 205 current = parent 206 return parentIncludes 207 208 # Find all includes, skipping includes already present in parent directories. 209 def findIncludes(): 210 parentIncludes = findParentIncludes() 211 for include in includesMap[relPath]: 212 if include not in parentIncludes: 213 yield include 214 215 def readIncludeFile(filePath): 216 with open(filePath, "rb") as includeFile: 217 return b"// file: %s\n%s" % ( 218 os.path.basename(filePath).encode("utf-8"), 219 includeFile.read(), 220 ) 221 222 localIncludes = localIncludesMap[relPath] if relPath in localIncludesMap else [] 223 224 # Concatenate all includes files. 225 includeSource = b"\n".join( 226 map( 227 readIncludeFile, 228 chain( 229 # The requested include files. 230 map(partial(os.path.join, harnessDir), sorted(findIncludes())), 231 # And additional local include files. 232 map(partial(os.path.join, os.getcwd()), sorted(localIncludes)), 233 ), 234 ) 235 ) 236 237 # Write the concatenated include sources to shell.js. 238 with open(os.path.join(test262OutDir, relPath, "shell.js"), "wb") as shellFile: 239 if includeSource: 240 shellFile.write(b"// GENERATED, DO NOT EDIT\n") 241 shellFile.write(includeSource) 242 243 # The browser.js file is always empty for test262 tests. 244 with open(os.path.join(test262OutDir, relPath, "browser.js"), "wb") as browserFile: 245 browserFile.write(b"") 246 247 248 def pathStartsWith(path, *args): 249 prefix = os.path.join(*args) 250 return os.path.commonprefix([path, prefix]) == prefix 251 252 253 def convertTestFile(test262parser, testSource, testName, includeSet, strictTests): 254 """ 255 Convert a test262 test to a compatible jstests test file. 256 """ 257 258 # The test record dictionary, its contents are explained in depth at 259 # <https://github.com/tc39/test262/blob/main/INTERPRETING.md>. 260 testRec = tryParseTestFile(test262parser, testSource.decode("utf-8"), testName) 261 262 # jsreftest meta data 263 refTestOptions = [] 264 refTestSkip = [] 265 refTestSkipIf = [] 266 267 # Skip all files which contain YAML errors. 268 if testRec is None: 269 refTestSkip.append("has YAML errors") 270 testRec = dict() 271 272 # onlyStrict is set when the test must only be run in strict mode. 273 onlyStrict = "onlyStrict" in testRec 274 275 # noStrict is set when the test must not be run in strict mode. 276 noStrict = "noStrict" in testRec 277 278 # The "raw" attribute is used in the default test262 runner to prevent 279 # prepending additional content (use-strict directive, harness files) 280 # before the actual test source code. 281 raw = "raw" in testRec 282 283 # Negative tests have additional meta-data to specify the error type and 284 # when the error is issued (runtime error or early parse error). We're 285 # currently ignoring the error phase attribute. 286 # testRec["negative"] == {type=<error name>, phase=parse|resolution|runtime} 287 isNegative = "negative" in testRec 288 assert not isNegative or type(testRec["negative"]) is dict 289 errorType = testRec["negative"]["type"] if isNegative else None 290 291 # Async tests are marked with the "async" attribute. 292 isAsync = "async" in testRec 293 294 # Test262 tests cannot be both "negative" and "async". (In principle a 295 # negative async test is permitted when the error phase is not "parse" or 296 # the error type is not SyntaxError, but no such tests exist now.) 297 # assert not (isNegative and isAsync), ( 298 # "Can't have both async and negative attributes: %s" % testName 299 # ) 300 # TODO: Print a warning instead of asserting until 301 # <https://github.com/tc39/test262/issues/4638> is fixed. 302 if isNegative and isAsync: 303 print( 304 f"bad isnegative + async: {testName} (https://github.com/tc39/test262/issues/4638)" 305 ) 306 307 # Only async tests may use the $DONE or asyncTest function. However, 308 # negative parse tests may "use" the $DONE (or asyncTest) function (of 309 # course they don't actually use it!) without specifying the "async" 310 # attribute. Otherwise, neither $DONE nor asyncTest must appear in the test. 311 # 312 # Some "harness" tests redefine $DONE, so skip this check when the test file 313 # is in the "harness" directory. 314 assert ( 315 (b"$DONE" not in testSource and b"asyncTest" not in testSource) 316 or isAsync 317 or isNegative 318 or testName.split(os.path.sep)[0] == "harness" 319 ), "Missing async attribute in: %s" % testName 320 321 # When the "module" attribute is set, the source code is module code. 322 isModule = "module" in testRec 323 324 # CanBlockIsFalse is set when the test expects that the implementation 325 # cannot block on the main thread. 326 if "CanBlockIsFalse" in testRec: 327 refTestSkipIf.append(("xulRuntime.shell", "shell can block main thread")) 328 329 # CanBlockIsTrue is set when the test expects that the implementation 330 # can block on the main thread. 331 if "CanBlockIsTrue" in testRec: 332 refTestSkipIf.append(("!xulRuntime.shell", "browser cannot block main thread")) 333 334 # Skip tests with unsupported features. 335 if "features" in testRec: 336 unsupported = [f for f in testRec["features"] if f in UNSUPPORTED_FEATURES] 337 if unsupported: 338 refTestSkip.append("%s is not supported" % ",".join(unsupported)) 339 else: 340 releaseOrBeta = [f for f in testRec["features"] if f in RELEASE_OR_BETA] 341 if releaseOrBeta: 342 refTestSkipIf.append(( 343 "release_or_beta", 344 "%s is not released yet" % ",".join(releaseOrBeta), 345 )) 346 347 featureCheckNeeded = [ 348 f for f in testRec["features"] if f in FEATURE_CHECK_NEEDED 349 ] 350 if featureCheckNeeded: 351 refTestSkipIf.append(( 352 "||".join([FEATURE_CHECK_NEEDED[f] for f in featureCheckNeeded]), 353 "%s is not enabled unconditionally" % ",".join(featureCheckNeeded), 354 )) 355 356 if ( 357 "Atomics" in testRec["features"] 358 and "SharedArrayBuffer" in testRec["features"] 359 ): 360 refTestSkipIf.append(( 361 "(this.hasOwnProperty('getBuildConfiguration')" 362 "&&getBuildConfiguration('arm64-simulator'))", 363 "ARM64 Simulator cannot emulate atomics", 364 )) 365 366 shellOptions = { 367 SHELL_OPTIONS[f] for f in testRec["features"] if f in SHELL_OPTIONS 368 } 369 if shellOptions: 370 refTestSkipIf.append(("!xulRuntime.shell", "requires shell-options")) 371 refTestOptions.extend( 372 f"shell-option({opt})" for opt in sorted(shellOptions) 373 ) 374 375 # Optional shell options. Some tests use feature detection for additional 376 # test coverage. We want to get this extra coverage without having to skip 377 # these tests in browser builds. 378 if "includes" in testRec: 379 optionalShellOptions = ( 380 SHELL_OPTIONS[INCLUDE_FEATURE_DETECTED_OPTIONAL_SHELL_OPTIONS[include]] 381 for include in testRec["includes"] 382 if include in INCLUDE_FEATURE_DETECTED_OPTIONAL_SHELL_OPTIONS 383 ) 384 refTestOptions.extend( 385 f"shell-option({opt})" for opt in sorted(optionalShellOptions) 386 ) 387 388 # Includes for every test file in a directory is collected in a single 389 # shell.js file per directory level. This is done to avoid adding all 390 # test harness files to the top level shell.js file. 391 if "includes" in testRec: 392 assert not raw, "Raw test with includes: %s" % testName 393 includeSet.update(testRec["includes"]) 394 395 # Add reportCompare() after all positive, synchronous tests. 396 if not isNegative and not isAsync: 397 testEpilogue = "reportCompare(0, 0);" 398 else: 399 testEpilogue = "" 400 401 if raw: 402 refTestOptions.append("test262-raw") 403 404 (terms, comments) = createRefTestEntry( 405 refTestOptions, refTestSkip, refTestSkipIf, errorType, isModule, isAsync 406 ) 407 if raw: 408 refTest = "" 409 externRefTest = (terms, comments) 410 else: 411 refTest = createRefTestLine(terms, comments) 412 externRefTest = None 413 414 # Don't write a strict-mode variant for raw or module files. 415 noStrictVariant = raw or isModule 416 assert not (noStrictVariant and (onlyStrict or noStrict)), ( 417 "Unexpected onlyStrict or noStrict attribute: %s" % testName 418 ) 419 420 # Write non-strict mode test. 421 if noStrictVariant or noStrict or not onlyStrict: 422 testPrologue = "" 423 nonStrictSource = createSource(testSource, refTest, testPrologue, testEpilogue) 424 testFileName = testName 425 yield (testFileName, nonStrictSource, externRefTest) 426 427 # Write strict mode test. 428 if not noStrictVariant and (onlyStrict or (not noStrict and strictTests)): 429 testPrologue = "'use strict';" 430 strictSource = createSource(testSource, refTest, testPrologue, testEpilogue) 431 testFileName = testName 432 if not noStrict: 433 testFileName = addSuffixToFileName(testFileName, "-strict") 434 yield (testFileName, strictSource, externRefTest) 435 436 437 def convertFixtureFile(fixtureSource, fixtureName): 438 """ 439 Convert a test262 fixture file to a compatible jstests test file. 440 """ 441 442 # jsreftest meta data 443 refTestOptions = [] 444 refTestSkip = ["not a test file"] 445 refTestSkipIf = [] 446 errorType = None 447 isModule = False 448 isAsync = False 449 450 (terms, comments) = createRefTestEntry( 451 refTestOptions, refTestSkip, refTestSkipIf, errorType, isModule, isAsync 452 ) 453 refTest = createRefTestLine(terms, comments) 454 455 source = createSource(fixtureSource, refTest, "", "") 456 externRefTest = None 457 yield (fixtureName, source, externRefTest) 458 459 460 def process_test262(test262Dir, test262OutDir, strictTests, externManifests): 461 """ 462 Process all test262 files and converts them into jstests compatible tests. 463 """ 464 465 harnessDir = os.path.join(test262Dir, "harness") 466 testDir = os.path.join(test262Dir, "test") 467 test262parser = loadTest262Parser(test262Dir) 468 469 # Map of test262 subdirectories to the set of include files required for 470 # tests in that subdirectory. The includes for all tests in a subdirectory 471 # are merged into a single shell.js. 472 # map<dirname, set<includeFiles>> 473 includesMap = {} 474 475 # Additional local includes keyed by test262 directory names. The include 476 # files in this map must be located in the js/src/tests directory. 477 # map<dirname, list<includeFiles>> 478 localIncludesMap = {} 479 480 # The root directory contains required harness files and test262-host.js. 481 includesMap[""] = set(["sta.js", "assert.js"]) 482 localIncludesMap[""] = ["test262-host.js"] 483 484 # Also add files known to be used by many tests to the root shell.js file. 485 includesMap[""].update(["propertyHelper.js", "compareArray.js"]) 486 487 # Write the root shell.js file. 488 writeShellAndBrowserFiles( 489 test262OutDir, harnessDir, includesMap, localIncludesMap, "" 490 ) 491 492 # Additional explicit includes inserted at well-chosen locations to reduce 493 # code duplication in shell.js files. 494 explicitIncludes = {} 495 explicitIncludes[os.path.join("built-ins", "Atomics")] = [ 496 "testAtomics.js", 497 "testTypedArray.js", 498 ] 499 explicitIncludes[os.path.join("built-ins", "DataView")] = [ 500 "byteConversionValues.js" 501 ] 502 explicitIncludes[os.path.join("built-ins", "Promise")] = ["promiseHelper.js"] 503 explicitIncludes[os.path.join("built-ins", "Temporal")] = ["temporalHelpers.js"] 504 explicitIncludes[os.path.join("built-ins", "TypedArray")] = [ 505 "byteConversionValues.js", 506 "detachArrayBuffer.js", 507 "nans.js", 508 ] 509 explicitIncludes[os.path.join("built-ins", "TypedArrays")] = [ 510 "detachArrayBuffer.js" 511 ] 512 513 # We can't include "sm/non262.js", because it conflicts with our test harness, 514 # but some definitions from "sm/non262.js" are still needed. 515 localIncludesMap[os.path.join("staging", "sm")] = ["test262-non262.js"] 516 517 # Process all test directories recursively. 518 for dirPath, dirNames, fileNames in os.walk(testDir): 519 relPath = os.path.relpath(dirPath, testDir) 520 if relPath == ".": 521 continue 522 523 # Skip creating a "prs" directory if it already exists 524 if relPath not in ("prs", "local") and not os.path.exists( 525 os.path.join(test262OutDir, relPath) 526 ): 527 os.makedirs(os.path.join(test262OutDir, relPath)) 528 529 includeSet = set() 530 includesMap[relPath] = includeSet 531 532 if relPath in explicitIncludes: 533 includeSet.update(explicitIncludes[relPath]) 534 535 # Convert each test file. 536 for fileName in fileNames: 537 filePath = os.path.join(dirPath, fileName) 538 testName = os.path.relpath(filePath, testDir) 539 540 # Copy non-test files as is. 541 (_, fileExt) = os.path.splitext(fileName) 542 if fileExt != ".js": 543 shutil.copyfile(filePath, os.path.join(test262OutDir, testName)) 544 continue 545 546 # Files ending with "_FIXTURE.js" are fixture files: 547 # https://github.com/tc39/test262/blob/main/INTERPRETING.md#modules 548 isFixtureFile = fileName.endswith("_FIXTURE.js") 549 550 # Read the original test source and preprocess it for the jstests harness. 551 with open(filePath, "rb") as testFile: 552 testSource = testFile.read() 553 554 if isFixtureFile: 555 convert = convertFixtureFile(testSource, testName) 556 else: 557 convert = convertTestFile( 558 test262parser, 559 testSource, 560 testName, 561 includeSet, 562 strictTests, 563 ) 564 565 for newFileName, newSource, externRefTest in convert: 566 writeTestFile(test262OutDir, newFileName, newSource) 567 568 if externRefTest is not None: 569 externManifests.append({ 570 "name": newFileName, 571 "reftest": externRefTest, 572 }) 573 574 # Remove "sm/non262.js" because it overwrites our test harness with stub 575 # functions. 576 includeSet.discard("sm/non262.js") 577 578 # Add shell.js and browers.js files for the current directory. 579 writeShellAndBrowserFiles( 580 test262OutDir, harnessDir, includesMap, localIncludesMap, relPath 581 ) 582 583 584 def fetch_local_changes(inDir, outDir, srcDir, strictTests): 585 """ 586 Fetch the changes from a local clone of Test262. 587 588 1. Get the list of file changes made by the current branch used on Test262 (srcDir). 589 2. Copy only the (A)dded, (C)opied, (M)odified, and (R)enamed files to inDir. 590 3. inDir is treated like a Test262 checkout, where files will be converted. 591 4. Fetches the current branch name to set the outDir. 592 5. Processed files will be added to `<outDir>/local/<branchName>`. 593 """ 594 import subprocess 595 596 # TODO: fail if it's in the default branch? or require a branch name? 597 # Checks for unstaged or non committed files. A clean branch provides a clean status. 598 status = subprocess.check_output( 599 ("git -C %s status --porcelain" % srcDir).split(" "), encoding="utf-8" 600 ) 601 602 if status.strip(): 603 raise RuntimeError( 604 "Please commit files and cleanup the local test262 folder before importing files.\n" 605 "Current status: \n%s" % status 606 ) 607 608 # Captures the branch name to be used on the output 609 branchName = subprocess.check_output( 610 ("git -C %s rev-parse --abbrev-ref HEAD" % srcDir).split(" "), encoding="utf-8" 611 ).split("\n")[0] 612 613 # Fetches the file names to import 614 files = subprocess.check_output( 615 ("git -C %s diff main --diff-filter=ACMR --name-only" % srcDir).split(" "), 616 encoding="utf-8", 617 ) 618 619 # Fetches the deleted files to print an output log. This can be used to 620 # set up the skip list, if necessary. 621 deletedFiles = subprocess.check_output( 622 ("git -C %s diff main --diff-filter=D --name-only" % srcDir).split(" "), 623 encoding="utf-8", 624 ) 625 626 # Fetches the modified files as well for logging to support maintenance 627 # in the skip list. 628 modifiedFiles = subprocess.check_output( 629 ("git -C %s diff main --diff-filter=M --name-only" % srcDir).split(" "), 630 encoding="utf-8", 631 ) 632 633 # Fetches the renamed files for the same reason, this avoids duplicate 634 # tests if running the new local folder and the general imported Test262 635 # files. 636 renamedFiles = subprocess.check_output( 637 ("git -C %s diff main --diff-filter=R --summary" % srcDir).split(" "), 638 encoding="utf-8", 639 ) 640 641 # Print some friendly output 642 print("From the branch %s in %s \n" % (branchName, srcDir)) 643 print("Files being copied to the local folder: \n%s" % files) 644 if deletedFiles: 645 print( 646 "Deleted files (use this list to update the skip list): \n%s" % deletedFiles 647 ) 648 if modifiedFiles: 649 print( 650 "Modified files (use this list to update the skip list): \n%s" 651 % modifiedFiles 652 ) 653 if renamedFiles: 654 print("Renamed files (already added with the new names): \n%s" % renamedFiles) 655 656 for f in files.splitlines(): 657 # Capture the subdirectories names to recreate the file tree 658 # TODO: join the file tree with -- instead of multiple subfolders? 659 fileTree = os.path.join(inDir, os.path.dirname(f)) 660 if not os.path.exists(fileTree): 661 os.makedirs(fileTree) 662 663 shutil.copyfile( 664 os.path.join(srcDir, f), os.path.join(fileTree, os.path.basename(f)) 665 ) 666 667 # Extras from Test262. Copy the current support folders - including the 668 # harness - for a proper conversion process 669 shutil.copytree(os.path.join(srcDir, "tools"), os.path.join(inDir, "tools")) 670 shutil.copytree(os.path.join(srcDir, "harness"), os.path.join(inDir, "harness")) 671 672 # Reset any older directory in the output using the same branch name 673 outDir = os.path.join(outDir, "local", branchName) 674 if os.path.isdir(outDir): 675 shutil.rmtree(outDir) 676 os.makedirs(outDir) 677 678 process_test262(inDir, outDir, strictTests, []) 679 680 681 def fetch_pr_files(inDir, outDir, prNumber, strictTests): 682 import requests 683 684 prTestsOutDir = os.path.join(outDir, "prs", prNumber) 685 if os.path.isdir(prTestsOutDir): 686 print("Removing folder %s" % prTestsOutDir) 687 shutil.rmtree(prTestsOutDir) 688 os.makedirs(prTestsOutDir) 689 690 # Reuses current Test262 clone's harness and tools folders only, the clone's test/ 691 # folder can be discarded from here 692 shutil.rmtree(os.path.join(inDir, "test")) 693 694 prRequest = requests.get( 695 "https://api.github.com/repos/tc39/test262/pulls/%s" % prNumber 696 ) 697 prRequest.raise_for_status() 698 699 pr = prRequest.json() 700 701 if pr["state"] != "open": 702 # Closed PR, remove respective files from folder 703 return print("PR %s is closed" % prNumber) 704 705 url = "https://api.github.com/repos/tc39/test262/pulls/%s/files" % prNumber 706 hasNext = True 707 708 while hasNext: 709 files = requests.get(url) 710 files.raise_for_status() 711 712 for item in files.json(): 713 if not item["filename"].startswith("test/"): 714 continue 715 716 filename = item["filename"] 717 fileStatus = item["status"] 718 719 print("%s %s" % (fileStatus, filename)) 720 721 # Do not add deleted files 722 if fileStatus == "removed": 723 continue 724 725 contents = requests.get(item["raw_url"]) 726 contents.raise_for_status() 727 728 fileText = contents.text 729 730 filePathDirs = os.path.join(inDir, *filename.split("/")[:-1]) 731 732 if not os.path.isdir(filePathDirs): 733 os.makedirs(filePathDirs) 734 735 with open(os.path.join(inDir, *filename.split("/")), "wb") as output_file: 736 output_file.write(fileText.encode("utf8")) 737 738 hasNext = False 739 740 # Check if the pull request changes are split over multiple pages. 741 if "link" in files.headers: 742 link = files.headers["link"] 743 744 # The links are comma separated and the entries within a link are separated by a 745 # semicolon. For example the first two links entries for PR 3199: 746 # 747 # https://api.github.com/repos/tc39/test262/pulls/3199/files 748 # """ 749 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=2>; rel="next", 750 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=14>; rel="last" 751 # """ 752 # 753 # https://api.github.com/repositories/16147933/pulls/3199/files?page=2 754 # """ 755 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=1>; rel="prev", 756 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=3>; rel="next", 757 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=14>; rel="last", 758 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=1>; rel="first" 759 # """ 760 761 for pages in link.split(", "): 762 (pageUrl, rel) = pages.split("; ") 763 764 assert pageUrl[0] == "<" 765 assert pageUrl[-1] == ">" 766 767 # Remove the angle brackets around the URL. 768 pageUrl = pageUrl[1:-1] 769 770 # Make sure we only request data from github and not some other place. 771 assert pageUrl.startswith("https://api.github.com/") 772 773 # Ensure the relative URL marker has the expected format. 774 assert rel in {'rel="prev"', 'rel="next"', 'rel="first"', 'rel="last"'} 775 776 # We only need the URL for the next page. 777 if rel == 'rel="next"': 778 url = pageUrl 779 hasNext = True 780 781 process_test262(inDir, prTestsOutDir, strictTests, []) 782 783 784 def general_update(inDir, outDir, strictTests): 785 import subprocess 786 787 restoreLocalTestsDir = False 788 restorePrsTestsDir = False 789 localTestsOutDir = os.path.join(outDir, "local") 790 prsTestsOutDir = os.path.join(outDir, "prs") 791 792 # Stash test262/local and test262/prs. Currently the Test262 repo does not have any 793 # top-level subdirectories named "local" or "prs". 794 # This prevents these folders from being removed during the update process. 795 if os.path.isdir(localTestsOutDir): 796 shutil.move(localTestsOutDir, inDir) 797 restoreLocalTestsDir = True 798 799 if os.path.isdir(prsTestsOutDir): 800 shutil.move(prsTestsOutDir, inDir) 801 restorePrsTestsDir = True 802 803 # Create the output directory from scratch. 804 if os.path.isdir(outDir): 805 shutil.rmtree(outDir) 806 os.makedirs(outDir) 807 808 # Copy license file. 809 shutil.copyfile(os.path.join(inDir, "LICENSE"), os.path.join(outDir, "LICENSE")) 810 811 # Create the git info file. 812 with open(os.path.join(outDir, "GIT-INFO"), "w", encoding="utf-8") as info: 813 subprocess.check_call(["git", "-C", inDir, "log", "-1"], stdout=info) 814 815 # Copy the test files. 816 externManifests = [] 817 process_test262(inDir, outDir, strictTests, externManifests) 818 819 # Create the external reftest manifest file. 820 with open(os.path.join(outDir, "jstests.list"), "wb") as manifestFile: 821 manifestFile.write(b"# GENERATED, DO NOT EDIT\n\n") 822 for externManifest in sorted(externManifests, key=itemgetter("name")): 823 (terms, comments) = externManifest["reftest"] 824 if terms: 825 entry = "%s script %s%s\n" % ( 826 terms, 827 externManifest["name"], 828 (" # %s" % comments) if comments else "", 829 ) 830 manifestFile.write(entry.encode("utf-8")) 831 832 # Move test262/local back. 833 if restoreLocalTestsDir: 834 shutil.move(os.path.join(inDir, "local"), outDir) 835 836 # Restore test262/prs if necessary after a general Test262 update. 837 if restorePrsTestsDir: 838 shutil.move(os.path.join(inDir, "prs"), outDir) 839 840 841 def update_test262(args): 842 import subprocess 843 844 url = args.url 845 branch = args.branch 846 revision = args.revision 847 outDir = args.out 848 prNumber = args.pull 849 srcDir = args.local 850 851 if not os.path.isabs(outDir): 852 outDir = os.path.join(os.getcwd(), outDir) 853 854 strictTests = args.strict 855 856 # Download the requested branch in a temporary directory. 857 with TemporaryDirectory() as inDir: 858 # If it's a local import, skip the git clone parts. 859 if srcDir: 860 return fetch_local_changes(inDir, outDir, srcDir, strictTests) 861 862 if revision == "HEAD": 863 subprocess.check_call([ 864 "git", 865 "clone", 866 "--depth=1", 867 "--branch=%s" % branch, 868 url, 869 inDir, 870 ]) 871 else: 872 subprocess.check_call([ 873 "git", 874 "clone", 875 "--single-branch", 876 "--branch=%s" % branch, 877 url, 878 inDir, 879 ]) 880 subprocess.check_call(["git", "-C", inDir, "reset", "--hard", revision]) 881 882 # If a PR number is provided, fetches only the new and modified files 883 # from that PR. It also creates a new folder for that PR or replaces if 884 # it already exists, without updating the regular Test262 tests. 885 if prNumber: 886 return fetch_pr_files(inDir, outDir, prNumber, strictTests) 887 888 # Without a PR or a local import, follows through a regular copy. 889 general_update(inDir, outDir, strictTests) 890 891 892 if __name__ == "__main__": 893 import argparse 894 895 # This script must be run from js/src/tests to work correctly. 896 if "/".join(os.path.normpath(os.getcwd()).split(os.sep)[-3:]) != "js/src/tests": 897 raise RuntimeError("%s must be run from js/src/tests" % sys.argv[0]) 898 899 parser = argparse.ArgumentParser(description="Update the test262 test suite.") 900 parser.add_argument( 901 "--url", 902 default="https://github.com/tc39/test262.git", 903 help="URL to git repository (default: %(default)s)", 904 ) 905 parser.add_argument( 906 "--branch", default="main", help="Git branch (default: %(default)s)" 907 ) 908 parser.add_argument( 909 "--revision", default="HEAD", help="Git revision (default: %(default)s)" 910 ) 911 parser.add_argument( 912 "--out", 913 default="test262", 914 help="Output directory. Any existing directory will be removed!" 915 "(default: %(default)s)", 916 ) 917 parser.add_argument( 918 "--pull", help="Import contents from a Pull Request specified by its number" 919 ) 920 parser.add_argument( 921 "--local", 922 help="Import new and modified contents from a local folder, a new folder " 923 "will be created on local/branch_name", 924 ) 925 parser.add_argument( 926 "--strict", 927 default=False, 928 action="store_true", 929 help="Generate additional strict mode tests. Not enabled by default.", 930 ) 931 parser.set_defaults(func=update_test262) 932 args = parser.parse_args() 933 args.func(args)