Bug 1885337 - Part 3: Import tests from test262 PR. r=spidermonkey-reviewers,dminor
[gecko.git] / js / src / tests / test262-update.py
blobd84916f0054402f603803af62bccb5fb39c4ff40
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 "regexp-duplicate-named-groups", # Bug 1773135
27 "json-parse-with-source", # Bug 1658310
28 "set-methods", # Bug 1805038
31 FEATURE_CHECK_NEEDED = {
32 "Atomics": "!this.hasOwnProperty('Atomics')",
33 "FinalizationRegistry": "!this.hasOwnProperty('FinalizationRegistry')",
34 "SharedArrayBuffer": "!this.hasOwnProperty('SharedArrayBuffer')",
35 "Temporal": "!this.hasOwnProperty('Temporal')",
36 "WeakRef": "!this.hasOwnProperty('WeakRef')",
37 "decorators": "!(this.hasOwnProperty('getBuildConfiguration')&&getBuildConfiguration('decorators'))", # Bug 1435869
38 "iterator-helpers": "!this.hasOwnProperty('Iterator')", # Bug 1568906
39 "Intl.Segmenter": "!Intl.Segmenter", # Bug 1423593
40 "resizable-arraybuffer": "!ArrayBuffer.prototype.resize", # Bug 1670026
41 "uint8array-base64": "!Uint8Array.fromBase64", # Bug 1862220
43 RELEASE_OR_BETA = set(
45 "symbols-as-weakmap-keys",
48 SHELL_OPTIONS = {
49 "import-assertions": "--enable-import-assertions",
50 "import-attributes": "--enable-import-attributes",
51 "ShadowRealm": "--enable-shadow-realms",
52 "iterator-helpers": "--enable-iterator-helpers",
53 "symbols-as-weakmap-keys": "--enable-symbols-as-weakmap-keys",
54 "resizable-arraybuffer": "--enable-arraybuffer-resizable",
55 "uint8array-base64": "--enable-uint8array-base64",
59 @contextlib.contextmanager
60 def TemporaryDirectory():
61 tmpDir = tempfile.mkdtemp()
62 try:
63 yield tmpDir
64 finally:
65 shutil.rmtree(tmpDir)
68 def loadTest262Parser(test262Dir):
69 """
70 Loads the test262 test record parser.
71 """
72 import importlib.machinery
73 import importlib.util
75 packagingDir = os.path.join(test262Dir, "tools", "packaging")
76 moduleName = "parseTestRecord"
78 # Create a FileFinder to load Python source files.
79 loader_details = (
80 importlib.machinery.SourceFileLoader,
81 importlib.machinery.SOURCE_SUFFIXES,
83 finder = importlib.machinery.FileFinder(packagingDir, loader_details)
85 # Find the module spec.
86 spec = finder.find_spec(moduleName)
87 if spec is None:
88 raise RuntimeError("Can't find parseTestRecord module")
90 # Create and execute the module.
91 module = importlib.util.module_from_spec(spec)
92 spec.loader.exec_module(module)
94 # Return the executed module
95 return module
98 def tryParseTestFile(test262parser, source, testName):
99 """
100 Returns the result of test262parser.parseTestRecord() or None if a parser
101 error occured.
103 See <https://github.com/tc39/test262/blob/main/INTERPRETING.md> for an
104 overview of the returned test attributes.
106 try:
107 return test262parser.parseTestRecord(source, testName)
108 except Exception as err:
109 print("Error '%s' in file: %s" % (err, testName), file=sys.stderr)
110 print("Please report this error to the test262 GitHub repository!")
111 return None
114 def createRefTestEntry(options, skip, skipIf, error, isModule, isAsync):
116 Returns the |reftest| tuple (terms, comments) from the input arguments. Or a
117 tuple of empty strings if no reftest entry is required.
120 terms = []
121 comments = []
123 if options:
124 terms.extend(options)
126 if skip:
127 terms.append("skip")
128 comments.extend(skip)
130 if skipIf:
131 terms.append("skip-if(" + "||".join([cond for (cond, _) in skipIf]) + ")")
132 comments.extend([comment for (_, comment) in skipIf])
134 if error:
135 terms.append("error:" + error)
137 if isModule:
138 terms.append("module")
140 if isAsync:
141 terms.append("async")
143 return (" ".join(terms), ", ".join(comments))
146 def createRefTestLine(terms, comments):
148 Creates the |reftest| line using the given terms and comments.
151 refTest = terms
152 if comments:
153 refTest += " -- " + comments
154 return refTest
157 def createSource(testSource, refTest, prologue, epilogue):
159 Returns the post-processed source for |testSource|.
162 source = []
164 # Add the |reftest| line.
165 if refTest:
166 source.append(b"// |reftest| " + refTest.encode("utf-8"))
168 # Prepend any directives if present.
169 if prologue:
170 source.append(prologue.encode("utf-8"))
172 source.append(testSource)
174 # Append the test epilogue, i.e. the call to "reportCompare".
175 # TODO: Does this conflict with raw tests?
176 if epilogue:
177 source.append(epilogue.encode("utf-8"))
178 source.append(b"")
180 return b"\n".join(source)
183 def writeTestFile(test262OutDir, testFileName, source):
185 Writes the test source to |test262OutDir|.
188 with io.open(os.path.join(test262OutDir, testFileName), "wb") as output:
189 output.write(source)
192 def addSuffixToFileName(fileName, suffix):
193 (filePath, ext) = os.path.splitext(fileName)
194 return filePath + suffix + ext
197 def writeShellAndBrowserFiles(
198 test262OutDir, harnessDir, includesMap, localIncludesMap, relPath
201 Generate the shell.js and browser.js files for the test harness.
204 # Find all includes from parent directories.
205 def findParentIncludes():
206 parentIncludes = set()
207 current = relPath
208 while current:
209 (parent, child) = os.path.split(current)
210 if parent in includesMap:
211 parentIncludes.update(includesMap[parent])
212 current = parent
213 return parentIncludes
215 # Find all includes, skipping includes already present in parent directories.
216 def findIncludes():
217 parentIncludes = findParentIncludes()
218 for include in includesMap[relPath]:
219 if include not in parentIncludes:
220 yield include
222 def readIncludeFile(filePath):
223 with io.open(filePath, "rb") as includeFile:
224 return b"// file: %s\n%s" % (
225 os.path.basename(filePath).encode("utf-8"),
226 includeFile.read(),
229 localIncludes = localIncludesMap[relPath] if relPath in localIncludesMap else []
231 # Concatenate all includes files.
232 includeSource = b"\n".join(
233 map(
234 readIncludeFile,
235 chain(
236 # The requested include files.
237 map(partial(os.path.join, harnessDir), sorted(findIncludes())),
238 # And additional local include files.
239 map(partial(os.path.join, os.getcwd()), sorted(localIncludes)),
244 # Write the concatenated include sources to shell.js.
245 with io.open(os.path.join(test262OutDir, relPath, "shell.js"), "wb") as shellFile:
246 if includeSource:
247 shellFile.write(b"// GENERATED, DO NOT EDIT\n")
248 shellFile.write(includeSource)
250 # The browser.js file is always empty for test262 tests.
251 with io.open(
252 os.path.join(test262OutDir, relPath, "browser.js"), "wb"
253 ) as browserFile:
254 browserFile.write(b"")
257 def pathStartsWith(path, *args):
258 prefix = os.path.join(*args)
259 return os.path.commonprefix([path, prefix]) == prefix
262 def convertTestFile(test262parser, testSource, testName, includeSet, strictTests):
264 Convert a test262 test to a compatible jstests test file.
267 # The test record dictionary, its contents are explained in depth at
268 # <https://github.com/tc39/test262/blob/main/INTERPRETING.md>.
269 testRec = tryParseTestFile(test262parser, testSource.decode("utf-8"), testName)
271 # jsreftest meta data
272 refTestOptions = []
273 refTestSkip = []
274 refTestSkipIf = []
276 # Skip all files which contain YAML errors.
277 if testRec is None:
278 refTestSkip.append("has YAML errors")
279 testRec = dict()
281 # onlyStrict is set when the test must only be run in strict mode.
282 onlyStrict = "onlyStrict" in testRec
284 # noStrict is set when the test must not be run in strict mode.
285 noStrict = "noStrict" in testRec
287 # The "raw" attribute is used in the default test262 runner to prevent
288 # prepending additional content (use-strict directive, harness files)
289 # before the actual test source code.
290 raw = "raw" in testRec
292 # Negative tests have additional meta-data to specify the error type and
293 # when the error is issued (runtime error or early parse error). We're
294 # currently ignoring the error phase attribute.
295 # testRec["negative"] == {type=<error name>, phase=parse|resolution|runtime}
296 isNegative = "negative" in testRec
297 assert not isNegative or type(testRec["negative"]) == dict
298 errorType = testRec["negative"]["type"] if isNegative else None
300 # Async tests are marked with the "async" attribute.
301 isAsync = "async" in testRec
303 # Test262 tests cannot be both "negative" and "async". (In principle a
304 # negative async test is permitted when the error phase is not "parse" or
305 # the error type is not SyntaxError, but no such tests exist now.)
306 assert not (isNegative and isAsync), (
307 "Can't have both async and negative attributes: %s" % testName
310 # Only async tests may use the $DONE or asyncTest function. However,
311 # negative parse tests may "use" the $DONE (or asyncTest) function (of
312 # course they don't actually use it!) without specifying the "async"
313 # attribute. Otherwise, neither $DONE nor asyncTest must appear in the test.
315 # Some "harness" tests redefine $DONE, so skip this check when the test file
316 # is in the "harness" directory.
317 assert (
318 (b"$DONE" not in testSource and b"asyncTest" not in testSource)
319 or isAsync
320 or isNegative
321 or testName.split(os.path.sep)[0] == "harness"
322 ), ("Missing async attribute in: %s" % testName)
324 # When the "module" attribute is set, the source code is module code.
325 isModule = "module" in testRec
327 # CanBlockIsFalse is set when the test expects that the implementation
328 # cannot block on the main thread.
329 if "CanBlockIsFalse" in testRec:
330 refTestSkipIf.append(("xulRuntime.shell", "shell can block main thread"))
332 # CanBlockIsTrue is set when the test expects that the implementation
333 # can block on the main thread.
334 if "CanBlockIsTrue" in testRec:
335 refTestSkipIf.append(("!xulRuntime.shell", "browser cannot block main thread"))
337 # Skip tests with unsupported features.
338 if "features" in testRec:
339 unsupported = [f for f in testRec["features"] if f in UNSUPPORTED_FEATURES]
340 if unsupported:
341 refTestSkip.append("%s is not supported" % ",".join(unsupported))
342 else:
343 releaseOrBeta = [f for f in testRec["features"] if f in RELEASE_OR_BETA]
344 if releaseOrBeta:
345 refTestSkipIf.append(
347 "release_or_beta",
348 "%s is not released yet" % ",".join(releaseOrBeta),
352 featureCheckNeeded = [
353 f for f in testRec["features"] if f in FEATURE_CHECK_NEEDED
355 if featureCheckNeeded:
356 refTestSkipIf.append(
358 "||".join(
359 [FEATURE_CHECK_NEEDED[f] for f in featureCheckNeeded]
361 "%s is not enabled unconditionally"
362 % ",".join(featureCheckNeeded),
366 if (
367 "Atomics" in testRec["features"]
368 and "SharedArrayBuffer" in testRec["features"]
370 refTestSkipIf.append(
372 "(this.hasOwnProperty('getBuildConfiguration')"
373 "&&getBuildConfiguration('arm64-simulator'))",
374 "ARM64 Simulator cannot emulate atomics",
378 shellOptions = {
379 SHELL_OPTIONS[f] for f in testRec["features"] if f in SHELL_OPTIONS
381 if shellOptions:
382 refTestSkipIf.append(("!xulRuntime.shell", "requires shell-options"))
383 refTestOptions.extend(
384 ("shell-option({})".format(opt) for opt in sorted(shellOptions))
387 # Includes for every test file in a directory is collected in a single
388 # shell.js file per directory level. This is done to avoid adding all
389 # test harness files to the top level shell.js file.
390 if "includes" in testRec:
391 assert not raw, "Raw test with includes: %s" % testName
392 includeSet.update(testRec["includes"])
394 # Add reportCompare() after all positive, synchronous tests.
395 if not isNegative and not isAsync:
396 testEpilogue = "reportCompare(0, 0);"
397 else:
398 testEpilogue = ""
400 if raw:
401 refTestOptions.append("test262-raw")
403 (terms, comments) = createRefTestEntry(
404 refTestOptions, refTestSkip, refTestSkipIf, errorType, isModule, isAsync
406 if raw:
407 refTest = ""
408 externRefTest = (terms, comments)
409 else:
410 refTest = createRefTestLine(terms, comments)
411 externRefTest = None
413 # Don't write a strict-mode variant for raw or module files.
414 noStrictVariant = raw or isModule
415 assert not (noStrictVariant and (onlyStrict or noStrict)), (
416 "Unexpected onlyStrict or noStrict attribute: %s" % testName
419 # Write non-strict mode test.
420 if noStrictVariant or noStrict or not onlyStrict:
421 testPrologue = ""
422 nonStrictSource = createSource(testSource, refTest, testPrologue, testEpilogue)
423 testFileName = testName
424 yield (testFileName, nonStrictSource, externRefTest)
426 # Write strict mode test.
427 if not noStrictVariant and (onlyStrict or (not noStrict and strictTests)):
428 testPrologue = "'use strict';"
429 strictSource = createSource(testSource, refTest, testPrologue, testEpilogue)
430 testFileName = testName
431 if not noStrict:
432 testFileName = addSuffixToFileName(testFileName, "-strict")
433 yield (testFileName, strictSource, externRefTest)
436 def convertFixtureFile(fixtureSource, fixtureName):
438 Convert a test262 fixture file to a compatible jstests test file.
441 # jsreftest meta data
442 refTestOptions = []
443 refTestSkip = ["not a test file"]
444 refTestSkipIf = []
445 errorType = None
446 isModule = False
447 isAsync = False
449 (terms, comments) = createRefTestEntry(
450 refTestOptions, refTestSkip, refTestSkipIf, errorType, isModule, isAsync
452 refTest = createRefTestLine(terms, comments)
454 source = createSource(fixtureSource, refTest, "", "")
455 externRefTest = None
456 yield (fixtureName, source, externRefTest)
459 def process_test262(test262Dir, test262OutDir, strictTests, externManifests):
461 Process all test262 files and converts them into jstests compatible tests.
464 harnessDir = os.path.join(test262Dir, "harness")
465 testDir = os.path.join(test262Dir, "test")
466 test262parser = loadTest262Parser(test262Dir)
468 # Map of test262 subdirectories to the set of include files required for
469 # tests in that subdirectory. The includes for all tests in a subdirectory
470 # are merged into a single shell.js.
471 # map<dirname, set<includeFiles>>
472 includesMap = {}
474 # Additional local includes keyed by test262 directory names. The include
475 # files in this map must be located in the js/src/tests directory.
476 # map<dirname, list<includeFiles>>
477 localIncludesMap = {}
479 # The root directory contains required harness files and test262-host.js.
480 includesMap[""] = set(["sta.js", "assert.js"])
481 localIncludesMap[""] = ["test262-host.js"]
483 # Also add files known to be used by many tests to the root shell.js file.
484 includesMap[""].update(["propertyHelper.js", "compareArray.js"])
486 # Write the root shell.js file.
487 writeShellAndBrowserFiles(
488 test262OutDir, harnessDir, includesMap, localIncludesMap, ""
491 # Additional explicit includes inserted at well-chosen locations to reduce
492 # code duplication in shell.js files.
493 explicitIncludes = {}
494 explicitIncludes[os.path.join("built-ins", "Atomics")] = [
495 "testAtomics.js",
496 "testTypedArray.js",
498 explicitIncludes[os.path.join("built-ins", "DataView")] = [
499 "byteConversionValues.js"
501 explicitIncludes[os.path.join("built-ins", "Promise")] = ["promiseHelper.js"]
502 explicitIncludes[os.path.join("built-ins", "Temporal")] = ["temporalHelpers.js"]
503 explicitIncludes[os.path.join("built-ins", "TypedArray")] = [
504 "byteConversionValues.js",
505 "detachArrayBuffer.js",
506 "nans.js",
508 explicitIncludes[os.path.join("built-ins", "TypedArrays")] = [
509 "detachArrayBuffer.js"
511 explicitIncludes[os.path.join("built-ins", "Temporal")] = ["temporalHelpers.js"]
513 # Process all test directories recursively.
514 for dirPath, dirNames, fileNames in os.walk(testDir):
515 relPath = os.path.relpath(dirPath, testDir)
516 if relPath == ".":
517 continue
519 # Skip creating a "prs" directory if it already exists
520 if relPath not in ("prs", "local") and not os.path.exists(
521 os.path.join(test262OutDir, relPath)
523 os.makedirs(os.path.join(test262OutDir, relPath))
525 includeSet = set()
526 includesMap[relPath] = includeSet
528 if relPath in explicitIncludes:
529 includeSet.update(explicitIncludes[relPath])
531 # Convert each test file.
532 for fileName in fileNames:
533 filePath = os.path.join(dirPath, fileName)
534 testName = os.path.relpath(filePath, testDir)
536 # Copy non-test files as is.
537 (_, fileExt) = os.path.splitext(fileName)
538 if fileExt != ".js":
539 shutil.copyfile(filePath, os.path.join(test262OutDir, testName))
540 continue
542 # Files ending with "_FIXTURE.js" are fixture files:
543 # https://github.com/tc39/test262/blob/main/INTERPRETING.md#modules
544 isFixtureFile = fileName.endswith("_FIXTURE.js")
546 # Read the original test source and preprocess it for the jstests harness.
547 with io.open(filePath, "rb") as testFile:
548 testSource = testFile.read()
550 if isFixtureFile:
551 convert = convertFixtureFile(testSource, testName)
552 else:
553 convert = convertTestFile(
554 test262parser, testSource, testName, includeSet, strictTests
557 for newFileName, newSource, externRefTest in convert:
558 writeTestFile(test262OutDir, newFileName, newSource)
560 if externRefTest is not None:
561 externManifests.append(
563 "name": newFileName,
564 "reftest": externRefTest,
568 # Add shell.js and browers.js files for the current directory.
569 writeShellAndBrowserFiles(
570 test262OutDir, harnessDir, includesMap, localIncludesMap, relPath
574 def fetch_local_changes(inDir, outDir, srcDir, strictTests):
576 Fetch the changes from a local clone of Test262.
578 1. Get the list of file changes made by the current branch used on Test262 (srcDir).
579 2. Copy only the (A)dded, (C)opied, (M)odified, and (R)enamed files to inDir.
580 3. inDir is treated like a Test262 checkout, where files will be converted.
581 4. Fetches the current branch name to set the outDir.
582 5. Processed files will be added to `<outDir>/local/<branchName>`.
584 import subprocess
586 # TODO: fail if it's in the default branch? or require a branch name?
587 # Checks for unstaged or non committed files. A clean branch provides a clean status.
588 status = subprocess.check_output(
589 ("git -C %s status --porcelain" % srcDir).split(" ")
592 if status.strip():
593 raise RuntimeError(
594 "Please commit files and cleanup the local test262 folder before importing files.\n"
595 "Current status: \n%s" % status
598 # Captures the branch name to be used on the output
599 branchName = subprocess.check_output(
600 ("git -C %s rev-parse --abbrev-ref HEAD" % srcDir).split(" ")
601 ).split("\n")[0]
603 # Fetches the file names to import
604 files = subprocess.check_output(
605 ("git -C %s diff main --diff-filter=ACMR --name-only" % srcDir).split(" ")
608 # Fetches the deleted files to print an output log. This can be used to
609 # set up the skip list, if necessary.
610 deletedFiles = subprocess.check_output(
611 ("git -C %s diff main --diff-filter=D --name-only" % srcDir).split(" ")
614 # Fetches the modified files as well for logging to support maintenance
615 # in the skip list.
616 modifiedFiles = subprocess.check_output(
617 ("git -C %s diff main --diff-filter=M --name-only" % srcDir).split(" ")
620 # Fetches the renamed files for the same reason, this avoids duplicate
621 # tests if running the new local folder and the general imported Test262
622 # files.
623 renamedFiles = subprocess.check_output(
624 ("git -C %s diff main --diff-filter=R --summary" % srcDir).split(" ")
627 # Print some friendly output
628 print("From the branch %s in %s \n" % (branchName, srcDir))
629 print("Files being copied to the local folder: \n%s" % files)
630 if deletedFiles:
631 print(
632 "Deleted files (use this list to update the skip list): \n%s" % deletedFiles
634 if modifiedFiles:
635 print(
636 "Modified files (use this list to update the skip list): \n%s"
637 % modifiedFiles
639 if renamedFiles:
640 print("Renamed files (already added with the new names): \n%s" % renamedFiles)
642 for f in files.splitlines():
643 # Capture the subdirectories names to recreate the file tree
644 # TODO: join the file tree with -- instead of multiple subfolders?
645 fileTree = os.path.join(inDir, os.path.dirname(f))
646 if not os.path.exists(fileTree):
647 os.makedirs(fileTree)
649 shutil.copyfile(
650 os.path.join(srcDir, f), os.path.join(fileTree, os.path.basename(f))
653 # Extras from Test262. Copy the current support folders - including the
654 # harness - for a proper conversion process
655 shutil.copytree(os.path.join(srcDir, "tools"), os.path.join(inDir, "tools"))
656 shutil.copytree(os.path.join(srcDir, "harness"), os.path.join(inDir, "harness"))
658 # Reset any older directory in the output using the same branch name
659 outDir = os.path.join(outDir, "local", branchName)
660 if os.path.isdir(outDir):
661 shutil.rmtree(outDir)
662 os.makedirs(outDir)
664 process_test262(inDir, outDir, strictTests, [])
667 def fetch_pr_files(inDir, outDir, prNumber, strictTests):
668 import requests
670 prTestsOutDir = os.path.join(outDir, "prs", prNumber)
671 if os.path.isdir(prTestsOutDir):
672 print("Removing folder %s" % prTestsOutDir)
673 shutil.rmtree(prTestsOutDir)
674 os.makedirs(prTestsOutDir)
676 # Reuses current Test262 clone's harness and tools folders only, the clone's test/
677 # folder can be discarded from here
678 shutil.rmtree(os.path.join(inDir, "test"))
680 prRequest = requests.get(
681 "https://api.github.com/repos/tc39/test262/pulls/%s" % prNumber
683 prRequest.raise_for_status()
685 pr = prRequest.json()
687 if pr["state"] != "open":
688 # Closed PR, remove respective files from folder
689 return print("PR %s is closed" % prNumber)
691 url = "https://api.github.com/repos/tc39/test262/pulls/%s/files" % prNumber
692 hasNext = True
694 while hasNext:
695 files = requests.get(url)
696 files.raise_for_status()
698 for item in files.json():
699 if not item["filename"].startswith("test/"):
700 continue
702 filename = item["filename"]
703 fileStatus = item["status"]
705 print("%s %s" % (fileStatus, filename))
707 # Do not add deleted files
708 if fileStatus == "removed":
709 continue
711 contents = requests.get(item["raw_url"])
712 contents.raise_for_status()
714 fileText = contents.text
716 filePathDirs = os.path.join(inDir, *filename.split("/")[:-1])
718 if not os.path.isdir(filePathDirs):
719 os.makedirs(filePathDirs)
721 with io.open(
722 os.path.join(inDir, *filename.split("/")), "wb"
723 ) as output_file:
724 output_file.write(fileText.encode("utf8"))
726 hasNext = False
728 # Check if the pull request changes are split over multiple pages.
729 if "link" in files.headers:
730 link = files.headers["link"]
732 # The links are comma separated and the entries within a link are separated by a
733 # semicolon. For example the first two links entries for PR 3199:
735 # https://api.github.com/repos/tc39/test262/pulls/3199/files
736 # """
737 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=2>; rel="next",
738 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=14>; rel="last"
739 # """
741 # https://api.github.com/repositories/16147933/pulls/3199/files?page=2
742 # """
743 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=1>; rel="prev",
744 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=3>; rel="next",
745 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=14>; rel="last",
746 # <https://api.github.com/repositories/16147933/pulls/3199/files?page=1>; rel="first"
747 # """
749 for pages in link.split(", "):
750 (pageUrl, rel) = pages.split("; ")
752 assert pageUrl[0] == "<"
753 assert pageUrl[-1] == ">"
755 # Remove the angle brackets around the URL.
756 pageUrl = pageUrl[1:-1]
758 # Make sure we only request data from github and not some other place.
759 assert pageUrl.startswith("https://api.github.com/")
761 # Ensure the relative URL marker has the expected format.
762 assert (
763 rel == 'rel="prev"'
764 or rel == 'rel="next"'
765 or rel == 'rel="first"'
766 or rel == 'rel="last"'
769 # We only need the URL for the next page.
770 if rel == 'rel="next"':
771 url = pageUrl
772 hasNext = True
774 process_test262(inDir, prTestsOutDir, strictTests, [])
777 def general_update(inDir, outDir, strictTests):
778 import subprocess
780 restoreLocalTestsDir = False
781 restorePrsTestsDir = False
782 localTestsOutDir = os.path.join(outDir, "local")
783 prsTestsOutDir = os.path.join(outDir, "prs")
785 # Stash test262/local and test262/prs. Currently the Test262 repo does not have any
786 # top-level subdirectories named "local" or "prs".
787 # This prevents these folders from being removed during the update process.
788 if os.path.isdir(localTestsOutDir):
789 shutil.move(localTestsOutDir, inDir)
790 restoreLocalTestsDir = True
792 if os.path.isdir(prsTestsOutDir):
793 shutil.move(prsTestsOutDir, inDir)
794 restorePrsTestsDir = True
796 # Create the output directory from scratch.
797 if os.path.isdir(outDir):
798 shutil.rmtree(outDir)
799 os.makedirs(outDir)
801 # Copy license file.
802 shutil.copyfile(os.path.join(inDir, "LICENSE"), os.path.join(outDir, "LICENSE"))
804 # Create the git info file.
805 with io.open(os.path.join(outDir, "GIT-INFO"), "w", encoding="utf-8") as info:
806 subprocess.check_call(["git", "-C", inDir, "log", "-1"], stdout=info)
808 # Copy the test files.
809 externManifests = []
810 process_test262(inDir, outDir, strictTests, externManifests)
812 # Create the external reftest manifest file.
813 with io.open(os.path.join(outDir, "jstests.list"), "wb") as manifestFile:
814 manifestFile.write(b"# GENERATED, DO NOT EDIT\n\n")
815 for externManifest in sorted(externManifests, key=itemgetter("name")):
816 (terms, comments) = externManifest["reftest"]
817 if terms:
818 entry = "%s script %s%s\n" % (
819 terms,
820 externManifest["name"],
821 (" # %s" % comments) if comments else "",
823 manifestFile.write(entry.encode("utf-8"))
825 # Move test262/local back.
826 if restoreLocalTestsDir:
827 shutil.move(os.path.join(inDir, "local"), outDir)
829 # Restore test262/prs if necessary after a general Test262 update.
830 if restorePrsTestsDir:
831 shutil.move(os.path.join(inDir, "prs"), outDir)
834 def update_test262(args):
835 import subprocess
837 url = args.url
838 branch = args.branch
839 revision = args.revision
840 outDir = args.out
841 prNumber = args.pull
842 srcDir = args.local
844 if not os.path.isabs(outDir):
845 outDir = os.path.join(os.getcwd(), outDir)
847 strictTests = args.strict
849 # Download the requested branch in a temporary directory.
850 with TemporaryDirectory() as inDir:
851 # If it's a local import, skip the git clone parts.
852 if srcDir:
853 return fetch_local_changes(inDir, outDir, srcDir, strictTests)
855 if revision == "HEAD":
856 subprocess.check_call(
857 ["git", "clone", "--depth=1", "--branch=%s" % branch, url, inDir]
859 else:
860 subprocess.check_call(
861 ["git", "clone", "--single-branch", "--branch=%s" % branch, url, inDir]
863 subprocess.check_call(["git", "-C", inDir, "reset", "--hard", revision])
865 # If a PR number is provided, fetches only the new and modified files
866 # from that PR. It also creates a new folder for that PR or replaces if
867 # it already exists, without updating the regular Test262 tests.
868 if prNumber:
869 return fetch_pr_files(inDir, outDir, prNumber, strictTests)
871 # Without a PR or a local import, follows through a regular copy.
872 general_update(inDir, outDir, strictTests)
875 if __name__ == "__main__":
876 import argparse
878 # This script must be run from js/src/tests to work correctly.
879 if "/".join(os.path.normpath(os.getcwd()).split(os.sep)[-3:]) != "js/src/tests":
880 raise RuntimeError("%s must be run from js/src/tests" % sys.argv[0])
882 parser = argparse.ArgumentParser(description="Update the test262 test suite.")
883 parser.add_argument(
884 "--url",
885 default="https://github.com/tc39/test262.git",
886 help="URL to git repository (default: %(default)s)",
888 parser.add_argument(
889 "--branch", default="main", help="Git branch (default: %(default)s)"
891 parser.add_argument(
892 "--revision", default="HEAD", help="Git revision (default: %(default)s)"
894 parser.add_argument(
895 "--out",
896 default="test262",
897 help="Output directory. Any existing directory will be removed!"
898 "(default: %(default)s)",
900 parser.add_argument(
901 "--pull", help="Import contents from a Pull Request specified by its number"
903 parser.add_argument(
904 "--local",
905 help="Import new and modified contents from a local folder, a new folder "
906 "will be created on local/branch_name",
908 parser.add_argument(
909 "--strict",
910 default=False,
911 action="store_true",
912 help="Generate additional strict mode tests. Not enabled by default.",
914 parser.set_defaults(func=update_test262)
915 args = parser.parse_args()
916 args.func(args)