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