tor-browser

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

test262-export.py (24188B)


      1 #!/usr/bin/env python3
      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 math
      8 import os
      9 import re
     10 import shutil
     11 import sys
     12 import traceback
     13 from datetime import date
     14 from typing import Any, Optional
     15 
     16 import yaml
     17 
     18 # Skip all common files used to support tests for jstests
     19 # These files are listed in the README.txt
     20 SUPPORT_FILES = set([
     21    "browser.js",
     22    "shell.js",
     23    "template.js",
     24    "user.js",
     25    "js-test-driver-begin.js",
     26    "js-test-driver-end.js",
     27 ])
     28 
     29 
     30 # Run once per subdirectory
     31 def findAndCopyIncludes(dirPath: str, baseDir: str, includeDir: str) -> "list[str]":
     32    relPath = os.path.relpath(dirPath, baseDir)
     33    includes: list[str] = ["sm/non262.js"]
     34    os.makedirs(os.path.join(includeDir, "sm"), exist_ok=True)
     35 
     36    # Recurse down all folders in the relative path until
     37    # we reach the base directory of shell.js include files.
     38    # Each directory will have a shell.js file to copy.
     39    while relPath:
     40        # find the shell.js
     41        shellFile = os.path.join(baseDir, relPath, "shell.js")
     42 
     43        # if the file isn't excluded, exists, and is not empty, include in includes
     44        if (
     45            not any(relPath == f"non262/{path}" for path in UNSUPPORTED_PATHS)
     46            and os.path.exists(shellFile)
     47            and os.path.getsize(shellFile) > 0
     48        ):
     49            with open(shellFile, "rb") as f:
     50                testSource = f.read()
     51 
     52            if b"// SKIP test262 export" not in testSource:
     53                # create new shell.js file name
     54                includeFileName = "sm/" + relPath.replace("/", "-") + "-shell.js"
     55                includes.append(includeFileName)
     56 
     57                includesPath = os.path.join(includeDir, includeFileName)
     58                shutil.copyfile(shellFile, includesPath)
     59 
     60        relPath = os.path.split(relPath)[0]
     61 
     62    return includes
     63 
     64 
     65 UNSUPPORTED_CODE: "list[bytes]" = [
     66    b"// SKIP test262 export",
     67    b"inTimeZone(",
     68    b"getTimeZone(",
     69    b"setTimeZone(",
     70    b"getAvailableLocalesOf(",
     71    b"uneval(",
     72    b"Debugger",
     73    b"SpecialPowers",
     74    b"evalcx(",
     75    b"evaluate(",
     76    b"drainJobQueue(",
     77    b"getPromiseResult(",
     78    b"assertEventuallyEq(",
     79    b"assertEventuallyThrows(",
     80    b"settlePromiseNow(",
     81    b"setPromiseRejectionTrackerCallback",
     82    b"displayName(",
     83    b"InternalError",
     84    b"toSource(",
     85    b"toSource.call(",
     86    b"isRope(",
     87    b"isSameCompartment(",
     88    b"isCCW",
     89    b"nukeCCW",
     90    b"representativeStringArray(",
     91    b"largeArrayBufferSupported(",
     92    b"helperThreadCount(",
     93    b"serialize(",
     94    b"deserialize(",
     95    b"clone_object_check",
     96    b"grayRoot(",
     97    b"blackRoot(",
     98    b"gczeal",
     99    b"getSelfHostedValue(",
    100    b"oomTest(",
    101    b"assertLineAndColumn(",
    102    b"wrapWithProto(",
    103    b"Reflect.parse(",
    104    b"relazifyFunctions(",
    105    b"ignoreUnhandledRejections",
    106    b".lineNumber",
    107    b"expectExitCode",
    108    b"loadRelativeToScript",
    109    b"XorShiftGenerator",
    110 ]
    111 
    112 
    113 def skipTest(source: bytes) -> Optional[bytes]:
    114    if b"This Source Code Form is subject to the terms of the Mozilla Public" in source:
    115        return b"MPL license"
    116    for c in UNSUPPORTED_CODE:
    117        if c in source:
    118            return c
    119 
    120    return None
    121 
    122 
    123 MODELINE_PATTERN = re.compile(rb"/(/|\*) -\*- .* -\*-( \*/)?[\r\n]+")
    124 
    125 UNSUPPORTED_FEATURES = [
    126    "async-iterator-helpers",
    127    "Iterator.range",
    128 ]
    129 
    130 UNSUPPORTED_PATHS = [
    131    "Intl",
    132    "Temporal/Intl",
    133    "reflect-parse",
    134    "extensions/empty.txt",
    135    "extensions/file-mapped-arraybuffers.txt",
    136    "module/bug1693261-async.mjs",
    137    "module/bug1693261-c1.mjs",
    138    "module/bug1693261-c2.mjs",
    139    "module/bug1693261-x.mjs",
    140 ]
    141 
    142 
    143 def convertTestFile(source: bytes, includes: "list[str]") -> Optional[bytes]:
    144    """
    145    Convert a jstest test to a compatible Test262 test file.
    146    """
    147 
    148    source = MODELINE_PATTERN.sub(b"", source)
    149 
    150    # Extract the reftest data from the source
    151    source, reftest = parseHeader(source)
    152 
    153    # Add copyright, if needed.
    154    copyright, source = insertCopyrightLines(source)
    155 
    156    # Extract the frontmatter data from the source
    157    frontmatter, source = extractMeta(source)
    158 
    159    source, addincludes = translateHelpers(source)
    160    includes = includes + addincludes
    161 
    162    meta = computeMeta(source, reftest, frontmatter, includes)
    163 
    164    if "features" in meta and any(f in UNSUPPORTED_FEATURES for f in meta["features"]):
    165        return None
    166 
    167    source = insertMeta(source, meta)
    168 
    169    source = convertReportCompare(source)
    170 
    171    return copyright + source
    172 
    173 
    174 ## parseHeader
    175 
    176 
    177 class ReftestEntry:
    178    def __init__(
    179        self,
    180        features: "list[str]",
    181        error: Optional[str],
    182        module: bool,
    183        info: Optional[str],
    184    ):
    185        self.features: list[str] = features
    186        self.error: Optional[str] = error
    187        self.module: bool = module
    188        self.info: Optional[str] = info
    189 
    190 
    191 def featureFromReftest(reftest: str) -> Optional[str]:
    192    if reftest == "Iterator":
    193        return "iterator-helpers"
    194    if reftest == "AsyncIterator":
    195        return "async-iterator-helpers"
    196    if reftest == "Temporal":
    197        return "Temporal"
    198    if reftest in ("Intl", "addIntlExtras"):
    199        return None
    200    raise Exception(f"Unexpected feature {reftest}")
    201 
    202 
    203 def fetchReftestEntries(reftest: str) -> ReftestEntry:
    204    """
    205    Collects and stores the entries from the reftest header.
    206    """
    207 
    208    # TODO: fails, slow, skip, random, random-if
    209 
    210    features: list[str] = []
    211    error: Optional[str] = None
    212    comments: Optional[str] = None
    213    module: bool = False
    214 
    215    # should capture conditions to skip
    216    matchesSkip = re.search(r"skip-if\((.*)\)", reftest)
    217    if matchesSkip:
    218        matches = matchesSkip.group(1).split("||")
    219        for match in matches:
    220            # captures a features list
    221            dependsOnProp = re.search(
    222                r"!this.hasOwnProperty\([\'\"](.*?)[\'\"]\)", match
    223            )
    224            if dependsOnProp:
    225                if feature := featureFromReftest(dependsOnProp.group(1)):
    226                    features.append(feature)
    227            else:
    228                print("# Can't parse the following skip-if rule: %s" % match)
    229 
    230    # should capture the expected error
    231    matchesError = re.search(r"error:\s*(\w*)", reftest)
    232    if matchesError:
    233        # The metadata from the reftests won't say if it's a runtime or an
    234        # early error. This specification is required for the frontmatter tags.
    235        error = matchesError.group(1)
    236 
    237    # just tells if it's a module
    238    matchesModule = re.search(r"\bmodule\b", reftest)
    239    if matchesModule:
    240        module = True
    241 
    242    # captures any comments
    243    matchesComments = re.search(r" -- (.*)", reftest)
    244    if matchesComments:
    245        comments = matchesComments.group(1)
    246 
    247    return ReftestEntry(features=features, error=error, module=module, info=comments)
    248 
    249 
    250 def parseHeader(source: bytes) -> "tuple[bytes, Optional[ReftestEntry]]":
    251    """
    252    Parse the source to return it with the extracted the header
    253    """
    254    from lib.manifest import TEST_HEADER_PATTERN_INLINE
    255 
    256    # Bail early if we do not start with a single comment.
    257    if not source.startswith(b"//"):
    258        return (source, None)
    259 
    260    # Extract the token.
    261    part, _, rest = source.partition(b"\n")
    262    part = part.decode("utf-8")
    263    matches = TEST_HEADER_PATTERN_INLINE.match(part)
    264 
    265    if matches and matches.group(0):
    266        reftest = matches.group(0)
    267 
    268        # Remove the found header from the source;
    269        # Fetch and return the reftest entries
    270        return (rest, fetchReftestEntries(reftest))
    271 
    272    return (source, None)
    273 
    274 
    275 ## insertCopyrightLines
    276 
    277 
    278 LICENSE_PATTERN = re.compile(
    279    rb"// Copyright( \([C]\))? (\w+) .+\. {1,2}All rights reserved\.[\r\n]{1,2}"
    280    + rb"("
    281    + rb"// This code is governed by the( BSD)? license found in the LICENSE file\."
    282    + rb"|"
    283    + rb"// See LICENSE for details."
    284    + rb"|"
    285    + rb"// Use of this source code is governed by a BSD-style license that can be[\r\n]{1,2}"
    286    + rb"// found in the LICENSE file\."
    287    + rb"|"
    288    + rb"// See LICENSE or https://github\.com/tc39/test262/blob/HEAD/LICENSE"
    289    + rb")[\r\n]{1,2}",
    290    re.IGNORECASE,
    291 )
    292 
    293 PD_PATTERN1 = re.compile(
    294    rb"/\*[\r\n]{1,2}"
    295    + rb" \* Any copyright is dedicated to the Public Domain\.[\r\n]{1,2}"
    296    + rb" \* (http://creativecommons\.org/licenses/publicdomain/|https://creativecommons\.org/publicdomain/zero/1\.0/)[\r\n]{1,2}"
    297    + rb"( \* Contributors?:"
    298    + rb"(( [^\r\n]*[\r\n]{1,2})|"
    299    + rb"([\r\n]{1,2}( \* [^\r\n]*[\r\n]{1,2})+)))?"
    300    + rb" \*/[\r\n]{1,2}",
    301    re.IGNORECASE,
    302 )
    303 
    304 PD_PATTERN2 = re.compile(
    305    rb"// Any copyright is dedicated to the Public Domain\.[\r\n]{1,2}"
    306    + rb"// (http://creativecommons\.org/licenses/publicdomain/|https://creativecommons\.org/publicdomain/zero/1\.0/)[\r\n]{1,2}"
    307    + rb"(// Contributors?: [^\r\n]*[\r\n]{1,2})?",
    308    re.IGNORECASE,
    309 )
    310 
    311 PD_PATTERN3 = re.compile(
    312    rb"/\* Any copyright is dedicated to the Public Domain\.[\r\n]{1,2}"
    313    + rb" \* (http://creativecommons\.org/licenses/publicdomain/|https://creativecommons\.org/publicdomain/zero/1\.0/) \*/[\r\n]{1,2}",
    314    re.IGNORECASE,
    315 )
    316 
    317 
    318 BSD_TEMPLATE = (
    319    b"""\
    320 // Copyright (C) %d Mozilla Corporation. All rights reserved.
    321 // This code is governed by the BSD license found in the LICENSE file.
    322 
    323 """
    324    % date.today().year
    325 )
    326 
    327 PD_TEMPLATE = b"""\
    328 /*
    329 * Any copyright is dedicated to the Public Domain.
    330 * http://creativecommons.org/licenses/publicdomain/
    331 */
    332 
    333 """
    334 
    335 
    336 def insertCopyrightLines(source: bytes) -> "tuple[bytes, bytes]":
    337    """
    338    Insert the copyright lines into the file.
    339    """
    340    if match := LICENSE_PATTERN.search(source):
    341        start, end = match.span()
    342        return source[start:end], source[:start] + source[end:]
    343 
    344    if (
    345        match := PD_PATTERN1.search(source)
    346        or PD_PATTERN2.search(source)
    347        or PD_PATTERN3.search(source)
    348    ):
    349        start, end = match.span()
    350        return PD_TEMPLATE, source[:start] + source[end:]
    351 
    352    return BSD_TEMPLATE, source
    353 
    354 
    355 ## extractMeta
    356 
    357 FRONTMATTER_WRAPPER_PATTERN = re.compile(
    358    rb"/\*\---\n([\s]*)((?:\s|\S)*)[\n\s*]---\*/[\r\n]{1,2}", flags=re.DOTALL
    359 )
    360 
    361 
    362 def extractMeta(source: bytes) -> "tuple[dict[str, Any], bytes]":
    363    """
    364    Capture the frontmatter metadata as yaml if it exists.
    365    Returns a new dict if it doesn't.
    366    """
    367 
    368    match = FRONTMATTER_WRAPPER_PATTERN.search(source)
    369    if not match:
    370        return {}, source
    371 
    372    indent, frontmatter_lines = match.groups()
    373 
    374    unindented = re.sub(b"^%s" % indent, b"", frontmatter_lines)
    375 
    376    yamlresult = yaml.safe_load(unindented)
    377    if isinstance(yamlresult, str):
    378        result = {"info": yamlresult}
    379    else:
    380        result = yamlresult
    381    start, end = match.span()
    382    return result, source[:start] + source[end:]
    383 
    384 
    385 ## computeMeta
    386 
    387 
    388 def translateHelpers(source: bytes) -> "tuple[bytes, list[str]]":
    389    """
    390    Translate SpiderMonkey helper methods that have standard variants in test262.
    391    This also returns a list of includes that are needed to use these variants in test262, if any.
    392    """
    393 
    394    includes: list[str] = []
    395    source, n = re.subn(rb"\bassertDeepEq\b", b"assert.deepEqual", source)
    396    if n:
    397        includes.append("deepEqual.js")
    398 
    399    source, n = re.subn(rb"\bassertEqArray\b", b"assert.compareArray", source)
    400    if n:
    401        includes.append("compareArray.js")
    402 
    403    source = re.sub(rb"\bdetachArrayBuffer\b", b"$262.detachArrayBuffer", source)
    404    source = re.sub(rb"\bnewGlobal\b", b"createNewGlobal", source)
    405    source = re.sub(rb"\bassertEq\b", b"assert.sameValue", source)
    406 
    407    return (source, includes)
    408 
    409 
    410 def mergeMeta(
    411    reftest: "Optional[ReftestEntry]",
    412    frontmatter: "dict[str, Any]",
    413    includes: "list[str]",
    414 ) -> "dict[str, Any]":
    415    """
    416    Merge the metadata from reftest and an existing frontmatter and populate
    417    required frontmatter fields properly.
    418    """
    419 
    420    # Merge the meta from reftest to the frontmatter
    421 
    422    # Add the shell specific includes
    423    if includes:
    424        frontmatter["includes"] = frontmatter.get("includes", []) + list(includes)
    425 
    426    flags: list[str] = frontmatter.get("flags", [])
    427    if "noStrict" not in flags and "onlyStrict" not in flags:
    428        frontmatter.setdefault("flags", []).append("noStrict")
    429 
    430    if not reftest:
    431        return frontmatter
    432 
    433    frontmatter.setdefault("features", []).extend(reftest.features)
    434 
    435    # Only add the module flag if the value from reftest is truish
    436    if reftest.module:
    437        frontmatter.setdefault("flags", []).append("module")
    438        if "noStrict" in frontmatter["flags"]:
    439            frontmatter["flags"].remove("noStrict")
    440        if "onlyStrict" in frontmatter["flags"]:
    441            frontmatter["flags"].remove("onlyStrict")
    442 
    443    # Add any comments to the info tag
    444    if reftest.info:
    445        info = reftest.info
    446        # Open some space in an existing info text
    447        if "info" in frontmatter and frontmatter["info"]:
    448            frontmatter["info"] += "\n\n%s" % info
    449        else:
    450            frontmatter["info"] = info
    451 
    452    # Set the negative flags
    453    if reftest.error:
    454        error = reftest.error
    455        if "negative" not in frontmatter:
    456            frontmatter["negative"] = {
    457                # This code is assuming error tags are parse errors, but they
    458                # might be runtime errors as well.
    459                # From this point, this code can also print a warning asking to
    460                # specify the error phase in the generated code or fill the
    461                # phase with an empty string.
    462                "phase": "parse",
    463                "type": error,
    464            }
    465        # Print a warning if the errors don't match
    466        elif frontmatter["negative"].get("type") != error:
    467            print(
    468                "Warning: The reftest error doesn't match the existing "
    469                + "frontmatter error. %s != %s"
    470                % (error, frontmatter["negative"]["type"])
    471            )
    472 
    473    return frontmatter
    474 
    475 
    476 def cleanupMeta(meta: "dict[str, Any]") -> "dict[str, Any]":
    477    """
    478    Clean up all the frontmatter meta tags. This is not a lint tool, just a
    479    simple cleanup to remove trailing spaces and duplicate entries from lists.
    480    """
    481 
    482    # Trim values on each string tag
    483    for tag in ("description", "esid", "es5id", "es6id", "info", "author"):
    484        if tag in meta:
    485            if not meta[tag]:
    486                del meta[tag]
    487            else:
    488                meta[tag] = meta[tag].strip()
    489                if not len(meta[tag]):
    490                    del meta[tag]
    491 
    492    # Populate required tags
    493    for tag in ("description", "esid"):
    494        meta.setdefault(tag, "pending")
    495 
    496    # Remove duplicate entries on each list tag
    497    for tag in ("features", "flags", "includes"):
    498        if tag in meta:
    499            if not meta[tag]:
    500                del meta[tag]
    501            else:
    502                # We need the list back for the yaml dump
    503                meta[tag] = sorted(set(meta[tag]), reverse=tag == "includes")
    504                if not len(meta[tag]):
    505                    del meta[tag]
    506 
    507    if "negative" in meta:
    508        # If the negative tag exists, phase needs to be present and set
    509        if meta["negative"].get("phase") not in ["parse", "resolution", "runtime"]:
    510            print(
    511                "Warning: the negative.phase is not properly set.\n"
    512                + "Ref https://github.com/tc39/test262/blob/main/INTERPRETING.md#negative"
    513            )
    514        # If the negative tag exists, type is required
    515        if "type" not in meta["negative"]:
    516            print(
    517                "Warning: the negative.type is not set.\n"
    518                + "Ref https://github.com/tc39/test262/blob/main/INTERPRETING.md#negative"
    519            )
    520 
    521    return meta
    522 
    523 
    524 def insertMeta(source: bytes, frontmatter: "dict[str, Any]") -> bytes:
    525    """
    526    Insert the formatted frontmatter into the file, use the current existing
    527    space if any
    528    """
    529    lines: list[bytes] = []
    530 
    531    lines.append(b"/*---")
    532 
    533    for key, value in frontmatter.items():
    534        if key in ("description", "info"):
    535            lines.append(b"%s: |" % key.encode("ascii"))
    536            lines.append(
    537                yaml.dump(
    538                    value,
    539                    encoding="utf8",
    540                    default_style="|",
    541                    default_flow_style=False,
    542                    allow_unicode=True,
    543                )
    544                .strip()
    545                .replace(b"|-\n", b"")
    546            )
    547        elif key in ["flags", "includes", "features"]:
    548            lines.append(
    549                b"%s: " % key.encode("ascii")
    550                + yaml.dump(
    551                    value, encoding="utf8", default_flow_style=True, width=math.inf
    552                ).strip()
    553            )
    554        else:
    555            lines.append(
    556                yaml.dump(
    557                    {key: value}, encoding="utf8", default_flow_style=False
    558                ).strip()
    559            )
    560 
    561    lines.append(b"---*/\n")
    562    source = b"\n".join(lines) + source
    563 
    564    if frontmatter.get("negative", {}).get("phase", "") == "parse":
    565        source += b"$DONOTEVALUATE();\n"
    566 
    567    return source
    568 
    569 
    570 def computeMeta(
    571    source: bytes,
    572    reftest: "Optional[ReftestEntry]",
    573    frontmatter: "dict[str, Any]",
    574    includes: "list[str]",
    575 ) -> "dict[str, Any]":
    576    """
    577    Captures the reftest meta and a pre-existing meta if any and merge them
    578    into a single dict.
    579    """
    580 
    581    if source.startswith((b'"use strict"', b"'use strict'")):
    582        frontmatter.setdefault("flags", []).append("onlyStrict")
    583 
    584    if b"createIsHTMLDDA" in source:
    585        frontmatter.setdefault("features", []).append("IsHTMLDDA")
    586 
    587    # Merge the reftest and frontmatter
    588    merged = mergeMeta(reftest, frontmatter, includes)
    589 
    590    # Cleanup the metadata
    591    return cleanupMeta(merged)
    592 
    593 
    594 ## convertReportCompare
    595 
    596 
    597 def convertReportCompare(source: bytes) -> bytes:
    598    """
    599    Captures all the reportCompare and convert them accordingly.
    600 
    601    Cases with reportCompare calls where the arguments are the same and one of
    602    0, true, or null, will be discarded as they are not necessary for Test262.
    603 
    604    Otherwise, reportCompare will be replaced with assert.sameValue, as the
    605    equivalent in Test262
    606    """
    607 
    608    def replaceFn(matchobj: "re.Match[bytes]") -> bytes:
    609        actual: bytes = matchobj.group(4)
    610        expected: bytes = matchobj.group(5)
    611 
    612        if actual == expected and actual in [b"0", b"true", b"null"]:
    613            return b""
    614 
    615        return matchobj.group()
    616 
    617    newSource = re.sub(
    618        rb".*(if \(typeof reportCompare ===? (\"|')function(\"|')\)\s*)?reportCompare\s*\(\s*(\w*)\s*,\s*(\w*)\s*(,\s*\S*)?\s*\)\s*;*\s*",
    619        replaceFn,
    620        source,
    621    )
    622 
    623    return re.sub(rb"\breportCompare\b", b"assert.sameValue", newSource)
    624 
    625 
    626 def exportTest262(
    627    outDir: str, providedSrcs: "list[str]", includeShell: bool, baseDir: str
    628 ):
    629    # Create the output directory from scratch.
    630    print(f"Generating output in {os.path.abspath(outDir)}")
    631    if os.path.isdir(outDir):
    632        shutil.rmtree(outDir)
    633 
    634    # only make the includes directory if requested
    635    includeDir = os.path.join(outDir, "harness-includes")
    636    if includeShell:
    637        os.makedirs(includeDir)
    638 
    639    skipped = 0
    640    skippedDirs = 0
    641 
    642    # Go through each source path
    643    for providedSrc in providedSrcs:
    644        src = os.path.abspath(providedSrc)
    645        if not os.path.isdir(src):
    646            print(f"Did not find directory {src}")
    647        # the basename of the path will be used in case multiple "src" arguments
    648        # are passed in to create an output directory for each "src".
    649        basename = os.path.basename(src)
    650 
    651        # Process all test directories recursively.
    652        for dirPath, dirNames, fileNames in os.walk(src):
    653            # we need to make and get the unique set of includes for this filepath
    654            includes = []
    655            if includeShell:
    656                includes = findAndCopyIncludes(dirPath, baseDir, includeDir)
    657 
    658            relPath = os.path.relpath(dirPath, src)
    659            fullRelPath = os.path.join(basename, relPath)
    660 
    661            if relPath in UNSUPPORTED_PATHS:
    662                print("SKIPPED unsupported path %s" % relPath)
    663                # Prevent recursing into subdirectories
    664                del dirNames[:]
    665                skippedDirs += 1
    666                continue
    667 
    668            # Make new test subdirectory to seperate from includes
    669            currentOutDir = os.path.join(outDir, "tests", fullRelPath)
    670 
    671            # This also creates the own outDir folder
    672            if not os.path.exists(currentOutDir):
    673                os.makedirs(currentOutDir)
    674 
    675            for fileName in fileNames:
    676                # Skip browser.js files
    677                if fileName in {"browser.js", "shell.js"}:
    678                    continue
    679 
    680                if fileName.endswith("~"):
    681                    continue
    682 
    683                filePath = os.path.join(dirPath, fileName)
    684                testName = os.path.join(
    685                    fullRelPath, fileName
    686                )  # captures folder(s)+filename
    687 
    688                # This is hacky, but we're unlikely to add anything new
    689                # that needs to be skipped this way.
    690                if relPath + "/" + fileName in UNSUPPORTED_PATHS:
    691                    print("SKIPPED %s" % testName)
    692                    skipped += 1
    693                    continue
    694 
    695                # Copy non-test files as is.
    696                if "_FIXTURE" in fileName or os.path.splitext(fileName)[1] != ".js":
    697                    shutil.copyfile(filePath, os.path.join(currentOutDir, fileName))
    698                    print("C %s" % testName)
    699                    continue
    700 
    701                # Read the original test source and preprocess it for Test262
    702                with open(filePath, "rb") as testFile:
    703                    testSource = testFile.read()
    704 
    705                if not testSource:
    706                    print("SKIPPED %s" % testName)
    707                    skipped += 1
    708                    continue
    709 
    710                skip = skipTest(testSource)
    711                if skip is not None:
    712                    print(
    713                        f"SKIPPED {testName} because file contains {skip.decode('ascii')}"
    714                    )
    715                    skipped += 1
    716                    continue
    717 
    718                try:
    719                    newSource = convertTestFile(testSource, includes)
    720                    if newSource is None:
    721                        print(f"SKIPPED {testName} due to disabled features")
    722                        skipped += 1
    723                        continue
    724                except Exception as e:
    725                    print(f"SKIPPED {testName} due to error {e}")
    726                    traceback.print_exc(file=sys.stdout)
    727                    skipped += 1
    728                    continue
    729 
    730                with open(os.path.join(currentOutDir, fileName), "wb") as output:
    731                    output.write(newSource)
    732 
    733                print("SAVED %s" % testName)
    734 
    735    print(f"Skipped {skipped} tests and {skippedDirs} test directories")
    736 
    737 
    738 if __name__ == "__main__":
    739    import argparse
    740 
    741    # This script must be run from js/src/tests to work correctly.
    742    if "/".join(os.path.normpath(os.getcwd()).split(os.sep)[-3:]) != "js/src/tests":
    743        raise RuntimeError("%s must be run from js/src/tests" % sys.argv[0])
    744 
    745    parser = argparse.ArgumentParser(
    746        description="Export tests to match Test262 file compliance."
    747    )
    748    parser.add_argument(
    749        "--out",
    750        default="test262/export",
    751        help="Output directory. Any existing directory will be removed! "
    752        "(default: %(default)s)",
    753    )
    754    parser.add_argument(
    755        "--exportshellincludes",
    756        action="store_true",
    757        help="Optionally export shell.js files as includes in exported tests. "
    758        "Only use for testing, do not use for exporting to test262 (test262 tests "
    759        "should have as few dependencies as possible).",
    760    )
    761    parser.add_argument(
    762        "src", nargs="+", help="Source folder with test files to export"
    763    )
    764    args = parser.parse_args()
    765    exportTest262(
    766        os.path.abspath(args.out), args.src, args.exportshellincludes, os.getcwd()
    767    )