tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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)