forked from yystopf/xiuos
951 lines
31 KiB
Python
951 lines
31 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright JS Foundation and other contributors, http://js.foundation
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
|
|
# This file is based on work under the following copyright and permission notice:
|
|
# https://github.com/test262-utils/test262-harness-py
|
|
# test262.py, _monkeyYaml.py, parseTestRecord.py
|
|
|
|
# license of test262.py:
|
|
# Copyright 2009 the Sputnik authors. All rights reserved.
|
|
# This code is governed by the BSD license found in the LICENSE file.
|
|
# This is derived from sputnik.py, the Sputnik console test runner,
|
|
# with elements from packager.py, which is separately
|
|
# copyrighted. TODO: Refactor so there is less duplication between
|
|
# test262.py and packager.py.
|
|
|
|
# license of _packager.py:
|
|
# Copyright (c) 2012 Ecma International. All rights reserved.
|
|
# This code is governed by the BSD license found in the LICENSE file.
|
|
|
|
# license of _monkeyYaml.py:
|
|
# Copyright 2014 by Sam Mikes. All rights reserved.
|
|
# This code is governed by the BSD license found in the LICENSE file.
|
|
|
|
# license of parseTestRecord.py:
|
|
# Copyright 2011 by Google, Inc. All rights reserved.
|
|
# This code is governed by the BSD license found in the LICENSE file.
|
|
|
|
|
|
from __future__ import print_function
|
|
|
|
import logging
|
|
import optparse
|
|
import os
|
|
from os import path
|
|
import platform
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import xml.dom.minidom
|
|
from collections import Counter
|
|
|
|
import signal
|
|
import threading
|
|
import multiprocessing
|
|
|
|
#######################################################################
|
|
# based on _monkeyYaml.py
|
|
#######################################################################
|
|
|
|
M_YAML_LIST_PATTERN = re.compile(r"^\[(.*)\]$")
|
|
M_YAML_MULTILINE_LIST = re.compile(r"^ *- (.*)$")
|
|
|
|
|
|
# The timeout of each test case
|
|
TEST262_CASE_TIMEOUT = 5
|
|
|
|
|
|
def yaml_load(string):
|
|
return my_read_dict(string.splitlines())[1]
|
|
|
|
|
|
def my_read_dict(lines, indent=""):
|
|
dictionary = {}
|
|
key = None
|
|
empty_lines = 0
|
|
|
|
while lines:
|
|
if not lines[0].startswith(indent):
|
|
break
|
|
|
|
line = lines.pop(0)
|
|
if my_is_all_spaces(line):
|
|
empty_lines += 1
|
|
continue
|
|
|
|
result = re.match(r"(.*?):(.*)", line)
|
|
|
|
if result:
|
|
if not dictionary:
|
|
dictionary = {}
|
|
key = result.group(1).strip()
|
|
value = result.group(2).strip()
|
|
(lines, value) = my_read_value(lines, value, indent)
|
|
dictionary[key] = value
|
|
else:
|
|
if dictionary and key and key in dictionary:
|
|
char = " " if empty_lines == 0 else "\n" * empty_lines
|
|
dictionary[key] += char + line.strip()
|
|
else:
|
|
raise Exception("monkeyYaml is confused at " + line)
|
|
empty_lines = 0
|
|
|
|
if not dictionary:
|
|
dictionary = None
|
|
|
|
return lines, dictionary
|
|
|
|
|
|
def my_read_value(lines, value, indent):
|
|
if value == ">" or value == "|":
|
|
(lines, value) = my_multiline(lines, value == "|")
|
|
value = value + "\n"
|
|
return (lines, value)
|
|
if lines and not value:
|
|
if my_maybe_list(lines[0]):
|
|
return my_multiline_list(lines, value)
|
|
indent_match = re.match("(" + indent + r"\s+)", lines[0])
|
|
if indent_match:
|
|
if ":" in lines[0]:
|
|
return my_read_dict(lines, indent_match.group(1))
|
|
return my_multiline(lines, False)
|
|
return lines, my_read_one_line(value)
|
|
|
|
|
|
def my_maybe_list(value):
|
|
return M_YAML_MULTILINE_LIST.match(value)
|
|
|
|
|
|
def my_multiline_list(lines, value):
|
|
# assume no explcit indentor (otherwise have to parse value)
|
|
value = []
|
|
indent = 0
|
|
while lines:
|
|
line = lines.pop(0)
|
|
leading = my_leading_spaces(line)
|
|
if my_is_all_spaces(line):
|
|
pass
|
|
elif leading < indent:
|
|
lines.insert(0, line)
|
|
break
|
|
else:
|
|
indent = indent or leading
|
|
value += [my_read_one_line(my_remove_list_header(indent, line))]
|
|
return (lines, value)
|
|
|
|
|
|
def my_remove_list_header(indent, line):
|
|
line = line[indent:]
|
|
return M_YAML_MULTILINE_LIST.match(line).group(1)
|
|
|
|
|
|
def my_read_one_line(value):
|
|
if M_YAML_LIST_PATTERN.match(value):
|
|
return my_flow_list(value)
|
|
elif re.match(r"^[-0-9]*$", value):
|
|
try:
|
|
value = int(value)
|
|
except ValueError:
|
|
pass
|
|
elif re.match(r"^[-.0-9eE]*$", value):
|
|
try:
|
|
value = float(value)
|
|
except ValueError:
|
|
pass
|
|
elif re.match(r"^('|\").*\1$", value):
|
|
value = value[1:-1]
|
|
return value
|
|
|
|
|
|
def my_flow_list(value):
|
|
result = M_YAML_LIST_PATTERN.match(value)
|
|
values = result.group(1).split(",")
|
|
return [my_read_one_line(v.strip()) for v in values]
|
|
|
|
|
|
def my_multiline(lines, preserve_newlines=False):
|
|
# assume no explcit indentor (otherwise have to parse value)
|
|
value = ""
|
|
indent = my_leading_spaces(lines[0])
|
|
was_empty = None
|
|
|
|
while lines:
|
|
line = lines.pop(0)
|
|
is_empty = my_is_all_spaces(line)
|
|
|
|
if is_empty:
|
|
if preserve_newlines:
|
|
value += "\n"
|
|
elif my_leading_spaces(line) < indent:
|
|
lines.insert(0, line)
|
|
break
|
|
else:
|
|
if preserve_newlines:
|
|
if was_empty != None:
|
|
value += "\n"
|
|
else:
|
|
if was_empty:
|
|
value += "\n"
|
|
elif was_empty is False:
|
|
value += " "
|
|
value += line[(indent):]
|
|
|
|
was_empty = is_empty
|
|
|
|
return (lines, value)
|
|
|
|
|
|
def my_is_all_spaces(line):
|
|
return len(line.strip()) == 0
|
|
|
|
|
|
def my_leading_spaces(line):
|
|
return len(line) - len(line.lstrip(' '))
|
|
|
|
|
|
#######################################################################
|
|
# based on parseTestRecord.py
|
|
#######################################################################
|
|
|
|
# Matches trailing whitespace and any following blank lines.
|
|
_BLANK_LINES = r"([ \t]*[\r\n]{1,2})*"
|
|
|
|
# Matches the YAML frontmatter block.
|
|
# It must be non-greedy because test262-es2015/built-ins/Object/assign/Override.js contains a comment like yaml pattern
|
|
_YAML_PATTERN = re.compile(r"/\*---(.*?)---\*/" + _BLANK_LINES, re.DOTALL)
|
|
|
|
# Matches all known variants for the license block.
|
|
# https://github.com/tc39/test262/blob/705d78299cf786c84fa4df473eff98374de7135a/tools/lint/lib/checks/license.py
|
|
_LICENSE_PATTERN = re.compile(
|
|
r'// Copyright( \([C]\))? (\w+) .+\. {1,2}All rights reserved\.[\r\n]{1,2}' +
|
|
r'(' +
|
|
r'// This code is governed by the( BSD)? license found in the LICENSE file\.' +
|
|
r'|' +
|
|
r'// See LICENSE for details.' +
|
|
r'|' +
|
|
r'// Use of this source code is governed by a BSD-style license that can be[\r\n]{1,2}' +
|
|
r'// found in the LICENSE file\.' +
|
|
r'|' +
|
|
r'// See LICENSE or https://github\.com/tc39/test262/blob/master/LICENSE' +
|
|
r')' + _BLANK_LINES, re.IGNORECASE)
|
|
|
|
|
|
def yaml_attr_parser(test_record, attrs, name, onerror=print):
|
|
parsed = yaml_load(attrs)
|
|
if parsed is None:
|
|
onerror("Failed to parse yaml in name %s" % name)
|
|
return
|
|
|
|
for key in parsed:
|
|
value = parsed[key]
|
|
if key == "info":
|
|
key = "commentary"
|
|
test_record[key] = value
|
|
|
|
if 'flags' in test_record:
|
|
for flag in test_record['flags']:
|
|
test_record[flag] = ""
|
|
|
|
|
|
def find_license(src):
|
|
match = _LICENSE_PATTERN.search(src)
|
|
if not match:
|
|
return None
|
|
|
|
return match.group(0)
|
|
|
|
|
|
def find_attrs(src):
|
|
match = _YAML_PATTERN.search(src)
|
|
if not match:
|
|
return (None, None)
|
|
|
|
return (match.group(0), match.group(1).strip())
|
|
|
|
|
|
def parse_test_record(src, name, onerror=print):
|
|
# Find the license block.
|
|
header = find_license(src)
|
|
|
|
# Find the YAML frontmatter.
|
|
(frontmatter, attrs) = find_attrs(src)
|
|
|
|
# YAML frontmatter is required for all tests.
|
|
if frontmatter is None:
|
|
onerror("Missing frontmatter: %s" % name)
|
|
|
|
# The license shuold be placed before the frontmatter and there shouldn't be
|
|
# any extra content between the license and the frontmatter.
|
|
if header is not None and frontmatter is not None:
|
|
header_idx = src.index(header)
|
|
frontmatter_idx = src.index(frontmatter)
|
|
if header_idx > frontmatter_idx:
|
|
onerror("Unexpected license after frontmatter: %s" % name)
|
|
|
|
# Search for any extra test content, but ignore whitespace only or comment lines.
|
|
extra = src[header_idx + len(header): frontmatter_idx]
|
|
if extra and any(line.strip() and not line.lstrip().startswith("//") for line in extra.split("\n")):
|
|
onerror(
|
|
"Unexpected test content between license and frontmatter: %s" % name)
|
|
|
|
# Remove the license and YAML parts from the actual test content.
|
|
test = src
|
|
if frontmatter is not None:
|
|
test = test.replace(frontmatter, '')
|
|
if header is not None:
|
|
test = test.replace(header, '')
|
|
|
|
test_record = {}
|
|
test_record['header'] = header.strip() if header else ''
|
|
test_record['test'] = test
|
|
|
|
if attrs:
|
|
yaml_attr_parser(test_record, attrs, name, onerror)
|
|
|
|
# Report if the license block is missing in non-generated tests.
|
|
if header is None and "generated" not in test_record and "hashbang" not in name:
|
|
onerror("No license found in: %s" % name)
|
|
|
|
return test_record
|
|
|
|
|
|
#######################################################################
|
|
# based on test262.py
|
|
#######################################################################
|
|
|
|
class Test262Error(Exception):
|
|
def __init__(self, message):
|
|
Exception.__init__(self)
|
|
self.message = message
|
|
|
|
|
|
def report_error(error_string):
|
|
raise Test262Error(error_string)
|
|
|
|
|
|
def build_options():
|
|
result = optparse.OptionParser()
|
|
result.add_option("--command", default=None,
|
|
help="The command-line to run")
|
|
result.add_option("--tests", default=path.abspath('.'),
|
|
help="Path to the tests")
|
|
result.add_option("--exclude-list", default=None,
|
|
help="Path to the excludelist.xml file")
|
|
result.add_option("--cat", default=False, action="store_true",
|
|
help="Print packaged test code that would be run")
|
|
result.add_option("--summary", default=False, action="store_true",
|
|
help="Print summary after running tests")
|
|
result.add_option("--full-summary", default=False, action="store_true",
|
|
help="Print summary and test output after running tests")
|
|
result.add_option("--strict_only", default=False, action="store_true",
|
|
help="Test only strict mode")
|
|
result.add_option("--non_strict_only", default=False, action="store_true",
|
|
help="Test only non-strict mode")
|
|
result.add_option("--unmarked_default", default="both",
|
|
help="default mode for tests of unspecified strictness")
|
|
result.add_option("-j", "--job-count", default=None, action="store", type=int,
|
|
help="Number of parallel test jobs to run. In case of '0' cpu count is used.")
|
|
result.add_option("--logname", help="Filename to save stdout to")
|
|
result.add_option("--loglevel", default="warning",
|
|
help="sets log level to debug, info, warning, error, or critical")
|
|
result.add_option("--print-handle", default="print",
|
|
help="Command to print from console")
|
|
result.add_option("--list-includes", default=False, action="store_true",
|
|
help="List includes required by tests")
|
|
result.add_option("--module-flag", default="-m",
|
|
help="List includes required by tests")
|
|
return result
|
|
|
|
|
|
def validate_options(options):
|
|
if not options.command:
|
|
report_error("A --command must be specified.")
|
|
if not path.exists(options.tests):
|
|
report_error("Couldn't find test path '%s'" % options.tests)
|
|
|
|
|
|
def is_windows():
|
|
actual_platform = platform.system()
|
|
return (actual_platform == 'Windows') or (actual_platform == 'Microsoft')
|
|
|
|
|
|
class TempFile(object):
|
|
|
|
def __init__(self, suffix="", prefix="tmp", text=False):
|
|
self.suffix = suffix
|
|
self.prefix = prefix
|
|
self.text = text
|
|
self.file_desc = None
|
|
self.name = None
|
|
self.is_closed = False
|
|
self.open_file()
|
|
|
|
def open_file(self):
|
|
(self.file_desc, self.name) = tempfile.mkstemp(
|
|
suffix=self.suffix,
|
|
prefix=self.prefix,
|
|
text=self.text)
|
|
|
|
def write(self, string):
|
|
os.write(self.file_desc, string)
|
|
|
|
def read(self):
|
|
file_desc = file(self.name)
|
|
result = file_desc.read()
|
|
file_desc.close()
|
|
return result
|
|
|
|
def close(self):
|
|
if not self.is_closed:
|
|
self.is_closed = True
|
|
os.close(self.file_desc)
|
|
|
|
def dispose(self):
|
|
try:
|
|
self.close()
|
|
os.unlink(self.name)
|
|
except OSError as exception:
|
|
logging.error("Error disposing temp file: %s", str(exception))
|
|
|
|
|
|
class TestResult(object):
|
|
|
|
def __init__(self, exit_code, stdout, stderr, case):
|
|
self.exit_code = exit_code
|
|
self.stdout = stdout
|
|
self.stderr = stderr
|
|
self.case = case
|
|
|
|
def report_outcome(self, long_format):
|
|
name = self.case.get_name()
|
|
mode = self.case.get_mode()
|
|
if self.has_unexpected_outcome():
|
|
if self.case.is_negative():
|
|
print("=== %s passed in %s, but was expected to fail ===" % (name, mode))
|
|
print("--- expected error: %s ---\n" % self.case.get_negative_type())
|
|
else:
|
|
if long_format:
|
|
print("=== %s failed in %s ===" % (name, mode))
|
|
else:
|
|
print("%s in %s: " % (name, mode))
|
|
self.write_output(sys.stdout)
|
|
if long_format:
|
|
print("===")
|
|
elif self.case.is_negative():
|
|
print("%s failed in %s as expected" % (name, mode))
|
|
else:
|
|
print("%s passed in %s" % (name, mode))
|
|
|
|
def write_output(self, target):
|
|
out = self.stdout.strip()
|
|
if out:
|
|
target.write("--- output --- \n %s" % out)
|
|
error = self.stderr.strip()
|
|
if error:
|
|
target.write("--- errors --- \n %s" % error)
|
|
|
|
target.write("\n--- exit code: %d ---\n" % self.exit_code)
|
|
|
|
def has_failed(self):
|
|
return self.exit_code != 0
|
|
|
|
def async_has_failed(self):
|
|
return 'Test262:AsyncTestComplete' not in self.stdout
|
|
|
|
def has_unexpected_outcome(self):
|
|
if self.case.is_async_test():
|
|
return self.async_has_failed() or self.has_failed()
|
|
elif self.case.is_negative():
|
|
return not (self.has_failed() and self.case.negative_match(self.get_error_output()))
|
|
|
|
return self.has_failed()
|
|
|
|
def get_error_output(self):
|
|
if self.stderr:
|
|
return self.stderr
|
|
return self.stdout
|
|
|
|
|
|
class TestCase(object):
|
|
|
|
def __init__(self, suite, name, full_path, strict_mode, command_template, module_flag):
|
|
self.suite = suite
|
|
self.name = name
|
|
self.full_path = full_path
|
|
self.strict_mode = strict_mode
|
|
with open(self.full_path) as file_desc:
|
|
self.contents = file_desc.read()
|
|
test_record = parse_test_record(self.contents, name)
|
|
self.test = test_record["test"]
|
|
del test_record["test"]
|
|
del test_record["header"]
|
|
test_record.pop("commentary", None) # do not throw if missing
|
|
self.test_record = test_record
|
|
self.command_template = command_template
|
|
self.module_flag = module_flag
|
|
|
|
self.validate()
|
|
|
|
def negative_match(self, stderr):
|
|
neg = re.compile(self.get_negative_type())
|
|
return re.search(neg, stderr)
|
|
|
|
def get_negative(self):
|
|
if not self.is_negative():
|
|
return None
|
|
return self.test_record["negative"]
|
|
|
|
def get_negative_type(self):
|
|
negative = self.get_negative()
|
|
if isinstance(negative, dict) and "type" in negative:
|
|
return negative["type"]
|
|
return negative
|
|
|
|
def get_negative_phase(self):
|
|
negative = self.get_negative()
|
|
return negative and "phase" in negative and negative["phase"]
|
|
|
|
def get_name(self):
|
|
return path.join(*self.name)
|
|
|
|
def get_mode(self):
|
|
if self.strict_mode:
|
|
return "strict mode"
|
|
return "non-strict mode"
|
|
|
|
def get_path(self):
|
|
return self.name
|
|
|
|
def is_negative(self):
|
|
return 'negative' in self.test_record
|
|
|
|
def is_only_strict(self):
|
|
return 'onlyStrict' in self.test_record
|
|
|
|
def is_no_strict(self):
|
|
return 'noStrict' in self.test_record or self.is_raw()
|
|
|
|
def is_raw(self):
|
|
return 'raw' in self.test_record
|
|
|
|
def is_async_test(self):
|
|
return 'async' in self.test_record or '$DONE' in self.test
|
|
|
|
def is_module(self):
|
|
return 'module' in self.test_record
|
|
|
|
def get_include_list(self):
|
|
if self.test_record.get('includes'):
|
|
return self.test_record['includes']
|
|
return []
|
|
|
|
def get_additional_includes(self):
|
|
return '\n'.join([self.suite.get_include(include) for include in self.get_include_list()])
|
|
|
|
def get_source(self):
|
|
if self.is_raw():
|
|
return self.test
|
|
|
|
source = self.suite.get_include("sta.js") + \
|
|
self.suite.get_include("assert.js")
|
|
|
|
if self.is_async_test():
|
|
source = source + \
|
|
self.suite.get_include("timer.js") + \
|
|
self.suite.get_include("doneprintHandle.js").replace(
|
|
'print', self.suite.print_handle)
|
|
|
|
source = source + \
|
|
self.get_additional_includes() + \
|
|
self.test + '\n'
|
|
|
|
if self.get_negative_phase() == "early":
|
|
source = ("throw 'Expected an early error, but code was executed.';\n" +
|
|
source)
|
|
|
|
if self.strict_mode:
|
|
source = '"use strict";\nvar strict_mode = true;\n' + source
|
|
else:
|
|
# add comment line so line numbers match in both strict and non-strict version
|
|
source = '//"no strict";\nvar strict_mode = false;\n' + source
|
|
|
|
return source
|
|
|
|
@staticmethod
|
|
def instantiate_template(template, params):
|
|
def get_parameter(match):
|
|
key = match.group(1)
|
|
return params.get(key, match.group(0))
|
|
|
|
return re.sub(r"\{\{(\w+)\}\}", get_parameter, template)
|
|
|
|
@staticmethod
|
|
def execute(command):
|
|
if is_windows():
|
|
args = '%s' % command
|
|
else:
|
|
args = command.split(" ")
|
|
stdout = TempFile(prefix="test262-out-")
|
|
stderr = TempFile(prefix="test262-err-")
|
|
try:
|
|
logging.info("exec: %s", str(args))
|
|
process = subprocess.Popen(
|
|
args,
|
|
shell=False,
|
|
stdout=stdout.file_desc,
|
|
stderr=stderr.file_desc
|
|
)
|
|
timer = threading.Timer(TEST262_CASE_TIMEOUT, process.kill)
|
|
timer.start()
|
|
code = process.wait()
|
|
timer.cancel()
|
|
out = stdout.read()
|
|
err = stderr.read()
|
|
finally:
|
|
stdout.dispose()
|
|
stderr.dispose()
|
|
return (code, out, err)
|
|
|
|
def run_test_in(self, tmp):
|
|
tmp.write(self.get_source())
|
|
tmp.close()
|
|
|
|
if self.is_module():
|
|
arg = self.module_flag + ' ' + tmp.name
|
|
else:
|
|
arg = tmp.name
|
|
|
|
command = TestCase.instantiate_template(self.command_template, {
|
|
'path': arg
|
|
})
|
|
|
|
(code, out, err) = TestCase.execute(command)
|
|
return TestResult(code, out, err, self)
|
|
|
|
def run(self):
|
|
tmp = TempFile(suffix=".js", prefix="test262-", text=True)
|
|
try:
|
|
result = self.run_test_in(tmp)
|
|
finally:
|
|
tmp.dispose()
|
|
return result
|
|
|
|
def print_source(self):
|
|
print(self.get_source())
|
|
|
|
def validate(self):
|
|
flags = self.test_record.get("flags")
|
|
phase = self.get_negative_phase()
|
|
|
|
if phase not in [None, False, "parse", "early", "runtime", "resolution"]:
|
|
raise TypeError("Invalid value for negative phase: " + phase)
|
|
|
|
if not flags:
|
|
return
|
|
|
|
if 'raw' in flags:
|
|
if 'noStrict' in flags:
|
|
raise TypeError("The `raw` flag implies the `noStrict` flag")
|
|
elif 'onlyStrict' in flags:
|
|
raise TypeError(
|
|
"The `raw` flag is incompatible with the `onlyStrict` flag")
|
|
elif self.get_include_list():
|
|
raise TypeError(
|
|
"The `raw` flag is incompatible with the `includes` tag")
|
|
|
|
|
|
def pool_init():
|
|
"""Ignore CTRL+C in the worker process."""
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
|
|
|
|
def test_case_run_process(case):
|
|
return case.run()
|
|
|
|
|
|
class ProgressIndicator(object):
|
|
|
|
def __init__(self, count):
|
|
self.count = count
|
|
self.succeeded = 0
|
|
self.failed = 0
|
|
self.failed_tests = []
|
|
|
|
def has_run(self, result):
|
|
result.report_outcome(True)
|
|
if result.has_unexpected_outcome():
|
|
self.failed += 1
|
|
self.failed_tests.append(result)
|
|
else:
|
|
self.succeeded += 1
|
|
|
|
|
|
def make_plural(num):
|
|
if num == 1:
|
|
return (num, "")
|
|
return (num, "s")
|
|
|
|
|
|
def percent_format(partial, total):
|
|
return "%i test%s (%.1f%%)" % (make_plural(partial) +
|
|
((100.0 * partial)/total,))
|
|
|
|
|
|
class TestSuite(object):
|
|
|
|
def __init__(self, options):
|
|
self.test_root = path.join(options.tests, 'test')
|
|
self.lib_root = path.join(options.tests, 'harness')
|
|
self.strict_only = options.strict_only
|
|
self.non_strict_only = options.non_strict_only
|
|
self.unmarked_default = options.unmarked_default
|
|
self.print_handle = options.print_handle
|
|
self.include_cache = {}
|
|
self.exclude_list_path = options.exclude_list
|
|
self.module_flag = options.module_flag
|
|
self.logf = None
|
|
|
|
def _load_excludes(self):
|
|
if self.exclude_list_path and os.path.exists(self.exclude_list_path):
|
|
xml_document = xml.dom.minidom.parse(self.exclude_list_path)
|
|
xml_tests = xml_document.getElementsByTagName("test")
|
|
return {x.getAttribute("id") for x in xml_tests}
|
|
|
|
return set()
|
|
|
|
def validate(self):
|
|
if not path.exists(self.test_root):
|
|
report_error("No test repository found")
|
|
if not path.exists(self.lib_root):
|
|
report_error("No test library found")
|
|
|
|
@staticmethod
|
|
def is_hidden(test_path):
|
|
return test_path.startswith('.') or test_path == 'CVS'
|
|
|
|
@staticmethod
|
|
def is_test_case(test_path):
|
|
return test_path.endswith('.js') and not test_path.endswith('_FIXTURE.js')
|
|
|
|
@staticmethod
|
|
def should_run(rel_path, tests):
|
|
if not tests:
|
|
return True
|
|
for test in tests:
|
|
if test in rel_path:
|
|
return True
|
|
return False
|
|
|
|
def get_include(self, name):
|
|
if not name in self.include_cache:
|
|
static = path.join(self.lib_root, name)
|
|
if path.exists(static):
|
|
with open(static) as file_desc:
|
|
contents = file_desc.read()
|
|
contents = re.sub(r'\r\n', '\n', contents)
|
|
self.include_cache[name] = contents + "\n"
|
|
else:
|
|
report_error("Can't find: " + static)
|
|
return self.include_cache[name]
|
|
|
|
def enumerate_tests(self, tests, command_template):
|
|
exclude_list = self._load_excludes()
|
|
|
|
logging.info("Listing tests in %s", self.test_root)
|
|
cases = []
|
|
for root, dirs, files in os.walk(self.test_root):
|
|
for hidden_dir in [x for x in dirs if self.is_hidden(x)]:
|
|
dirs.remove(hidden_dir)
|
|
dirs.sort()
|
|
for test_path in filter(TestSuite.is_test_case, sorted(files)):
|
|
full_path = path.join(root, test_path)
|
|
if full_path.startswith(self.test_root):
|
|
rel_path = full_path[len(self.test_root)+1:]
|
|
else:
|
|
logging.warning("Unexpected path %s", full_path)
|
|
rel_path = full_path
|
|
if self.should_run(rel_path, tests):
|
|
basename = path.basename(full_path)[:-3]
|
|
name = rel_path.split(path.sep)[:-1] + [basename]
|
|
if rel_path in exclude_list:
|
|
print('Excluded: ' + rel_path)
|
|
else:
|
|
if not self.non_strict_only:
|
|
strict_case = TestCase(self, name, full_path, True, command_template, self.module_flag)
|
|
if not strict_case.is_no_strict():
|
|
if strict_case.is_only_strict() or self.unmarked_default in ['both', 'strict']:
|
|
cases.append(strict_case)
|
|
if not self.strict_only:
|
|
non_strict_case = TestCase(self, name, full_path, False, command_template, self.module_flag)
|
|
if not non_strict_case.is_only_strict():
|
|
if non_strict_case.is_no_strict() or self.unmarked_default in ['both', 'non_strict']:
|
|
cases.append(non_strict_case)
|
|
logging.info("Done listing tests")
|
|
return cases
|
|
|
|
def print_summary(self, progress, logfile):
|
|
|
|
def write(string):
|
|
if logfile:
|
|
self.logf.write(string + "\n")
|
|
print(string)
|
|
|
|
print("")
|
|
write("=== Summary ===")
|
|
count = progress.count
|
|
succeeded = progress.succeeded
|
|
failed = progress.failed
|
|
write(" - Ran %i test%s" % make_plural(count))
|
|
if progress.failed == 0:
|
|
write(" - All tests succeeded")
|
|
else:
|
|
write(" - Passed " + percent_format(succeeded, count))
|
|
write(" - Failed " + percent_format(failed, count))
|
|
positive = [c for c in progress.failed_tests if not c.case.is_negative()]
|
|
negative = [c for c in progress.failed_tests if c.case.is_negative()]
|
|
if positive:
|
|
print("")
|
|
write("Failed Tests")
|
|
for result in positive:
|
|
write(" %s in %s" % (result.case.get_name(), result.case.get_mode()))
|
|
if negative:
|
|
print("")
|
|
write("Expected to fail but passed ---")
|
|
for result in negative:
|
|
write(" %s in %s" % (result.case.get_name(), result.case.get_mode()))
|
|
|
|
def print_failure_output(self, progress, logfile):
|
|
for result in progress.failed_tests:
|
|
if logfile:
|
|
self.write_log(result)
|
|
print("")
|
|
result.report_outcome(False)
|
|
|
|
def run(self, command_template, tests, print_summary, full_summary, logname, job_count=1):
|
|
if not "{{path}}" in command_template:
|
|
command_template += " {{path}}"
|
|
cases = self.enumerate_tests(tests, command_template)
|
|
if not cases:
|
|
report_error("No tests to run")
|
|
progress = ProgressIndicator(len(cases))
|
|
if logname:
|
|
self.logf = open(logname, "w")
|
|
|
|
if job_count == 1:
|
|
for case in cases:
|
|
result = case.run()
|
|
if logname:
|
|
self.write_log(result)
|
|
progress.has_run(result)
|
|
else:
|
|
if job_count == 0:
|
|
job_count = None # uses multiprocessing.cpu_count()
|
|
|
|
pool = multiprocessing.Pool(processes=job_count, initializer=pool_init)
|
|
try:
|
|
for result in pool.imap(test_case_run_process, cases):
|
|
if logname:
|
|
self.write_log(result)
|
|
progress.has_run(result)
|
|
except KeyboardInterrupt:
|
|
pool.terminate()
|
|
pool.join()
|
|
|
|
if print_summary:
|
|
self.print_summary(progress, logname)
|
|
if full_summary:
|
|
self.print_failure_output(progress, logname)
|
|
else:
|
|
print("")
|
|
print("Use --full-summary to see output from failed tests")
|
|
print("")
|
|
return progress.failed
|
|
|
|
def write_log(self, result):
|
|
name = result.case.get_name()
|
|
mode = result.case.get_mode()
|
|
if result.has_unexpected_outcome():
|
|
if result.case.is_negative():
|
|
self.logf.write(
|
|
"=== %s passed in %s, but was expected to fail === \n" % (name, mode))
|
|
self.logf.write("--- expected error: %s ---\n" % result.case.GetNegativeType())
|
|
result.write_output(self.logf)
|
|
else:
|
|
self.logf.write("=== %s failed in %s === \n" % (name, mode))
|
|
result.write_output(self.logf)
|
|
self.logf.write("===\n")
|
|
elif result.case.is_negative():
|
|
self.logf.write("%s failed in %s as expected \n" % (name, mode))
|
|
else:
|
|
self.logf.write("%s passed in %s \n" % (name, mode))
|
|
|
|
def print_source(self, tests):
|
|
cases = self.enumerate_tests(tests, "")
|
|
if cases:
|
|
cases[0].print_source()
|
|
|
|
def list_includes(self, tests):
|
|
cases = self.enumerate_tests(tests, "")
|
|
includes_dict = Counter()
|
|
for case in cases:
|
|
includes = case.get_include_list()
|
|
includes_dict.update(includes)
|
|
|
|
print(includes_dict)
|
|
|
|
|
|
def main():
|
|
code = 0
|
|
parser = build_options()
|
|
(options, args) = parser.parse_args()
|
|
validate_options(options)
|
|
|
|
test_suite = TestSuite(options)
|
|
|
|
test_suite.validate()
|
|
if options.loglevel == 'debug':
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
elif options.loglevel == 'info':
|
|
logging.basicConfig(level=logging.INFO)
|
|
elif options.loglevel == 'warning':
|
|
logging.basicConfig(level=logging.WARNING)
|
|
elif options.loglevel == 'error':
|
|
logging.basicConfig(level=logging.ERROR)
|
|
elif options.loglevel == 'critical':
|
|
logging.basicConfig(level=logging.CRITICAL)
|
|
|
|
if options.cat:
|
|
test_suite.print_source(args)
|
|
elif options.list_includes:
|
|
test_suite.list_includes(args)
|
|
else:
|
|
code = test_suite.run(options.command, args,
|
|
options.summary or options.full_summary,
|
|
options.full_summary,
|
|
options.logname,
|
|
options.job_count)
|
|
return code
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
sys.exit(main())
|
|
except Test262Error as exception:
|
|
print("Error: %s" % exception.message)
|
|
sys.exit(1)
|