diff --git a/js/tools/test262.py b/js/tools/test262.py new file mode 100755 index 00000000000000..aa9764bc3b5d95 --- /dev/null +++ b/js/tools/test262.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python + +import os +import re +import string +import subprocess +import sys +import tempfile +import threading +import yaml +import json + +from shutil import copyfile +from stat import * + +here = os.path.dirname(__file__) + +''' +This tool reads out test262 suite and generates wrappers for web-platform-tests. +Each WPT wrapper runs the target test262 test within 4 agents: IFrame, Window, +DedicatedWorker and SharedWorker. Each tests is run either in strict and +non-strict mode, unless the target test indicates otherwise. +''' + +TAB_WIDTH = 2 +DEST_PATH = os.path.join(here, '..', 'test262') + +def trim(text): + lines = text.split("\n") + return "\n".join(lines[1:len(lines)-1]) + +usage_text = trim(''' +Usage: test262-to-wpt [test262-dir] +''') + +def usage(): + print(usage_text) + exit(1) + +''' +Parses a Test262 test. + +A test262 tests are usually composed by two parts: a comment header, that +describes several properties of the test, and a body, which defines the proper +test itself. + +The test header is defined in YAML and contains properties such as esid (test ID), +description, features, flags, etc. Some of these properties are relevant to know +what harnessing libraries to import from the test262 suite (features), whether +the test should only be tested in strict mode or non-strict mode (flags), etc + +Some of the beforementioned properties would need to get serialized to the +resulting web-platform-test as a JSON object. + +The parser also fetches the body of the test. +''' +# Parses a Test262 test. +class Test262Parser(object): + + def __init__(self, text): + match = re.search('---\*/', text) + if match: + self.header = text[:match.end(0)] + self.body = text[match.end(0)+1:] + else: + self.header = "" + self.body = text + self.attrs = self.parse_header() + + def parse_header(self): + if len(self.header) == 0: + return {} + return yaml.safe_load(self.yaml_section()) or {} + + def yaml_section(self): + is_yaml = False + ret = [] + for line in self.header.split('\n'): + if re.search(r'/*---', line): + is_yaml = True + elif is_yaml and re.search(r'---*/', line): + break + elif is_yaml: + ret.append(line) + return "\n".join(ret) + +### + +# TODO: Put everything into one single template? +HEADER = trim(''' + + + + + ###TITLE### + + + + + + + + + + + +''') + +ASYNC_TEST = trim(''' +async_test(function(t) { + ###TEST_CALL###(test262, attrs, t); +}, '###TITLE###'); +''') + +def run_in_iframe(title): + output = ASYNC_TEST + output = re.sub('###TITLE###', 'IFrame: ' + title, output) + output = re.sub('###TEST_CALL###', 'run_in_iframe', output) + return output + +def run_in_iframe_strict(title): + output = ASYNC_TEST + output = re.sub('###TITLE###', 'IFrame (strict): ' + title, output) + output = re.sub('###TEST_CALL###', 'run_in_iframe_strict', output) + return output + +def run_in_window(title): + output = ASYNC_TEST + output = re.sub('###TITLE###', 'Window: ' + title, output) + output = re.sub('###TEST_CALL###', 'run_in_window', output) + return output + +def run_in_window_strict(title): + output = ASYNC_TEST + output = re.sub('###TITLE###', 'Window (strict): ' + title, output) + output = re.sub('###TEST_CALL###', 'run_in_window_strict', output) + return output + +def run_in_worker(title): + output = ASYNC_TEST + output = re.sub('###TITLE###', 'Worker: ' + title, output) + output = re.sub('###TEST_CALL###', 'run_in_worker', output) + return output + +def run_in_worker_strict(title): + output = ASYNC_TEST + output = re.sub('###TITLE###', 'Worker (strict): ' + title, output) + output = re.sub('###TEST_CALL###', 'run_in_worker_strict', output) + return output + +def run_in_shared_worker(title): + output = ASYNC_TEST + output = re.sub('###TITLE###', 'SharedWorker: ' + title, output) + output = re.sub('###TEST_CALL###', 'run_in_shared_worker', output) + return output + +def run_in_shared_worker_strict(title): + output = ASYNC_TEST + output = re.sub('###TITLE###', 'SharedWorker (strict): ' + title, output) + output = re.sub('###TEST_CALL###', 'run_in_shared_worker_strict', output) + return output + +# Indents content n tabs. A tab is 2 spaces (TAB_WIDTH). +def indent(num_tabs, content): + # Duplicates char n times. + def dup(char, times): + ret = [] + for i in range(0, times): + ret.append(char) + return "".join(ret) + ret = [] + space = dup(" ", num_tabs * TAB_WIDTH) + for line in content.split("\n"): + ret.append(space + line) + return "\n".join(ret) + +''' +Generates web-platform-test wrapper for test262 tests. + +If a directory is passed as origin for the tests, the class recursively reads +out al the JavaScript files contained and converts them to web-platform-tests. + +If a file is passed as origin, only that tests is converted. + +The resulting tests are placed inside an output directory named 'js'. +''' +class WPTestBuilder(object): + + def __init__(self, path): + self.path = path + + def generate(self): + path = self.path + mode = os.stat(self.path)[ST_MODE] + if S_ISDIR(mode): + # Check root folder contains 'test'. + test_dir = path + if not re.search("test262/test", path): + test_dir = os.path.join(path, "test") + mode = os.stat(test_dir)[ST_MODE] + if not S_ISDIR(mode): + print(("Could not locate 'test' folder at %s" % path)) + exit(1) + print("Generating web-platform-test wrappers for test262") + # Blacklist 'harness' from root folders. + folders = [test_dir] + for each in os.listdir(test_dir): + if each == 'harness': + continue + folders.append(os.path.join(test_dir, each)) + # Generate wrappers. + for filename in self.listall(folders, {'ext': 'js', 'skip_hidden': True}): + # Files that have the suffix _FIXTURE are not tests, but should be preserved. + if re.search('_FIXTURE.js$', filename): + self.keep_file(filename) + continue + # If they are not fixtures, they're tests. However, it's necessary to keep the + # original file because some tests reference themselves. + if re.search('module-code', filename): + self.keep_file(filename) + self.generate_single_wpt(filename) + print("Output: " + self.destination_path(path)) + elif S_ISREG(mode): + pos = path.find("test262/test") + if pos < 0: + print(("Not a test262 file: '%s'" % path)) + exit(1) + print("Generating web-platform-tests wrapper for {0}".format(path)) + print("Output: %s" % self.generate_single_wpt(path)) + else: + print("Skipping %s" % path) + + def keep_file(self, filename): + dirname = self.destination_path(filename) + dst = os.path.join(dirname, os.path.basename(filename)) + self.mkdir(dirname) + content = self.replace_module_path_if_any(dirname, self.readfile(filename)) + return self.savefile(dst, content) + + def destination_path(self, filename): + filename = re.sub(".*test262/test/", "", filename) + return os.path.join(DEST_PATH, os.path.dirname(filename)) + + def mkdir(self, path): + process = subprocess.Popen(("mkdir -p {0}".format(path)).split()) + process.wait() + + def replace_module_path_if_any(self, dirname, content): + # Replace "[import|export] from './filename'". + content = re.sub(r"from\s+(['\"])\.\/(.*?\.js)(['\"])", + r"from \1http://localhost:8000/" + dirname + r"/\2\3", content) + # Replace "import './filename'". + content = re.sub(r"import (['\"])\.\/(.*?\.js)(['\"])", + r"import \1http://localhost:8000/" + dirname + r"/\2\3", content) + # Replace "export './filename'". + content = re.sub(r"export (['\"])\.\/(.*?\.js)(['\"])", + r"export \1http://localhost:8000/" + dirname + r"/\2\3", content) + return content + + def savefile(self, output, content): + with open(output, 'wt') as fd: + fd.write(content) + return fd.name + + def generate_single_wpt(self, filename): + dst = self.destination_file(filename) + dirname = os.path.dirname(dst) + self.mkdir(dirname) + content = self.replace_module_path_if_any(dirname, self.readfile(filename)) + return self.savefile(dst, self.build(dst, content)) + + def destination_file(self, filename): + filename = re.sub(".*test262/test/", "", filename) + filename = re.sub('.js$', '.html', filename) + return os.path.join(DEST_PATH, filename) + + def build(self, title, content): + test262 = Test262Parser(content) + def header(): + attrs = tojson(test262.attrs) + ret = HEADER + ret = re.sub('###TITLE###', title, ret) + ret = ret.replace('###ATTRS###', indent(2, "let attrs = %s;" % tojson(test262.attrs))) + ret = ret.replace('###HEADER###', indent(2, test262.header)) + return ret + def tojson(attrs): + exclude = ['description', 'info', 'esid', 'es6id'] + for key in exclude: + attrs.pop(key, None) + return json.dumps(attrs) or '{}' + def body(): + def escape(text): + text = re.sub(r'\\', r'\\\\', text) + text = re.sub(r'"', r'\"', text) + return text + def quote(line): + return "\"" + line + "\\n\"" + def format(text): + output = [] + lines = text.split("\n"); + for line in text.split("\n"): + if len(line) > 0: + output.append(quote(line)) + return " + \n".join(output) + return indent(4, "return \"\" +\n%s;" % format(escape(test262.body))) + def footer(): + # By default tests are run in strict and non-strict modes, + # unless flags says otherwise. + ret = [] + flags = 'flags' in test262.attrs and test262.attrs['flags'] or [] + if 'onlyStrict' in flags: + ret.append(run_in_iframe_strict(title)) + ret.append(run_in_window_strict(title)) + ret.append(run_in_worker_strict(title)) + ret.append(run_in_shared_worker_strict(title)) + elif 'noStrict' in flags or 'raw' in flags: + ret.append(run_in_iframe(title)) + ret.append(run_in_window(title)) + ret.append(run_in_worker(title)) + ret.append(run_in_shared_worker(title)) + else: + ret.append(run_in_iframe_strict(title)) + ret.append(run_in_iframe(title)) + ret.append(run_in_window_strict(title)) + ret.append(run_in_window(title)) + ret.append(run_in_worker_strict(title)) + ret.append(run_in_worker(title)) + ret.append(run_in_shared_worker_strict(title)) + ret.append(run_in_shared_worker(title)) + return FOOTER.replace("###TESTS###", indent(3, "\n".join(ret))) + ret = [] + ret.append(header()) + ret.append(body()) + ret.append(footer()) + return "\n".join(ret) + + def readfile(self, path): + with open(path) as fd: + content = fd.read() + # Replace CR + LF for LF only. + content = re.sub("\r\n", "\n", content) + # Replace CR for LF. + return re.sub("\r", "\n", content) + + def listall(self, folders, opts): + opts = opts or {} + ret = [] + ext = opts['ext'] + skip_hidden = opts['skip_hidden'] + for folder in folders: + for root, dirs, files in os.walk(folder): + for filename in files: + if skip_hidden and filename.startswith('.'): + continue + if ext and filename.endswith(ext): + ret.append(os.path.join(root, filename)) + return ret + +def main(): + path = os.path.join(here, '..', '..', '..', '..', 'src', 'test262') + WPTestBuilder(path).generate() + +if __name__ == "__main__": + main() diff --git a/resources/test262-agent-harness.js b/resources/test262-agent-harness.js new file mode 100644 index 00000000000000..1af0a7f69fd028 --- /dev/null +++ b/resources/test262-agent-harness.js @@ -0,0 +1,358 @@ +// Helper file to run a test262 within an agent. +// +// The target test262 is in text format. The harnessing code composes an HTML +// page containing the test262, runs it and reports for errors if there was +// any. Similarly if the target agent is a Worker, the harnessing code embedes +// the test code within the target worker, runs the test and checks out if +// there were any errors. +// +// Currently supported agents are IFrame, Window, Worker and SharedWorker. + +// IFrame. +function run_in_iframe_strict(test262, attrs, t) { + let opts = {} + opts.strict = true; + run_in_iframe(test262, attrs, t, opts); +} + +// If 'negative' attribute was defined, the test must pass if 'message' +// matches 'negative.type'. +function is_negative(attrs, message) { + let negative = attrs.negative || {}; + return message && message.indexOf(negative.type) >= 0; +} + +function run_in_iframe(test262, attrs, t, opts) { + opts = opts || {}; + // Rethrow error from iframe. + window.addEventListener('message', t.step_func(function(e) { + if (e.data[0] == 'error') { + throw new Error(e.data[1]); + } + })); + let iframe = document.createElement('iframe'); + iframe.style = 'display: none'; + content = test262_as_html(test262, attrs, opts.strict); + let blob = new Blob([content], {type: 'text/html'}); + iframe.src = URL.createObjectURL(blob); + document.body.appendChild(iframe); + + let w = iframe.contentWindow; + // Finish test on completed event. + w.addEventListener('completed', t.step_func(function(e) { + t.done(); + })); + // If test failed, rethrow error. + let FAILED = 'iframe-failed' + opts.strict ? 'strict' : ''; + window.addEventListener(FAILED, t.step_func(function(e) { + t.set_status(t.FAIL); + throw new Error(e.detail); + })); + // In case of error send it to parent window. + w.addEventListener('error', function(e) { + e.preventDefault(); + if (is_negative(attrs, e.message)) { + t.done(); + return; + } + top.dispatchEvent(new CustomEvent(FAILED, {detail: e.message})); + }); +} + +let HEADER = ` + + + + + + + +