Bug 1374290 - Test262 export script. r=shu
authorLeo Balter <leonardo.balter@gmail.com>
Fri, 13 Oct 2017 14:40:01 -0700
changeset 436987 0a6f7d9bc6f0bcb55642c8e6422ace3c91424ee9
parent 436986 ec879327cd7c9ae4be6b8a1d609fa915366d740d
child 436988 5306302faf01b61e631dcc49ac449e7df9012132
push id8114
push userjlorenzo@mozilla.com
push dateThu, 02 Nov 2017 16:33:21 +0000
treeherdermozilla-beta@73e0d89a540f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersshu
bugs1374290
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1374290 - Test262 export script. r=shu
js/src/tests/Makefile.in
js/src/tests/requirements.txt
js/src/tests/test/expected/export/multi-header.js
js/src/tests/test/expected/export/reftest-and-frontmatter-error.js
js/src/tests/test/expected/export/reftest-error-syntaxerror.js
js/src/tests/test/expected/export/regular.js
js/src/tests/test/expected/export/reportCompare.js
js/src/tests/test/fixtures/export/.ignore.js
js/src/tests/test/fixtures/export/browser.js
js/src/tests/test/fixtures/export/empty.js
js/src/tests/test/fixtures/export/ignore.js~
js/src/tests/test/fixtures/export/multi-header.js
js/src/tests/test/fixtures/export/reftest-and-frontmatter-error.js
js/src/tests/test/fixtures/export/reftest-error-syntaxerror.js
js/src/tests/test/fixtures/export/regular.js
js/src/tests/test/fixtures/export/reportCompare.js
js/src/tests/test/fixtures/export/shell.js
js/src/tests/test/run.py
js/src/tests/test262-export.py
--- a/js/src/tests/Makefile.in
+++ b/js/src/tests/Makefile.in
@@ -28,16 +28,17 @@ TEST_FILES = \
   js1_4/ \
   js1_5/ \
   js1_6/ \
   js1_7/ \
   js1_8/ \
   js1_8_1/ \
   js1_8_5/ \
   shell/ \
+  test/ \
   test262/ \
   $(NULL)
 
 PKG_STAGE = $(DIST)/test-stage
 
 # stage tests for packaging
 stage-package:
 	$(NSINSTALL) -D $(PKG_STAGE)/jsreftest/tests
new file mode 100644
--- /dev/null
+++ b/js/src/tests/requirements.txt
@@ -0,0 +1,1 @@
+PyYAML==3.12
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/expected/export/multi-header.js
@@ -0,0 +1,25 @@
+// Copyright (C) 2017 Mozilla Corporation. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+info: |
+  foo bar  baz
+description: |
+  Outside AsyncFunction, |await| is a perfectly cromulent LexicalDeclaration variable
+  name.  Therefore ASI doesn't apply, and so the |0| where a |=| was expected is a
+  syntax error.
+author: Jeff Walden <jwalden+code@mit.edu>
+negative:
+  phase: early
+  type: SyntaxError
+flags:
+- module
+esid: sec-let-and-const-declarations
+features:
+- foobar
+---*/
+
+function f() {
+    let
+    await 0;
+}
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/expected/export/reftest-and-frontmatter-error.js
@@ -0,0 +1,22 @@
+// Copyright (C) 2017 Mozilla Corporation. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+
+/*---
+esid: sec-let-and-const-declarations
+features: []
+negative:
+  phase: runtime
+  type: SyntaxError
+description: |
+  Outside AsyncFunction, |await| is a perfectly cromulent LexicalDeclaration variable
+  name.  Therefore ASI doesn't apply, and so the |0| where a |=| was expected is a
+  syntax error.
+---*/
+
+eval(`
+    function f() {
+        let
+        await 0;
+    }
+`);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/expected/export/reftest-error-syntaxerror.js
@@ -0,0 +1,21 @@
+// Copyright (C) 2017 Mozilla Corporation. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+
+/*---
+negative:
+  phase: early
+  type: SyntaxError
+author: Jeff Walden <jwalden+code@mit.edu>
+features: []
+description: |
+  Outside AsyncFunction, |await| is a perfectly cromulent LexicalDeclaration variable
+  name.  Therefore ASI doesn't apply, and so the |0| where a |=| was expected is a
+  syntax error.
+esid: sec-let-and-const-declarations
+---*/
+
+function f() {
+    let
+    await 0;
+}
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/expected/export/regular.js
@@ -0,0 +1,18 @@
+// Copyright (C) 2017 Mozilla Corporation. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+author: Jeff Walden <jwalden+code@mit.edu>
+description: |
+  '|await| is excluded from LexicalDeclaration by grammar parameter, in AsyncFunction.  Therefore
+  |let| followed by |await| inside AsyncFunction is an ASI opportunity, and this code
+  must parse without error.'
+esid: sec-let-and-const-declarations
+---*/
+
+async function f() {
+    let
+    await 0;
+}
+
+assert.sameValue(true, f instanceof Function);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/expected/export/reportCompare.js
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 Mozilla Corporation. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+description: |
+  assert.sameValue
+esid: pending
+---*/
+
+
+var a = 42;
+
+// comment
+assert.sameValue(trueish, true, "ok");
+
+assert.sameValue ( true, /*lol*/true, "ok");
+
+assert.sameValue(true, f instanceof Function);
+assert.sameValue(true, true, "don't crash");
+assert.sameValue(42, foo);
+
+    // this was a assert.sameValue Line
+
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/.ignore.js
@@ -0,0 +1,1 @@
+.
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/browser.js
@@ -0,0 +1,1 @@
+// not an empty file
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/ignore.js~
@@ -0,0 +1,1 @@
+.
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/multi-header.js
@@ -0,0 +1,20 @@
+// |reftest| skip-if(!this.hasOwnProperty('foobar')||outro()) error:SyntaxError module -- foo bar  baz
+// Copyright (C) 2017 Mozilla Corporation. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+author: Jeff Walden <jwalden+code@mit.edu>
+esid: sec-let-and-const-declarations
+description: >
+  Outside AsyncFunction, |await| is a perfectly cromulent LexicalDeclaration
+  variable name.  Therefore ASI doesn't apply, and so the |0| where a |=| was
+  expected is a syntax error.
+negative:
+  phase: early
+  type: SyntaxError
+---*/
+
+function f() {
+    let
+    await 0;
+}
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/reftest-and-frontmatter-error.js
@@ -0,0 +1,19 @@
+// |reftest| error:SyntaxError
+
+/*---
+esid: sec-let-and-const-declarations
+description: >
+  Outside AsyncFunction, |await| is a perfectly cromulent LexicalDeclaration
+  variable name.  Therefore ASI doesn't apply, and so the |0| where a |=| was
+  expected is a syntax error.
+negative:
+  phase: runtime
+  type: SyntaxError
+---*/
+
+eval(`
+    function f() {
+        let
+        await 0;
+    }
+`);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/reftest-error-syntaxerror.js
@@ -0,0 +1,15 @@
+// |reftest| error:SyntaxError
+
+/*---
+author: Jeff Walden <jwalden+code@mit.edu>
+esid: sec-let-and-const-declarations
+description: >
+  Outside AsyncFunction, |await| is a perfectly cromulent LexicalDeclaration
+  variable name.  Therefore ASI doesn't apply, and so the |0| where a |=| was
+  expected is a syntax error.
+---*/
+
+function f() {
+    let
+    await 0;
+}
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/regular.js
@@ -0,0 +1,18 @@
+// Copyright (C) 2017 Mozilla Corporation. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+author: Jeff Walden <jwalden+code@mit.edu>
+esid: sec-let-and-const-declarations
+description: >
+  |await| is excluded from LexicalDeclaration by grammar parameter, in
+  AsyncFunction.  Therefore |let| followed by |await| inside AsyncFunction is
+  an ASI opportunity, and this code must parse without error.
+---*/
+
+async function f() {
+    let
+    await 0;
+}
+
+reportCompare(true, f instanceof Function);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/reportCompare.js
@@ -0,0 +1,38 @@
+// Copyright (C) 2017 Mozilla Corporation. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+description: reportCompare
+---*/
+
+
+var a = 42;
+
+reportCompare(0, 0)
+reportCompare(0, 0);
+reportCompare(0, 0, "ok")
+reportCompare(true, true, "ok"); // comment
+reportCompare(trueish, true, "ok");
+
+reportCompare ( 0 , 0  ) ;
+reportCompare ( 0    , 0, "ok")
+reportCompare ( true, /*lol*/true, "ok");
+
+reportCompare(null, null, "test");
+reportCompare(true, f instanceof Function);
+reportCompare(true, true)
+reportCompare(true, true);
+reportCompare(true, true, "don't crash");
+reportCompare(true,true);
+this.reportCompare && reportCompare(0, 0, "ok");
+this.reportCompare && reportCompare(true, true);
+this.reportCompare && reportCompare(true,true);
+
+reportCompare(42, foo);
+
+    reportCompare(0, 0); // this was a reportCompare Line
+
+reportCompare(
+    true,
+    true
+);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/test/fixtures/export/shell.js
@@ -0,0 +1,1 @@
+// not an empty file
new file mode 100755
--- /dev/null
+++ b/js/src/tests/test/run.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# Adapted from https://github.com/tc39/test262/blob/master/tools/generation/test/run.py
+
+import shutil, subprocess, sys, os, unittest
+
+testDir = os.path.dirname(os.path.relpath(__file__))
+OUT_DIR = os.path.join(testDir, 'out')
+EXPECTED_DIR = os.path.join(testDir, 'expected')
+ex = os.path.join(testDir, '..', 'test262-export.py')
+
+class TestExport(unittest.TestCase):
+    maxDiff = None
+
+    def fixture(self, name):
+        relpath = os.path.relpath(os.path.join(testDir, 'fixtures', name))
+        sp = subprocess.Popen(
+            [ex, relpath, '--out', OUT_DIR],
+            stdout=subprocess.PIPE)
+        stdout, stderr = sp.communicate()
+        return dict(stdout=stdout, stderr=stderr, returncode=sp.returncode)
+
+    def isTestFile(self, filename):
+        return not (
+            filename.startswith('.') or
+            filename.startswith('#') or
+            filename.endswith('~')
+        )
+
+    def getFiles(self, path):
+        names = []
+        for root, _, fileNames in os.walk(path):
+            for fileName in filter(self.isTestFile, fileNames):
+                names.append(os.path.join(root, fileName))
+        names.sort()
+        return names
+
+    def compareTrees(self, targetName):
+        expectedPath = os.path.join(EXPECTED_DIR, targetName)
+        actualPath = OUT_DIR
+
+        expectedFiles = self.getFiles(expectedPath)
+        actualFiles = self.getFiles(actualPath)
+
+        self.assertListEqual(
+            map(lambda x: os.path.relpath(x, expectedPath), expectedFiles),
+            map(lambda x: os.path.relpath(x, actualPath), actualFiles))
+
+        for expectedFile, actualFile in zip(expectedFiles, actualFiles):
+            with open(expectedFile) as expectedHandle:
+                with open(actualFile) as actualHandle:
+                    self.assertMultiLineEqual(
+                        expectedHandle.read(),
+                        actualHandle.read())
+
+    def tearDown(self):
+        shutil.rmtree(OUT_DIR, ignore_errors=True)
+
+    def test_export(self):
+        result = self.fixture('export')
+        self.assertEqual(result['returncode'], 0)
+        self.compareTrees('export')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100755
--- /dev/null
+++ b/js/src/tests/test262-export.py
@@ -0,0 +1,359 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import print_function
+
+import contextlib
+import os
+import re
+import tempfile
+import shutil
+import sys
+import yaml
+
+from functools import partial
+from itertools import chain, imap
+
+# Skip all common files used to support tests for jstests
+# These files are listed in the README.txt
+SUPPORT_FILES = set(["browser.js", "shell.js", "template.js", "user.js",
+    "js-test-driver-begin.js", "js-test-driver-end.js"])
+
+FRONTMATTER_WRAPPER_PATTERN = re.compile(
+    r'/\*\---\n([\s]*)((?:\s|\S)*)[\n\s*]---\*/', flags=re.DOTALL)
+
+def convertTestFile(source):
+    """
+    Convert a jstest test to a compatible Test262 test file.
+    """
+
+    source = convertReportCompare(source)
+    source = updateMeta(source)
+    source = insertCopyrightLines(source)
+
+    return source
+
+def convertReportCompare(source):
+    """
+    Captures all the reportCompare and convert them accordingly.
+
+    Cases with reportCompare calls where the arguments are the same and one of
+    0, true, or null, will be discarded as they are not necessary for Test262.
+
+    Otherwise, reportCompare will be replaced with assert.sameValue, as the
+    equivalent in Test262
+    """
+
+    def replaceFn(matchobj):
+        actual = matchobj.group(1)
+        expected = matchobj.group(2)
+
+        if actual == expected and actual in ["0", "true", "null"]:
+            return ""
+
+        return matchobj.group()
+
+    newSource = re.sub(
+        r'.*reportCompare\s*\(\s*(\w*)\s*,\s*(\w*)\s*(,\s*\S*)?\s*\)\s*;*\s*',
+        replaceFn,
+        source
+    )
+
+    return re.sub(r'\breportCompare\b', "assert.sameValue", newSource)
+
+def fetchReftestEntries(reftest):
+    """
+    Collects and stores the entries from the reftest header.
+    """
+
+    # TODO: fails, slow, skip, random, random-if
+
+    features = []
+    error = None
+    comments = None
+    module = False
+
+    # should capture conditions to skip
+    matchesSkip = re.search(r'skip-if\((.*)\)', reftest)
+    if matchesSkip:
+        matches = matchesSkip.group(1).split("||")
+        for match in matches:
+            # captures a features list
+            dependsOnProp = re.search(
+                r'!this.hasOwnProperty\([\'\"](.*?)[\'\"]\)', match)
+            if dependsOnProp:
+                features.append(dependsOnProp.group(1))
+            else:
+                print("# Can't parse the following skip-if rule: %s" % match)
+
+    # should capture the expected error
+    matchesError = re.search(r'error:\s*(\w*)', reftest)
+    if matchesError:
+        # The metadata from the reftests won't say if it's a runtime or an
+        # early error. This specification is required for the frontmatter tags.
+        error = matchesError.group(1)
+
+    # just tells if it's a module
+    matchesModule = re.search(r'\bmodule\b', reftest)
+    if matchesModule:
+        module = True
+
+    # captures any comments
+    matchesComments = re.search(r' -- (.*)', reftest)
+    if matchesComments:
+        comments = matchesComments.group(1)
+
+    return {
+        "features": features,
+        "error": error,
+        "module": module,
+        "info": comments
+    }
+
+def parseHeader(source):
+    """
+    Parse the source to return it with the extracted the header
+    """
+    from lib.manifest import TEST_HEADER_PATTERN_INLINE
+
+    # Bail early if we do not start with a single comment.
+    if not source.startswith("//"):
+        return (source, {})
+
+    # Extract the token.
+    part, _, _ = source.partition("\n")
+    matches = TEST_HEADER_PATTERN_INLINE.match(part)
+
+    if matches and matches.group(0):
+        reftest = matches.group(0)
+
+        # Remove the found header from the source;
+        # Fetch and return the reftest entries
+        return (source.replace(reftest + "\n", ""), fetchReftestEntries(reftest))
+
+    return (source, {})
+
+def extractMeta(source):
+    """
+    Capture the frontmatter metadata as yaml if it exists.
+    Returns a new dict if it doesn't.
+    """
+
+    match = FRONTMATTER_WRAPPER_PATTERN.search(source)
+    if not match:
+        return {}
+
+    indent, frontmatter_lines = match.groups()
+
+    unindented = re.sub('^%s' % indent, '', frontmatter_lines)
+
+    return yaml.safe_load(unindented)
+
+def updateMeta(source):
+    """
+    Captures the reftest meta and a pre-existing meta if any and merge them
+    into a single dict.
+    """
+
+    # Extract the reftest data from the source
+    source, reftest = parseHeader(source)
+
+    # Extract the frontmatter data from the source
+    frontmatter = extractMeta(source)
+
+    # Merge the reftest and frontmatter
+    merged = mergeMeta(reftest, frontmatter)
+
+    # Cleanup the metadata
+    properData = cleanupMeta(merged)
+
+    return insertMeta(source, properData)
+
+def cleanupMeta(meta):
+    """
+    Clean up all the frontmatter meta tags. This is not a lint tool, just a
+    simple cleanup to remove trailing spaces and duplicate entries from lists.
+    """
+
+    # Populate required tags
+    for tag in ("description", "esid"):
+        meta.setdefault(tag, "pending")
+
+    # Trim values on each string tag
+    for tag in ("description", "esid", "es5id", "es6id", "info", "author"):
+        if tag in meta:
+            meta[tag] = meta[tag].strip()
+
+    # Remove duplicate entries on each list tag
+    for tag in ("features", "flags", "includes"):
+        if tag in meta:
+            # We need the list back for the yaml dump
+            meta[tag] = list(set(meta[tag]))
+
+    if "negative" in meta:
+        # If the negative tag exists, phase needs to be present and set
+        if meta["negative"].get("phase") not in ("early", "runtime"):
+            print("Warning: the negative.phase is not properly set.\n" + \
+                "Ref https://github.com/tc39/test262/blob/master/INTERPRETING.md#negative")
+        # If the negative tag exists, type is required
+        if "type" not in meta["negative"]:
+            print("Warning: the negative.type is not set.\n" + \
+                "Ref https://github.com/tc39/test262/blob/master/INTERPRETING.md#negative")
+
+    return meta
+
+def mergeMeta(reftest, frontmatter):
+    """
+    Merge the metadata from reftest and an existing frontmatter and populate
+    required frontmatter fields properly.
+    """
+
+    # Merge the meta from reftest to the frontmatter
+
+    if "features" in reftest:
+        frontmatter.setdefault("features", []) \
+            .extend(reftest.get("features", []))
+
+    # Only add the module flag if the value from reftest is truish
+    if reftest.get("module"):
+        frontmatter.setdefault("flags", []).append("module")
+
+    # Add any comments to the info tag
+    info = reftest.get("info")
+    if info:
+        # Open some space in an existing info text
+        if "info" in frontmatter:
+            frontmatter["info"] += "\n\n  \%" % info
+        else:
+            frontmatter["info"] = info
+
+    # Set the negative flags
+    if "error" in reftest:
+        error = reftest["error"]
+        if "negative" not in frontmatter:
+            frontmatter["negative"] = {
+                # This code is assuming error tags are early errors, but they
+                # might be runtime errors as well.
+                # From this point, this code can also print a warning asking to
+                # specify the error phase in the generated code or fill the
+                # phase with an empty string.
+                "phase": "early",
+                "type": error
+            }
+        # Print a warning if the errors don't match
+        elif frontmatter["negative"].get("type") != error:
+            print("Warning: The reftest error doesn't match the existing " + \
+                "frontmatter error. %s != %s" % (error,
+                frontmatter["negative"]["type"]))
+
+    return frontmatter
+
+def insertCopyrightLines(source):
+    """
+    Insert the copyright lines into the file.
+    """
+    from datetime import date
+
+    lines = []
+
+    if not re.match(r'\/\/\s+Copyright.*\. All rights reserved.', source):
+        year = date.today().year
+        lines.append("// Copyright (C) %s Mozilla Corporation. All rights reserved." % year)
+        lines.append("// This code is governed by the BSD license found in the LICENSE file.")
+        lines.append("\n")
+
+    return "\n".join(lines) + source
+
+def insertMeta(source, frontmatter):
+    """
+    Insert the formatted frontmatter into the file, use the current existing
+    space if any
+    """
+    lines = []
+
+    lines.append("/*---")
+
+    for (key, value) in frontmatter.items():
+        if key in ("description", "info"):
+            lines.append("%s: |" % key)
+            lines.append("  " + yaml.dump(value, encoding="utf8",
+                ).strip().replace('\n...', ''))
+        else:
+            lines.append(yaml.dump({key: value}, encoding="utf8",
+                default_flow_style=False).strip())
+
+    lines.append("---*/")
+
+    match = FRONTMATTER_WRAPPER_PATTERN.search(source)
+
+    if match:
+        return source.replace(match.group(0), "\n".join(lines))
+    else:
+        return "\n".join(lines) + source
+
+def exportTest262(args):
+    src = os.path.abspath(args.src[0])
+    outDir = os.path.abspath(args.out)
+
+    # Create the output directory from scratch.
+    if os.path.isdir(outDir):
+        shutil.rmtree(outDir)
+
+    # Process all test directories recursively.
+    for (dirPath, _, fileNames) in os.walk(src):
+        relPath = os.path.relpath(dirPath, src)
+
+        relOutDir = os.path.join(outDir, relPath)
+
+        # This also creates the own outDir folder
+        if not os.path.exists(relOutDir):
+            os.makedirs(relOutDir)
+
+        for fileName in fileNames:
+            # Skip browser.js and shell.js files
+            if fileName == "browser.js" or fileName == "shell.js":
+                continue
+
+            filePath = os.path.join(dirPath, fileName)
+            testName = os.path.relpath(filePath, src) # captures folder/fileName
+
+            # Copy non-test files as is.
+            (_, fileExt) = os.path.splitext(fileName)
+            if fileExt != ".js":
+                shutil.copyfile(filePath, os.path.join(outDir, testName))
+                print("C %s" % testName)
+                continue
+
+            # Read the original test source and preprocess it for Test262
+            with open(filePath, "rb") as testFile:
+                testSource = testFile.read()
+
+            if not testSource:
+                print("SKIPPED %s" % testName)
+                continue
+
+            newSource = convertTestFile(testSource)
+
+            with open(os.path.join(outDir, testName), "wb") as output:
+                output.write(newSource)
+
+            print("SAVED %s" % testName)
+
+if __name__ == "__main__":
+    import argparse
+
+    # This script must be run from js/src/tests to work correctly.
+    if "/".join(os.path.normpath(os.getcwd()).split(os.sep)[-3:]) != "js/src/tests":
+        raise RuntimeError("%s must be run from js/src/tests" % sys.argv[0])
+
+    parser = argparse.ArgumentParser(description="Export tests to match Test262 file compliance.")
+    parser.add_argument("--out", default="test262/export",
+                        help="Output directory. Any existing directory will be removed! (default: %(default)s)")
+    parser.add_argument("src", nargs="+", help="Source folder with test files to export")
+    parser.set_defaults(func=exportTest262)
+    args = parser.parse_args()
+    args.func(args)