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