Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,26 @@
# Copyright 2012 the V8 project authors. All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,224 @@
# Copyright 2018 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Wrapper around the Android device abstraction from src/build/android.
"""
import logging
import os
import sys
import re
from pathlib import Path
BASE_DIR = os.path.normpath(
os.path.join(os.path.dirname(__file__), '..', '..', '..'))
ANDROID_DIR = os.path.join(BASE_DIR, 'build', 'android')
DEVICE_DIR = '/data/local/tmp/v8/'
FILTER_LINES_EXPRESSIONS = [
r'WARNING: linker: .+ unsupported flags DT_FLAGS_1=0x8000001',
r'WARNING: linker: .+ unused DT entry:.*type 0x70000001 arg 0x0.*',
]
FILTER_LINES_EXPRESSIONS = [re.compile(exp) for exp in FILTER_LINES_EXPRESSIONS]
class TimeoutException(Exception):
def __init__(self, timeout, output=None):
self.timeout = timeout
self.output = output
class CommandFailedException(Exception):
def __init__(self, status, output):
self.status = status
self.output = output
class Driver(object):
__instance = None
@staticmethod
def instance(device):
if not Driver.__instance:
Driver.__instance = Driver(device)
return Driver.__instance
"""Helper class to execute shell commands on an Android device."""
def __init__(self, device=None):
assert os.path.exists(ANDROID_DIR)
sys.path.insert(0, ANDROID_DIR)
# We import the dependencies only on demand, so that this file can be
# imported unconditionally.
import devil_chromium
from devil.android import device_errors # pylint: disable=import-error
from devil.android import device_utils # pylint: disable=import-error
from devil.android.perf import cache_control # pylint: disable=import-error
from devil.android.perf import perf_control # pylint: disable=import-error
global cache_control
global device_errors
global perf_control
devil_chromium.Initialize()
# Find specified device or a single attached device if none was specified.
# In case none or multiple devices are attached, this raises an exception.
self.device = device_utils.DeviceUtils.HealthyDevices(
retries=5, enable_usb_resets=True, device_arg=device)[0]
# Retrieve device parameters.
product_prop = 'getprop ro.build.product'
self.device_type = self.device.adb.Shell(product_prop).rstrip('\n')
assert self.device_type, 'No device type found in android environment.'
# This remembers what we have already pushed to the device.
self.pushed = set()
def tear_down(self):
"""Clean up files after running all tests."""
self.device.RemovePath(DEVICE_DIR, force=True, recursive=True)
def push_file(self, host_dir, file_name, target_rel='.',
skip_if_missing=False):
"""Push a single file to the device (cached).
Args:
host_dir: Absolute parent directory of the file to push.
file_name: Name of the file to push.
target_rel: Parent directory of the target location on the device
(relative to the device's base dir for testing).
skip_if_missing: Keeps silent about missing files when set. Otherwise logs
error.
"""
# TODO(sergiyb): Implement this method using self.device.PushChangedFiles to
# avoid accessing low-level self.device.adb.
file_on_host = os.path.join(host_dir, file_name)
# Only push files not yet pushed in one execution.
if file_on_host in self.pushed:
return
file_on_device_tmp = os.path.join(DEVICE_DIR, '_tmp_', file_name)
file_on_device = os.path.join(DEVICE_DIR, target_rel, file_name)
folder_on_device = os.path.dirname(file_on_device)
# Only attempt to push files that exist.
if not os.path.exists(file_on_host):
if not skip_if_missing:
logging.critical('Missing file on host: %s' % file_on_host)
return
# Work-around for 'text file busy' errors. Push the files to a temporary
# location and then copy them with a shell command.
output = self.device.adb.Push(file_on_host, file_on_device_tmp)
# Success looks like this: '3035 KB/s (12512056 bytes in 4.025s)'.
# Errors look like this: 'failed to copy ... '.
if output and not re.search('^[0-9]', output.splitlines()[-1]):
logging.critical('PUSH FAILED: ' + output)
self.device.adb.Shell('mkdir -p %s' % folder_on_device)
self.device.adb.Shell('cp %s %s' % (file_on_device_tmp, file_on_device))
self.pushed.add(file_on_host)
def push_files_rec(self, host_dir, target_rel='.'):
"""As above, but push the whole directory tree under host_dir."""
root = Path(host_dir)
for entry in root.rglob('*'):
if entry.is_file():
self.push_file(host_dir, entry.relative_to(root), target_rel)
def push_executable(self, shell_dir, target_dir, binary):
"""Push files required to run a V8 executable.
Args:
shell_dir: Absolute parent directory of the executable on the host.
target_dir: Parent directory of the executable on the device (relative to
devices' base dir for testing).
binary: Name of the binary to push.
"""
self.push_file(shell_dir, binary, target_dir)
# Push external startup data. Backwards compatible for revisions where
# these files didn't exist. Or for bots that don't produce these files.
self.push_file(
shell_dir,
'natives_blob.bin',
target_dir,
skip_if_missing=True,
)
self.push_file(
shell_dir,
'snapshot_blob.bin',
target_dir,
skip_if_missing=True,
)
self.push_file(
shell_dir,
'icudtl.dat',
target_dir,
skip_if_missing=True,
)
def filter_line(self, line):
for exp in FILTER_LINES_EXPRESSIONS:
if exp.match(line):
return False
return True
def run(self, target_dir, binary, args, rel_path, timeout, env=None,
logcat_file=False):
"""Execute a command on the device's shell.
Args:
target_dir: Parent directory of the executable on the device (relative to
devices' base dir for testing).
binary: Name of the binary.
args: List of arguments to pass to the binary.
rel_path: Relative path on device to use as CWD.
timeout: Timeout in seconds.
env: The environment variables with which the command should be run.
logcat_file: File into which to stream adb logcat log.
"""
binary_on_device = os.path.join(DEVICE_DIR, target_dir, binary)
cmd = [binary_on_device] + args
def run_inner():
try:
output = self.device.RunShellCommand(
cmd,
cwd=os.path.join(DEVICE_DIR, rel_path),
check_return=True,
env=env,
timeout=timeout,
retries=0,
)
# Return output without linker warnings (https://crbug.com/1454414).
return '\n'.join(filter(self.filter_line, output))
except device_errors.AdbCommandFailedError as e:
raise CommandFailedException(e.status, e.output)
except device_errors.CommandTimeoutError as e:
raise TimeoutException(timeout, e.output)
if logcat_file:
with self.device.GetLogcatMonitor(output_file=logcat_file) as logmon:
result = run_inner()
logmon.Close()
return result
else:
return run_inner()
def drop_ram_caches(self):
"""Drop ran caches on device."""
cache = cache_control.CacheControl(self.device)
cache.DropRamCaches()
def set_high_perf_mode(self):
"""Set device into high performance mode."""
perf = perf_control.PerfControl(self.device)
perf.SetHighPerfMode()
def set_default_perf_mode(self):
"""Set device into default performance mode."""
perf = perf_control.PerfControl(self.device)
perf.SetDefaultPerfMode()

View File

@ -0,0 +1,457 @@
# Copyright 2017 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from contextlib import contextmanager
from pathlib import Path
import logging
import os
import re
import signal
import subprocess
import sys
import threading
import time
from ..local.android import (Driver, CommandFailedException, TimeoutException)
from ..local.pool import AbortException
from ..objects import output
from .process_utils import ProcessStats, EMPTY_PROCESS_LOGGER, PROCESS_LOGGER
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
SEM_INVALID_VALUE = -1
SEM_NOGPFAULTERRORBOX = 0x0002 # Microsoft Platform SDK WinBase.h
def setup_testing():
"""For testing only: We use threading under the hood instead of
multiprocessing to make coverage work. Signal handling is only supported
in the main thread, so we disable it for testing.
"""
signal.signal = lambda *_: None
@contextmanager
def handle_sigterm(process, abort_fun, enabled):
"""Call`abort_fun` on sigterm and restore previous handler to prevent
erroneous termination of an already terminated process.
Args:
process: The process to terminate.
abort_fun: Function taking two parameters: the process to terminate and
an array with a boolean for storing if an abort occured.
enabled: If False, this wrapper will be a no-op.
"""
# Variable to communicate with the signal handler.
abort_occured = [False]
if enabled:
# TODO(https://crbug.com/v8/13113): There is a race condition on
# signal handler registration. In rare cases, the SIGTERM for stopping
# a worker might be caught right after a long running process has been
# started (or logic that starts it isn't interrupted), but before the
# registration of the abort_fun. In this case, process.communicate will
# block until the process is done.
previous = signal.getsignal(signal.SIGTERM)
def handler(signum, frame):
abort_fun(process, abort_occured)
if previous and callable(previous):
# Call default signal handler. If this command is called from a worker
# process, its signal handler will gracefully stop processing.
previous(signum, frame)
signal.signal(signal.SIGTERM, handler)
try:
yield
finally:
if enabled:
signal.signal(signal.SIGTERM, previous)
if abort_occured[0]:
raise AbortException()
class BaseCommand(object):
def __init__(self, shell, args=None, cmd_prefix=None, timeout=60, env=None,
verbose=False, test_case=None, handle_sigterm=False,
log_process_stats=False):
"""Initialize the command.
Args:
shell: The name of the executable (e.g. d8).
args: List of args to pass to the executable.
cmd_prefix: Prefix of command (e.g. a wrapper script).
timeout: Timeout in seconds.
env: Environment dict for execution.
verbose: Print additional output.
test_case: Test case reference.
handle_sigterm: Flag indicating if SIGTERM will be used to terminate the
underlying process. Should not be used from the main thread, e.g. when
using a command to list tests.
log_process_stats: Indicate if we want to probe for process statistics like
memory consumption.
"""
assert(timeout > 0)
self.shell = Path(shell)
self.args = list(map(str, args or []))
self.cmd_prefix = cmd_prefix or []
self.timeout = timeout
self.env = env or {}
self.verbose = verbose
self.handle_sigterm = handle_sigterm
if log_process_stats:
self.process_logger = self.get_process_logger()
else:
self.process_logger = EMPTY_PROCESS_LOGGER
def _result_overrides(self, returncode):
pass
@contextmanager
def log_errors(self):
try:
yield
except:
logging.exception(f'Error executing: {self}\n')
raise
def get_process_logger(self):
return EMPTY_PROCESS_LOGGER
def execute(self):
if self.verbose:
print('# %s' % self)
process = self._start_process()
with handle_sigterm(process, self._abort, self.handle_sigterm):
# Variable to communicate with the timer.
timeout_occured = [False]
timer = threading.Timer(
self.timeout, self._abort, [process, timeout_occured])
timer.start()
start_time = time.time()
with self.log_errors():
with self.process_logger.log_stats(process) as stats:
stdout, stderr = process.communicate()
end_time = time.time()
timer.cancel()
self._result_overrides(process)
return output.Output(
process.returncode,
timeout_occured[0],
stdout.decode('utf-8', 'replace'),
stderr.decode('utf-8', 'replace'),
process.pid,
start_time,
end_time,
stats=stats,
)
def _start_process(self):
with self.log_errors():
return subprocess.Popen(
args=self._get_popen_args(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=self._get_env(),
)
def _get_popen_args(self):
return self._to_args_list()
def _get_env(self):
env = os.environ.copy()
env.update(self.env)
# GTest shard information is read by the V8 tests runner. Make sure it
# doesn't leak into the execution of gtests we're wrapping. Those might
# otherwise apply a second level of sharding and as a result skip tests.
env.pop('GTEST_TOTAL_SHARDS', None)
env.pop('GTEST_SHARD_INDEX', None)
return env
def _kill_process(self, process):
raise NotImplementedError()
def _abort(self, process, abort_called):
abort_called[0] = True
started_as = self.to_string(relative=True)
process_text = 'process %d started as:\n %s\n' % (process.pid, started_as)
try:
logging.warning('Attempting to kill %s', process_text)
self._kill_process(process)
except OSError:
logging.exception('Unruly %s', process_text)
def __str__(self):
return self.to_string()
def to_string(self, relative=False):
def escape(part):
# Escape spaces. We may need to escape more characters for this to work
# properly.
if ' ' in part:
return '"%s"' % part
return part
parts = map(escape, self._to_args_list())
cmd = ' '.join(parts)
if relative:
cmd = cmd.replace(os.getcwd() + os.sep, '')
return cmd
def _to_args_list(self):
return list(map(str, self.cmd_prefix + [self.shell])) + self.args
class DesktopCommand(BaseCommand):
def get_process_logger(self):
return PROCESS_LOGGER
class PosixCommand(DesktopCommand):
# TODO(machenbach): Use base process start without shell once
# https://crbug.com/v8/8889 is resolved.
def _start_process(self):
def wrapped(arg):
if set('() \'"') & set(arg):
return "'%s'" % arg.replace("'", "'\"'\"'")
return arg
try:
return subprocess.Popen(
args=' '.join(map(wrapped, self._get_popen_args())),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=self._get_env(),
shell=True,
# Make the new shell create its own process group. This allows to kill
# all spawned processes reliably (https://crbug.com/v8/8292).
preexec_fn=os.setsid,
)
except Exception as e:
sys.stderr.write('Error executing: %s\n' % self)
raise e
def _kill_process(self, process):
# Kill the whole process group (PID == GPID after setsid).
# First try a soft term to allow some feedback
os.killpg(process.pid, signal.SIGTERM)
# Give the process some time to cleanly terminate.
time.sleep(0.1)
# Forcefully kill processes.
os.killpg(process.pid, signal.SIGKILL)
class IOSCommand(BaseCommand):
def __init__(self,
shell,
args=None,
cmd_prefix=None,
timeout=120,
env=None,
verbose=False,
test_case=None,
handle_sigterm=False,
log_process_stats=False):
"""Initialize the command and set a large enough timeout required for runs
through the iOS Simulator.
"""
super(IOSCommand, self).__init__(
shell,
args=args,
cmd_prefix=cmd_prefix,
timeout=timeout,
env=env,
verbose=verbose,
handle_sigterm=handle_sigterm,
log_process_stats=log_process_stats)
def _result_overrides(self, process):
# TODO(crbug.com/1445694): if iossim returns with code 65, force a
# successful exit instead.
if (process.returncode == 65):
process.returncode = 0
def _start_process(self):
try:
return subprocess.Popen(
args=self._get_popen_args(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=self._get_env(),
shell=True,
# Make the new shell create its own process group. This allows to kill
# all spawned processes reliably (https://crbug.com/v8/8292).
preexec_fn=os.setsid,
)
except Exception as e:
sys.stderr.write('Error executing: %s\n' % self)
raise e
def _kill_process(self, process):
# Kill the whole process group (PID == GPID after setsid).
# First try a soft term to allow some feedback
os.killpg(process.pid, signal.SIGTERM)
# Give the process some time to cleanly terminate.
time.sleep(0.1)
# Forcefully kill processes.
os.killpg(process.pid, signal.SIGKILL)
def _to_args_list(self):
return list(map(str, self.cmd_prefix + [self.shell]))
def terminate_process_windows(process):
try:
import _winapi
handle = _winapi.OpenProcess(
_winapi.PROCESS_ALL_ACCESS, False, process.pid)
_winapi.TerminateProcess(handle, 1)
_winapi.CloseHandle(handle)
except Exception:
logging.exception('Problem terminating process %s', process.pid)
class WindowsCommand(DesktopCommand):
def _start_process(self, **kwargs):
# Try to change the error mode to avoid dialogs on fatal errors. Don't
# touch any existing error mode flags by merging the existing error mode.
# See http://blogs.msdn.com/oldnewthing/archive/2004/07/27/198410.aspx.
def set_error_mode(mode):
prev_error_mode = SEM_INVALID_VALUE
try:
import ctypes
prev_error_mode = (
ctypes.windll.kernel32.SetErrorMode(mode)) #@UndefinedVariable
except ImportError:
pass
return prev_error_mode
error_mode = SEM_NOGPFAULTERRORBOX
prev_error_mode = set_error_mode(error_mode)
set_error_mode(error_mode | prev_error_mode)
try:
return super(WindowsCommand, self)._start_process(**kwargs)
finally:
if prev_error_mode != SEM_INVALID_VALUE:
set_error_mode(prev_error_mode)
def _get_popen_args(self):
return subprocess.list2cmdline(self._to_args_list())
def _kill_process(self, process):
terminate_process_windows(process)
class AndroidCommand(BaseCommand):
# This must be initialized before creating any instances of this class.
driver = None
def __init__(self, shell, args=None, cmd_prefix=None, timeout=60, env=None,
verbose=False, test_case=None, handle_sigterm=False,
log_process_stats=False):
"""Initialize the command and all files that need to be pushed to the
Android device.
"""
super(AndroidCommand, self).__init__(
shell, args=args, cmd_prefix=cmd_prefix, timeout=timeout, env=env,
verbose=verbose, handle_sigterm=handle_sigterm,
log_process_stats=log_process_stats)
rel_args, files_from_args = args_with_relative_paths(args)
self.args = rel_args
test_case_resources = test_case.get_android_resources() if test_case else []
self.files_to_push = test_case_resources + files_from_args
def execute(self, **additional_popen_kwargs):
"""Execute the command on the device.
This pushes all required files to the device and then runs the command.
"""
if self.verbose:
print('# %s' % self)
shell_name = self.shell.name
shell_dir = self.shell.parent
self.driver.push_executable(shell_dir, 'bin', shell_name)
self.push_test_resources()
start_time = time.time()
return_code = 0
timed_out = False
try:
stdout = self.driver.run(
'bin', shell_name, self.args, '.', self.timeout, self.env)
except CommandFailedException as e:
return_code = e.status
stdout = e.output
except TimeoutException as e:
return_code = 1
timed_out = True
# Sadly the Android driver doesn't provide output on timeout.
stdout = ''
end_time = time.time()
return output.Output(
return_code,
timed_out,
stdout,
'', # No stderr available.
-1, # No pid available.
start_time,
end_time,
)
def push_test_resources(self):
for abs_file in self.files_to_push:
abs_dir = abs_file.parent
file_name = abs_file.name
rel_dir = abs_dir.relative_to(BASE_DIR)
self.driver.push_file(abs_dir, file_name, rel_dir)
def args_with_relative_paths(args):
base_dir_str = re.escape(BASE_DIR.as_posix())
rel_args = []
files_to_push = []
find_path_re = re.compile(r'.*(%s/[^\'"]+).*' % base_dir_str)
for arg in (args or []):
match = find_path_re.match(str(arg))
if match:
files_to_push.append(Path(match.group(1)).resolve())
rel_args.append(re.sub(r'(.*)%s/(.*)' % base_dir_str, r'\1\2', str(arg)))
return rel_args, files_to_push
Command = None
# Deprecated : use context.os_context
def setup(target_os, device):
"""Set the Command class to the OS-specific version."""
global Command
if target_os == 'android':
AndroidCommand.driver = Driver.instance(device)
Command = AndroidCommand
elif target_os == 'ios':
Command = IOSCommand
elif target_os == 'windows':
Command = WindowsCommand
else:
Command = PosixCommand
# Deprecated : use context.os_context
def tear_down():
"""Clean up after using commands."""
if Command == AndroidCommand:
AndroidCommand.driver.tear_down()

View File

@ -0,0 +1,138 @@
# Copyright 2022 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import signal
import subprocess
import sys
from contextlib import contextmanager
from ..local.android import Driver
from .command import (
AndroidCommand,
IOSCommand,
PosixCommand,
WindowsCommand,
terminate_process_windows)
from .pool import DefaultExecutionPool
from .process_utils import EMPTY_PROCESS_LOGGER, PROCESS_LOGGER
from ..testproc.util import list_processes_linux
class DefaultOSContext:
def __init__(self, command, pool=None):
self.command = command
self.pool = pool or DefaultExecutionPool(self)
@contextmanager
def handle_context(self, options):
yield
def list_processes(self):
return []
def terminate_process(self, process):
pass
def platform_shell(self, shell, args, outdir):
return outdir.resolve() / shell
@property
def device_type(self):
return None
class DesktopContext(DefaultOSContext):
@contextmanager
def handle_context(self, options):
log_path = options.log_system_memory
logger = PROCESS_LOGGER if log_path else EMPTY_PROCESS_LOGGER
with logger.log_system_memory(log_path):
yield
class PosixContext(DesktopContext):
def __init__(self):
super().__init__(PosixCommand)
def list_processes(self):
return list_processes_linux()
def terminate_process(self, process):
os.kill(process.pid, signal.SIGTERM)
class WindowsContext(DesktopContext):
def __init__(self):
super().__init__(WindowsCommand)
def terminate_process(self, process):
terminate_process_windows(process)
def platform_shell(self, shell, args, outdir):
return outdir.resolve() / f'{shell}.exe'
class AndroidOSContext(DefaultOSContext):
def __init__(self):
super().__init__(AndroidCommand)
@contextmanager
def handle_context(self, options):
try:
AndroidCommand.driver = Driver.instance(options.device)
yield
finally:
AndroidCommand.driver.tear_down()
@property
def device_type(self):
return AndroidCommand.driver.device_type
class IOSContext(DefaultOSContext):
def __init__(self):
super().__init__(IOSCommand)
def terminate_process(self, process):
os.kill(process.pid, signal.SIGTERM)
def platform_shell(self, shell, appargs, outdir):
# Rather than having to use a physical device (iPhone, iPad, etc), we use
# the iOS Simulator for the test runners in order to ease the job of
# builders and testers.
# At the moment Chromium's iossim tool is being used, which is a wrapper
# around 'simctl' macOS command utility.
iossim = outdir.resolve() / "iossim -d 'iPhone X' "
if isinstance(appargs, list):
appargs = ' '.join(map(str, appargs))
if appargs != "":
iossim = f'{iossim}-c '
appargs = '\"' + appargs + '\"'
app = outdir.resolve() / f'{shell}.app'
return f'{iossim}{appargs} {app}'
# TODO(liviurau): Add documentation with diagrams to describe how context and
# its components gets initialized and eventually teared down and how does it
# interact with both tests and underlying platform specific concerns.
def find_os_context_factory(target_os):
registry = dict(
android=AndroidOSContext, ios=IOSContext, windows=WindowsContext)
return registry.get(target_os, PosixContext)
@contextmanager
def os_context(target_os, options):
factory = find_os_context_factory(target_os)
context_instance = factory()
with context_instance.handle_context(options):
yield context_instance

View File

@ -0,0 +1,5 @@
# Copyright 2019 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
[]

View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
# Copyright 2019 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from pathlib import Path
from testrunner.local import testsuite, statusfile
from testrunner.objects.testcase import TestCase
class TestLoader(testsuite.TestLoader):
def _list_test_filenames(self):
return ["fast", "slow", "heavy"]
def list_tests(self):
self.test_count_estimation = 3
fast = self._create_test(Path("fast"), self.suite)
slow = self._create_test(Path("slow"), self.suite)
heavy = self._create_test(Path("heavy"), self.suite)
slow._statusfile_outcomes.append(statusfile.SLOW)
heavy._statusfile_outcomes.append(statusfile.HEAVY)
yield fast
yield slow
yield heavy
class TestSuite(testsuite.TestSuite):
def _test_loader_class(self):
return TestLoader
def _test_class(self):
return TestCase

401
deps/v8/tools/testrunner/local/pool.py vendored Normal file
View File

@ -0,0 +1,401 @@
#!/usr/bin/env python3
# Copyright 2014 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import logging
import os
import signal
import threading
import traceback
from contextlib import contextmanager
from multiprocessing import Process, Queue
from queue import Empty
def setup_testing():
"""For testing only: Use threading under the hood instead of multiprocessing
to make coverage work.
"""
global Queue
global Process
del Queue
del Process
from queue import Queue
from threading import Thread as Process
# Monkeypatch os.kill and add fake pid property on Thread.
os.kill = lambda *args: None
Process.pid = property(lambda self: None)
class AbortException(Exception):
"""Indicates early abort on SIGINT, SIGTERM or internal hard timeout."""
pass
class NormalResult():
def __init__(self, result):
self.result = result
self.exception = None
class ExceptionResult():
def __init__(self, exception):
self.exception = exception
class MaybeResult():
def __init__(self, heartbeat, value):
self.heartbeat = heartbeat
self.value = value
@staticmethod
def create_heartbeat():
return MaybeResult(True, None)
@staticmethod
def create_result(value):
return MaybeResult(False, value)
def Worker(fn, work_queue, done_queue,
process_context_fn=None, process_context_args=None):
"""Worker to be run in a child process.
The worker stops when the poison pill "STOP" is reached.
"""
# Install a default signal handler for SIGTERM that stops the processing
# loop below on the next occasion. The job function "fn" is supposed to
# register their own handler to avoid blocking, but still chain to this
# handler on SIGTERM to terminate the loop quickly.
stop = [False]
def handler(signum, frame):
stop[0] = True
signal.signal(signal.SIGTERM, handler)
try:
kwargs = {}
if process_context_fn and process_context_args is not None:
kwargs.update(process_context=process_context_fn(*process_context_args))
for args in iter(work_queue.get, "STOP"):
if stop[0]:
# SIGINT, SIGTERM or internal hard timeout caught outside the execution
# of "fn".
break
try:
done_queue.put(NormalResult(fn(*args, **kwargs)))
except AbortException:
# SIGINT, SIGTERM or internal hard timeout caught during execution of
# "fn".
break
except Exception as e:
logging.exception('Unhandled error during worker execution.')
done_queue.put(ExceptionResult(e))
except KeyboardInterrupt:
assert False, 'Unreachable'
@contextmanager
def without_sig():
int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
term_handler = signal.signal(signal.SIGTERM, signal.SIG_IGN)
try:
yield
finally:
signal.signal(signal.SIGINT, int_handler)
signal.signal(signal.SIGTERM, term_handler)
@contextmanager
def drain_queue_async(queue):
"""Drains a queue in a background thread until the wrapped code unblocks.
This can be used to unblock joining a child process that might still write
to the queue. The join should be wrapped by this context manager.
"""
keep_running = True
def empty_queue():
elem_count = 0
while keep_running:
try:
while True:
queue.get(True, 0.1)
elem_count += 1
if elem_count < 200:
logging.info('Drained an element from queue.')
except Empty:
pass
except:
logging.exception('Error draining queue.')
emptier = threading.Thread(target=empty_queue)
emptier.start()
yield
keep_running = False
emptier.join()
class ContextPool():
def __init__(self):
self.abort_now = False
def init(self, num_workers, heartbeat_timeout=1, notify_function=None):
"""
Delayed initialization. At context creation time we have no access to the
below described parameters.
Args:
num_workers: Number of worker processes to run in parallel.
heartbeat_timeout: Timeout in seconds for waiting for results. Each time
the timeout is reached, a heartbeat is signalled and timeout is reset.
notify_function: Callable called to signal some events like termination. The
event name is passed as string.
"""
pass
def add_jobs(self, jobs):
pass
def results(self, requirement):
pass
def abort(self):
self.abort_now = True
ProcessContext = collections.namedtuple('ProcessContext', ['result_reduction'])
class DefaultExecutionPool(ContextPool):
"""Distributes tasks to a number of worker processes.
New tasks can be added dynamically even after the workers have been started.
Requirement: Tasks can only be added from the parent process, e.g. while
consuming the results generator."""
# Factor to calculate the maximum number of items in the work/done queue.
# Necessary to not overflow the queue's pipe if a keyboard interrupt happens.
BUFFER_FACTOR = 4
def __init__(self, os_context=None):
super(DefaultExecutionPool, self).__init__()
self.os_context = os_context
self.processes = []
self.terminated = False
# Invariant: processing_count >= #work_queue + #done_queue. It is greater
# when a worker takes an item from the work_queue and before the result is
# submitted to the done_queue. It is equal when no worker is working,
# e.g. when all workers have finished, and when no results are processed.
# Count is only accessed by the parent process. Only the parent process is
# allowed to remove items from the done_queue and to add items to the
# work_queue.
self.processing_count = 0
# Disable sigint and sigterm to prevent subprocesses from capturing the
# signals.
with without_sig():
self.work_queue = Queue()
self.done_queue = Queue()
def init(self, num_workers=1, heartbeat_timeout=1, notify_function=None):
"""
Args:
num_workers: Number of worker processes to run in parallel.
heartbeat_timeout: Timeout in seconds for waiting for results. Each time
the timeout is reached, a heartbeat is signalled and timeout is reset.
notify_function: Callable called to signal some events like termination. The
event name is passed as string.
"""
self.num_workers = num_workers
self.heartbeat_timeout = heartbeat_timeout
self.notify = notify_function or (lambda x: x)
def add_jobs(self, jobs):
self.add(jobs)
def results(self, requirement):
return self.imap_unordered(
fn=run_job,
gen=[],
process_context_fn=ProcessContext,
process_context_args=[requirement],
)
def imap_unordered(self, fn, gen,
process_context_fn=None, process_context_args=None):
"""Maps function "fn" to items in generator "gen" on the worker processes
in an arbitrary order. The items are expected to be lists of arguments to
the function. Returns a results iterator. A result value of type
MaybeResult either indicates a heartbeat of the runner, i.e. indicating
that the runner is still waiting for the result to be computed, or it wraps
the real result.
Args:
process_context_fn: Function executed once by each worker. Expected to
return a process-context object. If present, this object is passed
as additional argument to each call to fn.
process_context_args: List of arguments for the invocation of
process_context_fn. All arguments will be pickled and sent beyond the
process boundary.
"""
if self.terminated:
return
try:
internal_error = False
gen = iter(gen)
self.advance = self._advance_more
# Disable sigint and sigterm to prevent subprocesses from capturing the
# signals.
with without_sig():
for w in range(self.num_workers):
p = Process(target=Worker, args=(fn,
self.work_queue,
self.done_queue,
process_context_fn,
process_context_args))
p.start()
self.processes.append(p)
self.advance(gen)
while self.processing_count > 0:
while True:
try:
# Read from result queue in a responsive fashion. If available,
# this will return a normal result immediately or a heartbeat on
# heartbeat timeout (default 1 second).
result = self._get_result_from_queue()
except:
# TODO(machenbach): Handle a few known types of internal errors
# gracefully, e.g. missing test files.
logging.exception('Internal error in a worker process.')
internal_error = True
continue
finally:
if self.abort_now:
# SIGINT, SIGTERM or internal hard timeout.
return
yield result
break
self.advance(gen)
except KeyboardInterrupt:
assert False, 'Unreachable'
except Exception:
logging.exception('Unhandled error during pool execution.')
finally:
self._terminate()
if internal_error:
raise Exception('Internal error in a worker process.')
def _advance_more(self, gen):
while self.processing_count < self.num_workers * self.BUFFER_FACTOR:
try:
self.work_queue.put(next(gen))
self.processing_count += 1
except StopIteration:
self.advance = self._advance_empty
break
def _advance_empty(self, gen):
pass
def add(self, args):
"""Adds an item to the work queue. Can be called dynamically while
processing the results from imap_unordered."""
assert not self.terminated
self.work_queue.put(args)
self.processing_count += 1
def abort(self):
"""Schedules abort on next queue read.
This is safe to call when handling SIGINT, SIGTERM or when an internal
hard timeout is reached.
"""
self.abort_now = True
def _terminate_processes(self):
for p in self.processes:
self.os_context.terminate_process(p)
def _terminate(self):
"""Terminates execution and cleans up the queues.
If abort() was called before termination, this also terminates the
subprocesses and doesn't wait for ongoing tests.
"""
if self.terminated:
return
self.terminated = True
# Drain out work queue from tests
try:
while True:
self.work_queue.get(True, 0.1)
except Empty:
pass
# Make sure all processes stop
for _ in self.processes:
# During normal tear down the workers block on get(). Feed a poison pill
# per worker to make them stop.
self.work_queue.put("STOP")
# Send a SIGTERM to all workers. They will gracefully terminate their
# processing loop and if the signal is caught during job execution they
# will try to terminate the ongoing test processes quickly.
if self.abort_now:
self._terminate_processes()
self.notify("Joining workers")
with drain_queue_async(self.done_queue):
for p in self.processes:
p.join()
self.notify("Pool terminated")
def _get_result_from_queue(self):
"""Attempts to get the next result from the queue.
Returns: A wrapped result if one was available within heartbeat timeout,
a heartbeat result otherwise.
Raises:
Exception: If an exception occured when processing the task on the
worker side, it is reraised here.
"""
while True:
try:
result = self.done_queue.get(timeout=self.heartbeat_timeout)
self.processing_count -= 1
if result.exception:
raise result.exception
return MaybeResult.create_result(result.result)
except Empty:
return MaybeResult.create_heartbeat()
class SingleThreadedExecutionPool(ContextPool):
def __init__(self):
super(SingleThreadedExecutionPool, self).__init__()
self.work_queue = []
def add_jobs(self, jobs):
self.work_queue.extend(jobs)
def results(self, requirement):
while self.work_queue and not self.abort_now:
job = self.work_queue.pop()
yield MaybeResult.create_result(job.run(ProcessContext(requirement)))
# Global function for multiprocessing, because pickling a static method doesn't
# work on Windows.
def run_job(job, process_context):
return job.run(process_context)

82
deps/v8/tools/testrunner/local/pool_test.py vendored Executable file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
# Copyright 2014 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import unittest
from queue import Empty, Full, Queue
# Needed because the test runner contains relative imports.
TOOLS_PATH = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(TOOLS_PATH)
from testrunner.local.pool import DefaultExecutionPool, drain_queue_async
def Run(x):
if x == 10:
raise Exception("Expected exception triggered by test.")
return x
class PoolTest(unittest.TestCase):
def testNormal(self):
results = set()
pool = DefaultExecutionPool()
pool.init(3)
for result in pool.imap_unordered(Run, [[x] for x in range(0, 10)]):
if result.heartbeat:
# Any result can be a heartbeat due to timings.
continue
results.add(result.value)
self.assertEqual(set(range(0, 10)), results)
def testException(self):
results = set()
pool = DefaultExecutionPool()
pool.init(3)
with self.assertRaises(Exception):
for result in pool.imap_unordered(Run, [[x] for x in range(0, 12)]):
if result.heartbeat:
# Any result can be a heartbeat due to timings.
continue
# Item 10 will not appear in results due to an internal exception.
results.add(result.value)
expect = set(range(0, 12))
expect.remove(10)
self.assertEqual(expect, results)
def testAdd(self):
results = set()
pool = DefaultExecutionPool()
pool.init(3)
for result in pool.imap_unordered(Run, [[x] for x in range(0, 10)]):
if result.heartbeat:
# Any result can be a heartbeat due to timings.
continue
results.add(result.value)
if result.value < 30:
pool.add([result.value + 20])
self.assertEqual(
set(range(0, 10)) | set(range(20, 30)) | set(range(40, 50)), results)
class QueueTest(unittest.TestCase):
def testDrainQueueAsync(self):
queue = Queue(1)
queue.put('foo')
with self.assertRaises(Full):
queue.put('bar', timeout=0.01)
with drain_queue_async(queue):
queue.put('bar')
with self.assertRaises(Empty):
queue.get(False)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,170 @@
# Copyright 2023 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Tools for tracking process statistics like memory consumption.
"""
import platform
import time
from contextlib import contextmanager
from datetime import datetime
from threading import Thread, Event
PROBING_INTERVAL_SEC = 0.2
FLUSH_LOG_BUFFER_SEC = 2
class ProcessStats:
"""Storage class for process statistics indicating if data is available."""
def __init__(self):
self._max_rss = 0
self._max_vms = 0
self._available = False
@property
def max_rss(self):
return self._max_rss
@property
def max_vms(self):
return self._max_vms
@property
def available(self):
return self._available
def update(self, memory_info):
self._max_rss = max(self._max_rss, memory_info.rss)
self._max_vms = max(self._max_vms, memory_info.vms)
self._available = True
class EmptyProcessLogger:
@contextmanager
def log_stats(self, process):
"""When wrapped, logs memory statistics of the Popen process argument.
This base-class version can be used as a null object to turn off the
feature, yielding null-object stats.
"""
yield ProcessStats()
@contextmanager
def log_system_memory(self, log_path):
"""When wrapped, logs system memory statistics to 'log_path'.
This base-class version keeps logging off.
"""
yield
class PSUtilProcessLogger(EmptyProcessLogger):
def __init__(
self, probing_interval_sec=PROBING_INTERVAL_SEC,
flush_log_buffer_sec=FLUSH_LOG_BUFFER_SEC):
self.probing_interval_sec = probing_interval_sec
self.log_buffer_max = int(flush_log_buffer_sec / probing_interval_sec)
def get_pid(self, pid):
return pid
@contextmanager
def log_stats(self, process):
try:
process_handle = psutil.Process(self.get_pid(process.pid))
except (psutil.AccessDenied, psutil.NoSuchProcess):
# Fetching process stats has an expected race condition with the
# running process, which might just have ended already.
yield ProcessStats()
return
stats = ProcessStats()
finished = Event()
def run_logger():
try:
while True:
stats.update(process_handle.memory_info())
if finished.wait(self.probing_interval_sec):
break
except (psutil.AccessDenied, psutil.NoSuchProcess):
pass
logger = Thread(target=run_logger)
logger.start()
try:
yield stats
finally:
finished.set()
# Until we have joined the logger thread, we can't access the stats
# without a race condition.
logger.join()
@contextmanager
def log_system_memory(self, log_path):
with open(log_path, 'w') as handle:
finished = Event()
buffer = []
def flush_buffer():
time_str = datetime.utcfromtimestamp(time.time())
values = ', '.join(map(lambda s: f'{s}%', buffer))
print(f'{time_str} - {values}', file=handle)
buffer.clear()
def run_logger():
while True:
buffer.append(psutil.virtual_memory().percent)
if len(buffer) >= self.log_buffer_max:
flush_buffer()
if finished.wait(self.probing_interval_sec):
if buffer:
flush_buffer()
break
logger = Thread(target=run_logger)
logger.start()
try:
yield
finally:
finished.set()
logger.join()
class LinuxPSUtilProcessLogger(PSUtilProcessLogger):
def get_pid(self, pid):
"""Try to get the correct PID on Linux.
On Linux, we call subprocesses using shell, which on some systems (Debian)
has an optimization using exec and reusing the parent PID, while others
(Ubuntu) create a child process with its own PID. We don't want to log
memory stats of the shell parent.
"""
try:
with open(f'/proc/{pid}/task/{pid}/children') as f:
children = f.read().strip().split(' ')
if children and children[0]:
# On Debian, we don't have child processes here.
return int(children[0])
except FileNotFoundError:
# A quick process might already have finished.
pass
return pid
EMPTY_PROCESS_LOGGER = EmptyProcessLogger()
try:
# Process utils are only supported when we use vpython or when psutil is
# installed.
import psutil
if platform.system() == 'Linux':
PROCESS_LOGGER = LinuxPSUtilProcessLogger()
else:
PROCESS_LOGGER = PSUtilProcessLogger()
except:
PROCESS_LOGGER = EMPTY_PROCESS_LOGGER

View File

@ -0,0 +1,153 @@
#!/usr/bin/env python3
# Copyright 2023 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import psutil
import threading
import unittest
from collections import namedtuple
from mock import patch
from pathlib import Path
from pyfakefs import fake_filesystem_unittest
import process_utils
Process = namedtuple('Process', 'pid')
MemoryInfo = namedtuple('MemoryInfo', 'rss, vms')
SystemMemory = namedtuple('SystemMemory', 'percent')
class TestStats(unittest.TestCase):
def test_update(self):
stats = process_utils.ProcessStats()
self.assertFalse(stats.available)
stats.update(MemoryInfo(17, 42))
self.assertTrue(stats.available)
self.assertEqual(17, stats.max_rss)
self.assertEqual(42, stats.max_vms)
stats.update(MemoryInfo(23, 23))
self.assertTrue(stats.available)
self.assertEqual(23, stats.max_rss)
self.assertEqual(42, stats.max_vms)
class TestProcessLogger(unittest.TestCase):
def test_null(self):
with process_utils.EmptyProcessLogger().log_stats(None) as stats:
pass
self.assertFalse(stats.available)
self.assertEqual(0, stats.max_rss)
self.assertEqual(0, stats.max_vms)
def test_basic(self):
"""Test three iterations of probing with mocked psutil."""
done = threading.Event()
results = [MemoryInfo(17, 101), MemoryInfo(2, 11), MemoryInfo(23, 42)]
class FakeProcess:
def __init__(self, process):
self.iter = iter(results)
def memory_info(self):
try:
return self.iter.__next__()
except StopIteration:
done.set()
raise psutil.NoSuchProcess(123)
logger = process_utils.PSUtilProcessLogger(0.01)
with patch('psutil.Process', FakeProcess):
with logger.log_stats(Process(123)) as stats:
done.wait()
self.assertTrue(stats.available)
self.assertEqual(23, stats.max_rss)
self.assertEqual(101, stats.max_vms)
def test_fast_process(self):
"""Test a process that finished fast."""
class FakeProcess:
def __init__(self, process):
raise psutil.NoSuchProcess(123)
logger = process_utils.PSUtilProcessLogger(0.01)
with patch('psutil.Process', FakeProcess):
with logger.log_stats(Process(123)) as stats:
pass
self.assertFalse(stats.available)
self.assertEqual(0, stats.max_rss)
self.assertEqual(0, stats.max_vms)
class TestLinuxProcessLogger(fake_filesystem_unittest.TestCase):
def test_child_process(self):
self.setUpPyfakefs(allow_root_user=True)
os.makedirs('/proc/123/task/123')
with open('/proc/123/task/123/children', 'w') as f:
f.write('42')
logger = process_utils.LinuxPSUtilProcessLogger()
self.assertEqual(42, logger.get_pid(123))
def test_no_child_process(self):
self.setUpPyfakefs(allow_root_user=True)
os.makedirs('/proc/123/task/123')
with open('/proc/123/task/123/children', 'w') as f:
f.write('')
logger = process_utils.LinuxPSUtilProcessLogger()
self.assertEqual(123, logger.get_pid(123))
def test_process_ended(self):
self.setUpPyfakefs(allow_root_user=True)
logger = process_utils.LinuxPSUtilProcessLogger()
self.assertEqual(123, logger.get_pid(123))
class TestSystemMemoryLogger(fake_filesystem_unittest.TestCase):
def test_log(self):
"""Test logging memory stats from a background thread.
Simulate deterministic stats and timestamps. The main thread waits
for at least two iterations.
"""
done = threading.Event()
results = 3 * [SystemMemory(1.7)]
it = iter(results)
def side_effect():
try:
return it.__next__()
except StopIteration:
done.set()
# If the main thread is slow, we might keep running into this.
return results[0]
output_file = Path('dir') / 'memory.log'
self.setUpPyfakefs()
self.fs.create_dir(output_file.parent)
logger = process_utils.PSUtilProcessLogger(0.01, 0.02)
with patch('psutil.virtual_memory', side_effect=side_effect):
with patch('time.time', return_value=13123212323):
with logger.log_system_memory(output_file):
done.wait()
# We get at least two lines of deterministic log. We might get more
# due to unpredictable delay after done.wait() on the main thread.
with open(output_file) as f:
lines = f.readlines()
expected = '2385-11-10 00:45:23 - 1.7%, 1.7%\n'
self.assertLess(1, len(lines))
self.assertEqual(expected, lines[0])
self.assertEqual(expected, lines[1])
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,363 @@
# Copyright 2012 the V8 project authors. All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
import re
from testrunner.build_config import INITIALIZATION_ERROR
from testrunner.local.variants import ALL_VARIANTS
from testrunner.local.utils import Freeze
# Possible outcomes
FAIL = "FAIL"
PASS = "PASS"
TIMEOUT = "TIMEOUT"
CRASH = "CRASH"
# Outcomes only for status file, need special handling
FAIL_OK = "FAIL_OK"
FAIL_SLOPPY = "FAIL_SLOPPY"
# Modifiers
HEAVY = "HEAVY"
SKIP = "SKIP"
SLOW = "SLOW"
NO_VARIANTS = "NO_VARIANTS"
FAIL_PHASE_ONLY = "FAIL_PHASE_ONLY"
ALWAYS = "ALWAYS"
KEYWORDS = {}
for key in [SKIP, FAIL, PASS, CRASH, HEAVY, SLOW, FAIL_OK, NO_VARIANTS,
FAIL_SLOPPY, ALWAYS, FAIL_PHASE_ONLY]:
KEYWORDS[key] = key
# Support arches, modes to be written as keywords instead of strings.
VARIABLES = {ALWAYS: True}
for var in [
"debug", "release", "big", "little", "android", "arm", "arm64", "ia32",
"mips64", "mips64el", "x64", "ppc64", "s390x", "macos", "windows", "linux",
"aix", "r1", "r2", "r3", "r5", "r6", "riscv32", "riscv64", "loong64", "zos",
"bullhead", "panther",
]:
assert var not in VARIABLES
VARIABLES[var] = var
# Allow using variants as keywords.
for var in ALL_VARIANTS:
assert var not in VARIABLES
VARIABLES[var] = var
class StatusFile(object):
def __init__(self, path, variables):
"""
_rules: {variant: {test name: [rule]}}
_prefix_rules: {variant: {test name prefix: [rule]}}
"""
self.variables = variables
with open(path) as f:
self._rules, self._prefix_rules = ReadStatusFile(f.read(), variables)
def get_outcomes(self, testname, variant=None):
"""Merges variant dependent and independent rules."""
outcomes = frozenset()
for key in set([variant or '', '']):
rules = self._rules.get(key, {})
prefix_rules = self._prefix_rules.get(key, {})
if testname in rules:
outcomes |= rules[testname]
for prefix in prefix_rules:
if testname.startswith(prefix):
outcomes |= prefix_rules[prefix]
return outcomes
def warn_unused_rules(self, tests, check_variant_rules=False):
"""Finds and prints unused rules in status file.
Rule X is unused when it doesn't apply to any tests, which can also mean
that all matching tests were skipped by another rule before evaluating X.
Args:
tests: list of pairs (testname, variant)
check_variant_rules: if set variant dependent rules are checked
"""
if check_variant_rules:
variants = list(ALL_VARIANTS)
else:
variants = ['']
used_rules = set()
for testname, variant in tests:
variant = variant or ''
if testname in self._rules.get(variant, {}):
used_rules.add((testname, variant))
if SKIP in self._rules[variant][testname]:
continue
for prefix in self._prefix_rules.get(variant, {}):
if testname.startswith(prefix):
used_rules.add((prefix, variant))
if SKIP in self._prefix_rules[variant][prefix]:
break
for variant in variants:
for rule, value in (
list(self._rules.get(variant, {}).items()) +
list(self._prefix_rules.get(variant, {}).items())):
if (rule, variant) not in used_rules:
if variant == '':
variant_desc = 'variant independent'
else:
variant_desc = 'variant: %s' % variant
print('Unused rule: %s -> %s (%s)' % (rule, value, variant_desc))
def _JoinsPassAndFail(outcomes1, outcomes2):
"""Indicates if we join PASS and FAIL from two different outcome sets and
the first doesn't already contain both.
"""
return (
PASS in outcomes1 and
not (FAIL in outcomes1 or FAIL_OK in outcomes1) and
(FAIL in outcomes2 or FAIL_OK in outcomes2)
)
VARIANT_EXPRESSION = object()
def _EvalExpression(exp, variables):
"""Evaluates expression and returns its result. In case of NameError caused by
undefined "variant" identifier returns VARIANT_EXPRESSION marker.
"""
try:
return eval(exp, variables)
except NameError as e:
identifier = re.match("name '(.*)' is not defined", str(e)).group(1)
# If it's not a variant expression, it points to a missing build flag.
assert identifier == "variant", INITIALIZATION_ERROR % identifier
return VARIANT_EXPRESSION
def _EvalVariantExpression(
condition, section, variables, variant, rules, prefix_rules):
variables_with_variant = dict(variables)
variables_with_variant["variant"] = variant
result = _EvalExpression(condition, variables_with_variant)
assert result != VARIANT_EXPRESSION
if result is True:
_ReadSection(
section,
variables_with_variant,
rules[variant],
prefix_rules[variant],
)
else:
assert result is False, "Make sure expressions evaluate to boolean values"
def _ParseOutcomeList(rule, outcomes, variables, target_dict):
"""Outcome list format: [condition, outcome, outcome, ...]"""
result = set([])
if type(outcomes) == str:
outcomes = [outcomes]
for item in outcomes:
if type(item) == str:
result.add(item)
elif type(item) == list:
condition = item[0]
exp = _EvalExpression(condition, variables)
assert exp != VARIANT_EXPRESSION, (
"Nested variant expressions are not supported")
if exp is False:
continue
# Ensure nobody uses an identifier by mistake, like "default",
# which would evaluate to true here otherwise.
assert exp is True, "Make sure expressions evaluate to boolean values"
for outcome in item[1:]:
assert type(outcome) == str
result.add(outcome)
else:
assert False
if len(result) == 0:
return
if rule in target_dict:
# A FAIL without PASS in one rule has always precedence over a single
# PASS (without FAIL) in another. Otherwise the default PASS expectation
# in a rule with a modifier (e.g. PASS, SLOW) would be joined to a FAIL
# from another rule (which intended to mark a test as FAIL and not as
# PASS and FAIL).
if _JoinsPassAndFail(target_dict[rule], result):
target_dict[rule] -= set([PASS])
if _JoinsPassAndFail(result, target_dict[rule]):
result -= set([PASS])
target_dict[rule] |= result
else:
target_dict[rule] = result
def ReadContent(content):
return eval(content, KEYWORDS)
def ReadStatusFile(content, variables):
"""Status file format
Status file := [section]
section = [CONDITION, section_rules]
section_rules := {path: outcomes}
outcomes := outcome | [outcome, ...]
outcome := SINGLE_OUTCOME | [CONDITION, SINGLE_OUTCOME, SINGLE_OUTCOME, ...]
"""
# Empty defaults for rules and prefix_rules. Variant-independent
# rules are mapped by "", others by the variant name.
rules = {variant: {} for variant in ALL_VARIANTS}
rules[""] = {}
prefix_rules = {variant: {} for variant in ALL_VARIANTS}
prefix_rules[""] = {}
# This method can be called with the same `variables` object multiple times.
# Ensure we only update `variables` (and check it for consistency) once.
if ALWAYS not in variables:
# Ensure we don't silently overwrite any build variables with our set of
# default keywords in VARIABLES.
for var in VARIABLES:
assert var not in variables, (
"build_config variable '%s' conflicts with VARIABLES" % var)
variables.update(VARIABLES)
for conditional_section in ReadContent(content):
assert type(conditional_section) == list
assert len(conditional_section) == 2
condition, section = conditional_section
exp = _EvalExpression(condition, variables)
# The expression is variant-independent and evaluates to False.
if exp is False:
continue
# The expression is variant-independent and evaluates to True.
if exp is True:
_ReadSection(
section,
variables,
rules[''],
prefix_rules[''],
)
continue
# The expression is variant-dependent (contains "variant" keyword)
if exp == VARIANT_EXPRESSION:
# If the expression contains one or more "variant" keywords, we evaluate
# it for all possible variants and create rules for those that apply.
for variant in ALL_VARIANTS:
_EvalVariantExpression(
condition, section, variables, variant, rules, prefix_rules)
continue
assert False, "Make sure expressions evaluate to boolean values"
return Freeze(rules), Freeze(prefix_rules)
def _ReadSection(section, variables, rules, prefix_rules):
assert type(section) == dict
for rule, outcome_list in list(section.items()):
assert type(rule) == str
if rule[-1] == '*':
_ParseOutcomeList(rule[:-1], outcome_list, variables, prefix_rules)
else:
_ParseOutcomeList(rule, outcome_list, variables, rules)
JS_TEST_PATHS = {
'debugger': [[]],
'inspector': [[]],
'intl': [[]],
'message': [[]],
'mjsunit': [[]],
'mozilla': [['data']],
'test262': [['data', 'test'], ['local-tests', 'test']],
'wasm-js': [['tests']],
'wasm-spec-tests': [['tests']],
'webkit': [[]],
}
DEFAULT_JS_TEST_EXTENSIONS = ['.js', '.mjs']
JS_TEST_EXTENSIONS = {
'wasm-js': ['.any.js'],
}
def PresubmitCheck(path):
with open(path) as f:
contents = ReadContent(f.read())
basename = os.path.basename(os.path.dirname(path))
root_prefix = basename + "/"
test_extensions = JS_TEST_EXTENSIONS.get(
basename, DEFAULT_JS_TEST_EXTENSIONS)
status = {"success": True}
def _assert(check, message): # Like "assert", but doesn't throw.
if not check:
print("%s: Error: %s" % (path, message))
status["success"] = False
try:
for section in contents:
_assert(type(section) == list, "Section must be a list")
_assert(len(section) == 2, "Section list must have exactly 2 entries")
section = section[1]
_assert(type(section) == dict,
"Second entry of section must be a dictionary")
for rule in section:
_assert(type(rule) == str, "Rule key must be a string")
_assert(not rule.startswith(root_prefix),
"Suite name prefix must not be used in rule keys")
_assert(not rule.endswith('.js'),
".js extension must not be used in rule keys.")
_assert('*' not in rule or (rule.count('*') == 1 and rule[-1] == '*'),
"Only the last character of a rule key can be a wildcard")
if basename in JS_TEST_PATHS and '*' not in rule:
def _any_exist(paths):
return any(
os.path.exists(
os.path.join(
os.path.dirname(path), *(paths + [rule + ext])))
for ext in test_extensions)
_assert(any(_any_exist(paths)
for paths in JS_TEST_PATHS[basename]),
"missing file for %s test %s" % (basename, rule))
return status["success"]
except Exception as e:
print(e)
return False

View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
# Copyright 2016 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import unittest
TOOLS_PATH = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(TOOLS_PATH)
from testrunner.local import statusfile
from testrunner.local.utils import Freeze
TEST_VARIABLES = {
'system': 'linux',
'mode': 'release',
}
TEST_STATUS_FILE = """
[
[ALWAYS, {
'foo/bar': [PASS, SKIP],
'baz/bar': [PASS, FAIL],
'foo/*': [PASS, SLOW],
}], # ALWAYS
['%s', {
'baz/bar': [PASS, SLOW],
'foo/*': [FAIL],
}],
]
"""
def make_variables():
variables = {}
variables.update(TEST_VARIABLES)
return variables
class UtilsTest(unittest.TestCase):
def test_freeze(self):
self.assertEqual(2, Freeze({1: [2]})[1][0])
self.assertEqual(set([3]), Freeze({1: [2], 2: set([3])})[2])
with self.assertRaises(Exception):
Freeze({1: [], 2: set([3])})[2] = 4
with self.assertRaises(Exception):
Freeze({1: [], 2: set([3])}).update({3: 4})
with self.assertRaises(Exception):
Freeze({1: [], 2: set([3])})[1].append(2)
with self.assertRaises(Exception):
Freeze({1: [], 2: set([3])})[2] |= set([3])
# Sanity check that we can do the same calls on a non-frozen object.
{1: [], 2: set([3])}[2] = 4
{1: [], 2: set([3])}.update({3: 4})
{1: [], 2: set([3])}[1].append(2)
{1: [], 2: set([3])}[2] |= set([3])
class StatusFileTest(unittest.TestCase):
def test_eval_expression(self):
variables = make_variables()
variables.update(statusfile.VARIABLES)
self.assertTrue(
statusfile._EvalExpression('system==linux and mode==release',
variables))
self.assertTrue(
statusfile._EvalExpression('system==linux or variant==default',
variables))
self.assertFalse(
statusfile._EvalExpression('system==linux and mode==debug', variables))
self.assertRaises(
AssertionError, lambda: statusfile._EvalExpression(
'system==linux and mode==foo', variables))
self.assertRaises(
SyntaxError, lambda: statusfile._EvalExpression(
'system==linux and mode=release', variables))
self.assertEqual(
statusfile.VARIANT_EXPRESSION,
statusfile._EvalExpression('system==linux and variant==default',
variables))
def test_read_statusfile_section_true(self):
rules, prefix_rules = statusfile.ReadStatusFile(
TEST_STATUS_FILE % 'system==linux', make_variables())
self.assertEqual(
{
'foo/bar': set(['PASS', 'SKIP']),
'baz/bar': set(['PASS', 'FAIL', 'SLOW']),
},
rules[''],
)
self.assertEqual(
{
'foo/': set(['SLOW', 'FAIL']),
},
prefix_rules[''],
)
self.assertEqual({}, rules['default'])
self.assertEqual({}, prefix_rules['default'])
def test_read_statusfile_section_false(self):
rules, prefix_rules = statusfile.ReadStatusFile(
TEST_STATUS_FILE % 'system==windows', make_variables())
self.assertEqual(
{
'foo/bar': set(['PASS', 'SKIP']),
'baz/bar': set(['PASS', 'FAIL']),
},
rules[''],
)
self.assertEqual(
{
'foo/': set(['PASS', 'SLOW']),
},
prefix_rules[''],
)
self.assertEqual({}, rules['default'])
self.assertEqual({}, prefix_rules['default'])
def test_read_statusfile_section_variant(self):
rules, prefix_rules = statusfile.ReadStatusFile(
TEST_STATUS_FILE % 'system==linux and variant==default',
make_variables(),
)
self.assertEqual(
{
'foo/bar': set(['PASS', 'SKIP']),
'baz/bar': set(['PASS', 'FAIL']),
},
rules[''],
)
self.assertEqual(
{
'foo/': set(['PASS', 'SLOW']),
},
prefix_rules[''],
)
self.assertEqual(
{
'baz/bar': set(['PASS', 'SLOW']),
},
rules['default'],
)
self.assertEqual(
{
'foo/': set(['FAIL']),
},
prefix_rules['default'],
)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,326 @@
# Copyright 2012 the V8 project authors. All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import importlib.machinery
import itertools
import os
from contextlib import contextmanager
from pathlib import Path
from . import statusfile
from .variants import ALL_VARIANTS, ALL_VARIANT_FLAGS
STANDARD_VARIANT = set(["default"])
class VariantsGenerator(object):
def __init__(self, variants):
self._all_variants = [v for v in variants if v in ALL_VARIANTS]
self._standard_variant = [v for v in variants if v in STANDARD_VARIANT]
def gen(self, test):
"""Generator producing (variant, flags, procid suffix) tuples."""
flags_set = self._get_flags_set(test)
for n, variant in enumerate(self._get_variants(test)):
yield (variant, flags_set[variant][0], n)
def _get_flags_set(self, test):
return ALL_VARIANT_FLAGS
def _get_variants(self, test):
if test.only_standard_variant:
return self._standard_variant
return self._all_variants
class TestCombiner(object):
def get_group_key(self, test):
"""To indicate what tests can be combined with each other we define a group
key for each test. Tests with the same group key can be combined. Test
without a group key (None) is not combinable with any other test.
"""
raise NotImplementedError()
def combine(self, name, tests):
"""Returns test combined from `tests`. Since we identify tests by their
suite and name, `name` parameter should be unique within one suite.
"""
return self._combined_test_class()(name, tests)
def _combined_test_class(self):
raise NotImplementedError()
class TestLoader(object):
"""Base class for loading TestSuite tests after applying test suite
transformations."""
def __init__(self, ctx, suite, test_class, test_config, test_root):
self.ctx = ctx
self.suite = suite
self.test_class = test_class
self.test_config = test_config
self.test_root = test_root
self.test_count_estimation = len(list(self._list_test_filenames()))
def _list_test_filenames(self):
"""Implemented by the subclassed TestLoaders to list filenames.
Filenames are expected to be sorted and are deterministic."""
raise NotImplementedError
def _should_filter_by_name(self, name):
return False
def _should_filter_by_test(self, test):
return False
def _filename_to_testname(self, filename):
"""Hook for subclasses to write their own filename transformation
logic before the test creation."""
return filename
def _create_test(self, path, suite, **kwargs):
"""Converts paths into test objects using the given options"""
return self.test_class(suite, path, path.as_posix(), **kwargs)
def list_tests(self):
"""Loads and returns the test objects for a TestSuite"""
# TODO: detect duplicate tests.
for filename in map(Path, self._list_test_filenames()):
if self._should_filter_by_name(filename):
continue
testname = self._filename_to_testname(filename)
case = self._create_test(testname, self.suite)
if self._should_filter_by_test(case):
continue
yield case
class GenericTestLoader(TestLoader):
"""Generic TestLoader implementing the logic for listing filenames"""
@property
def excluded_files(self):
return set()
@property
def excluded_dirs(self):
return set()
@property
def excluded_suffixes(self):
return set()
@property
def test_dirs(self):
return [self.test_root]
@property
def extensions(self):
return []
def __find_extension(self, filename):
for extension in self.extensions:
if filename.name.endswith(extension):
return extension
return False
def _should_filter_by_name(self, filename):
if not self.__find_extension(filename):
return True
for suffix in self.excluded_suffixes:
if filename.name.endswith(suffix):
return True
if filename.name in self.excluded_files:
return True
return False
def _filename_to_testname(self, filename):
extension = self.__find_extension(filename)
if not extension:
return filename
return filename.parent / filename.name[:-len(extension)]
def _to_relpath(self, abspath, test_root):
return abspath.relative_to(test_root)
def _list_test_filenames(self):
for test_dir in sorted(self.test_dirs):
test_root = self.test_root / test_dir
for dirname, dirs, files in os.walk(test_root, followlinks=True):
dirs.sort()
for dir in dirs:
if dir in self.excluded_dirs or dir.startswith('.'):
dirs.remove(dir)
files.sort()
for filename in files:
abspath = Path(dirname) / filename
yield self._to_relpath(abspath, test_root)
class JSTestLoader(GenericTestLoader):
@property
def extensions(self):
return [".js", ".mjs"]
class TestGenerator(object):
def __init__(
self, test_count_estimate, heavy_tests, slow_tests, remaining_tests):
self.test_count_estimate = test_count_estimate
self.heavy_tests = heavy_tests
self.slow_tests = slow_tests
self.remaining_tests = remaining_tests
self._rebuild_iterator()
def _rebuild_iterator(self):
self._iterator = itertools.chain(
self.heavy_tests, self.slow_tests, self.remaining_tests)
def __iter__(self):
return self
def __next__(self):
return self.next()
def next(self):
return next(self._iterator)
def merge(self, test_generator):
self.test_count_estimate += test_generator.test_count_estimate
self.heavy_tests = itertools.chain(
self.heavy_tests, test_generator.heavy_tests)
self.slow_tests = itertools.chain(
self.slow_tests, test_generator.slow_tests)
self.remaining_tests = itertools.chain(
self.remaining_tests, test_generator.remaining_tests)
self._rebuild_iterator()
@contextmanager
def _load_testsuite_module(name, root):
f = None
try:
yield importlib.machinery.SourceFileLoader(
name + "_testcfg", f"{root}/testcfg.py").load_module()
finally:
if f:
f.close()
class TestSuite(object):
@staticmethod
def Load(ctx, root, test_config):
name = root.name
with _load_testsuite_module(name, root) as module:
return module.TestSuite(ctx, name, root, test_config)
def __init__(self, ctx, name, root, test_config):
self.name = name # string
self.root = root # pathlib path
self.test_config = test_config
self.tests = None # list of TestCase objects
self.statusfile = None
self._test_loader = self._test_loader_class()(ctx, self, self._test_class(),
self.test_config, self.root)
@property
def framework_name(self):
return self.test_config.framework_name
def status_file(self):
return "%s/%s.status" % (self.root, self.name)
def statusfile_outcomes(self, test_name, variant):
return self.statusfile.get_outcomes(test_name, variant)
@property
def _test_loader_class(self):
raise NotImplementedError
def ListTests(self):
return self._test_loader.list_tests()
def __initialize_test_count_estimation(self):
# Retrieves a single test to initialize the test generator.
next(iter(self.ListTests()), None)
def __calculate_test_count(self):
self.__initialize_test_count_estimation()
return self._test_loader.test_count_estimation
def load_tests_from_disk(self, statusfile_variables):
self.statusfile = statusfile.StatusFile(
self.status_file(), statusfile_variables)
test_count = self.__calculate_test_count()
heavy_tests = (
test for test in self.ListTests()
if test.is_heavy)
slow_tests = (
test for test in self.ListTests()
if not test.is_heavy and test.is_slow)
remaining_tests = (
test for test in self.ListTests()
if not test.is_heavy and not test.is_slow)
return TestGenerator(test_count, heavy_tests, slow_tests, remaining_tests)
def get_variants_gen(self, variants):
return self._variants_gen_class()(variants)
def _variants_gen_class(self):
return VariantsGenerator
def test_combiner_available(self):
return bool(self._test_combiner_class())
def get_test_combiner(self):
cls = self._test_combiner_class()
if cls:
return cls()
return None
def _test_combiner_class(self):
"""Returns Combiner subclass. None if suite doesn't support combining
tests.
"""
return None
def _test_class(self):
raise NotImplementedError

View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
# Copyright 2016 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import unittest
from pathlib import Path
# Needed because the test runner contains relative imports.
TOOLS_PATH = Path(__file__).resolve().parents[2]
sys.path.append(str(TOOLS_PATH))
from testrunner.local.command import PosixCommand
from testrunner.local.context import DefaultOSContext
from testrunner.local.testsuite import TestSuite
from testrunner.test_config import TestConfig
class TestSuiteTest(unittest.TestCase):
def setUp(self):
test_dir = Path(__file__).resolve().parent
self.test_root = test_dir / "fake_testsuite"
self.test_config = TestConfig(
command_prefix=[],
extra_flags=[],
extra_d8_flags=[],
framework_name='standard_runner',
isolates=False,
log_process_stats=False,
mode_flags=[],
no_harness=False,
noi18n=False,
random_seed=0,
run_skipped=False,
shard_count=1,
shard_id=0,
shell_dir=Path('fake_testsuite/fake_d8'),
target_os='macos',
timeout=10,
verbose=False,
)
self.suite = TestSuite.Load(
DefaultOSContext(PosixCommand), self.test_root, self.test_config)
def testLoadingTestSuites(self):
self.assertEqual(self.suite.name, "fake_testsuite")
self.assertEqual(self.suite.test_config, self.test_config)
# Verify that the components of the TestSuite aren't loaded yet.
self.assertIsNone(self.suite.tests)
self.assertIsNone(self.suite.statusfile)
def testLoadingTestsFromDisk(self):
tests = self.suite.load_tests_from_disk(statusfile_variables={})
def is_generator(iterator):
return iterator == iter(iterator)
self.assertTrue(is_generator(tests))
self.assertEqual(tests.test_count_estimate, 3)
heavy_tests = list(tests.heavy_tests)
slow_tests = list(tests.slow_tests)
remaining_tests = list(tests.remaining_tests)
# Verify that the components of the TestSuite are loaded.
self.assertTrue(
len(heavy_tests) == len(slow_tests) == len(remaining_tests) == 1)
self.assertTrue(all(test.is_heavy for test in heavy_tests))
self.assertTrue(all(test.is_slow for test in slow_tests))
self.assertFalse(any(test.is_heavy for test in remaining_tests))
self.assertFalse(any(test.is_slow for test in remaining_tests))
self.assertIsNotNone(self.suite.statusfile)
def testMergingTestGenerators(self):
tests = self.suite.load_tests_from_disk(statusfile_variables={})
more_tests = self.suite.load_tests_from_disk(statusfile_variables={})
# Merge the test generators
tests.merge(more_tests)
self.assertEqual(tests.test_count_estimate, 6)
# Check the tests are sorted by memory and speed.
tests = list(tests)
heavy_tests = [test.is_heavy for test in tests]
slow_tests = [test.is_slow for test in tests]
self.assertEqual(heavy_tests, [True, True, False, False, False, False])
self.assertEqual(slow_tests, [False, False, True, True, False, False])
if __name__ == '__main__':
unittest.main()

225
deps/v8/tools/testrunner/local/utils.py vendored Normal file
View File

@ -0,0 +1,225 @@
# Copyright 2012 the V8 project authors. All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from os.path import isdir
from os.path import join
import os
import platform
import re
### Exit codes and their meaning.
# Normal execution.
EXIT_CODE_PASS = 0
# Execution with test failures.
EXIT_CODE_FAILURES = 1
# Execution with no tests executed.
EXIT_CODE_NO_TESTS = 2
# Execution aborted with SIGINT (Ctrl-C).
EXIT_CODE_INTERRUPTED = 3
# Execution aborted with SIGTERM.
EXIT_CODE_TERMINATED = 4
# Internal error.
EXIT_CODE_INTERNAL_ERROR = 5
def GetSuitePaths(test_root):
return [ f for f in os.listdir(test_root) if isdir(join(test_root, f)) ]
# Reads a file into an array of strings
def ReadLinesFrom(name):
lines = []
with open(name) as f:
for line in f:
if line.startswith('#'): continue
if '#' in line:
line = line[:line.find('#')]
line = line.strip()
if not line: continue
lines.append(line)
return lines
def GuessOS():
system = platform.system()
if system == 'Linux':
return 'linux'
elif system == 'Darwin':
return 'macos'
elif system.find('CYGWIN') >= 0:
return 'cygwin'
elif system == 'Windows' or system == 'Microsoft':
# On Windows Vista platform.system() can return 'Microsoft' with some
# versions of Python, see http://bugs.python.org/issue1082
return 'windows'
elif system == 'FreeBSD':
return 'freebsd'
elif system == 'OpenBSD':
return 'openbsd'
elif system == 'SunOS':
return 'solaris'
elif system == 'NetBSD':
return 'netbsd'
elif system in ['AIX', 'OS400']:
# OS400 runs an AIX emulator called PASE
return 'aix'
elif system == "OS/390":
return 'zos'
else:
return None
# Check if Vector Enhancement Facility 1 is available on the
# host S390 machine. This facility is required for supporting Simd on V8.
def IsS390SimdSupported():
if GuessOS() == 'zos':
from ctypes import CDLL
libname = os.environ.get('ZOSLIB_LIBPATH') + '/libzoslib.so'
clib = CDLL(libname)
return clib.__is_vef1_available()
import subprocess
cpuinfo = subprocess.check_output("cat /proc/cpuinfo", shell=True)
cpuinfo_list = cpuinfo.strip().decode("utf-8").splitlines()
facilities = "".join(x for x in cpuinfo_list if x.startswith("facilities"))
facilities_list = facilities.split(" ")
# Having bit 135 set indicates VEF1 is available.
return "135" in facilities_list
# Returns power processor version, taking compatibility mode into account.
# (Power9 running in Power8 compatibility mode returns 8)
# Only useful if arch is ppc64
def GuessPowerProcessorVersion():
import ctypes, ctypes.util
os = GuessOS()
if os == 'linux':
AT_PLATFORM = 15 # from linux/auxvec.h
_LIBC = ctypes.CDLL(ctypes.util.find_library('c'))
_LIBC.getauxval.argtypes = [ctypes.c_ulong]
_LIBC.getauxval.restype = ctypes.c_char_p
at_platform = _LIBC.getauxval(AT_PLATFORM).decode('utf-8').lower()
if at_platform.startswith('power6'):
return 6
elif at_platform.startswith('power7'):
return 7
elif at_platform.startswith('power8'):
return 8
elif at_platform.startswith('power9'):
return 9
elif at_platform.startswith('power10'):
return 10
else:
raise Exception('Unable to guess power processor version')
elif os == 'aix':
# covers aix and os400
RTLD_MEMBER = 0x00040000
_LIBC = ctypes.CDLL(ctypes.util.find_library('c'),
ctypes.DEFAULT_MODE | RTLD_MEMBER)
class _system_configuration(ctypes.Structure):
_fields_ = [
('architecture', ctypes.c_int),
('implementation', ctypes.c_int),
]
cfg = _system_configuration.in_dll(_LIBC, '_system_configuration')
# Values found in sys/systemcfg.h
if cfg.implementation == 0x4000:
return 6
elif cfg.implementation == 0x8000:
return 7
elif cfg.implementation == 0x10000:
return 8
elif cfg.implementation == 0x20000:
return 9
elif cfg.implementation == 0x40000:
return 10
else:
raise Exception('Unable to guess power processor version')
else:
raise Exception('Unable to guess power processor version')
def UseSimulator(arch):
machine = platform.machine()
return (machine and (arch == "arm" or arch == "arm64") and
not arch.startswith(machine))
# This will default to building the 32 bit VM even on machines that are
# capable of running the 64 bit VM.
def DefaultArch():
machine = platform.machine()
machine = machine.lower() # Windows 7 capitalizes 'AMD64'.
if machine.startswith('arm'):
return 'arm'
elif (not machine) or (not re.match('(x|i[3-6])86$', machine) is None):
return 'ia32'
elif machine == 'i86pc':
return 'ia32'
elif machine == 'x86_64':
return 'ia32'
elif machine == 'amd64':
return 'ia32'
elif machine == 's390x':
return 's390'
elif machine == 'ppc64':
return 'ppc'
else:
return None
def GuessWordsize():
if '64' in platform.machine():
return '64'
else:
return '32'
def IsWindows():
return GuessOS() == 'windows'
class FrozenDict(dict):
def __setitem__(self, *args, **kwargs):
raise Exception('Tried to mutate a frozen dict')
def update(self, *args, **kwargs):
raise Exception('Tried to mutate a frozen dict')
def Freeze(obj):
if isinstance(obj, dict):
return FrozenDict((k, Freeze(v)) for k, v in list(obj.items()))
elif isinstance(obj, set):
return frozenset(obj)
elif isinstance(obj, list):
return tuple(Freeze(item) for item in obj)
else:
# Make sure object is hashable.
hash(obj)
return obj

View File

@ -0,0 +1,342 @@
# Copyright 2016 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Use this to run several variants of the tests.
ALL_VARIANT_FLAGS = {
"assert_types": [["--assert-types"]],
"code_serializer": [["--cache=code"]],
"default": [[]],
"future": [["--future"]],
"gc_stats": [["--gc-stats=1"]],
# Alias of exhaustive variants, but triggering new test framework features.
"infra_staging": [[]],
"interpreted_regexp": [["--regexp-interpret-all"]],
"stress_regexp_jit": [["--regexp-tier-up-ticks=0"]],
"experimental_regexp": [["--default-to-experimental-regexp-engine"]],
"jitless": [["--jitless", "--wasm-jitless-if-available-for-testing"]],
# Jit-fuzzing variants pass --no-fail as most test conditions are violated.
# We only look for dchecks and crashes. As a result, negative tests like
# in the bugs/* directory are not supported.
"jit_fuzzing": [["--fuzzing", "--jit-fuzzing", "--no-fail"]],
"jit_fuzzing_maglev": [[
"--fuzzing",
"--jit-fuzzing",
"--optimize-on-next-call-optimizes-to-maglev",
"--no-fail",
]],
"sparkplug": [["--sparkplug"]],
"maglev": [["--maglev"]],
"maglev_future": [["--maglev", "--maglev-future"]],
"maglev_no_turbofan": [[
"--maglev", "--no-turbofan",
"--optimize-on-next-call-optimizes-to-maglev"
]],
"stress_maglev": [[
"--maglev", "--stress-maglev",
"--optimize-on-next-call-optimizes-to-maglev"
]],
"stress_maglev_future": [[
"--maglev", "--maglev-future", "--stress-maglev",
"--optimize-on-next-call-optimizes-to-maglev"
]],
"stress_maglev_no_turbofan": [[
"--maglev", "--no-turbofan", "--stress-maglev",
"--optimize-on-next-call-optimizes-to-maglev"
]],
"stress_maglev_non_eager_inlining": [[
"--maglev", "--stress-maglev", "--maglev-non-eager-inlining",
"--optimize-on-next-call-optimizes-to-maglev"
]],
"conservative_pinning": [[
"--scavenger-conservative-object-pinning",
"--stress-scavenger-conservative-object-pinning"
]],
"precise_pinning": [[
"--precise-object-pinning", "--scavenger-precise-object-pinning"
]],
# We test both the JS and Wasm Turboshaft pipelines under the same variant.
# For extended Wasm Turboshaft coverage, we add --no-liftoff to the options.
"turboshaft": [[
"--turboshaft",
"--no-wasm-generic-wrapper",
"--no-liftoff",
]],
# Turboshaft with Maglev as a frontend
"turbolev": [[
"--turboshaft",
"--turbolev",
]],
"concurrent_sparkplug": [["--concurrent-sparkplug", "--sparkplug"]],
"always_sparkplug": [["--always-sparkplug", "--sparkplug"]],
# This combines two orthogonal variants always_sparkplug and
# stress_regexp_jit to use bot resources more effectively.
"always_sparkplug_and_stress_regexp_jit": [[
"--always-sparkplug",
"--sparkplug",
"--regexp-tier-up-ticks=0",
]],
"minor_ms": [["--minor-ms"]],
"no_lfa": [["--no-lazy-feedback-allocation"]],
"no_memory_protection_keys": [["--no-memory-protection-keys"]],
# No optimization means disable all optimizations. OptimizeFunctionOnNextCall
# would not force optimization too. It turns into a Nop. Please see
# https://chromium-review.googlesource.com/c/452620/ for more discussion.
# For WebAssembly, we test "Liftoff-only" in the nooptimization variant and
# "TurboFan-only" in the stress variant. The WebAssembly configuration is
# independent of JS optimizations, so we can combine those configs. We
# disable lazy compilation to have one test variant that tests eager
# compilation. "Liftoff-only" and eager compilation is not a problem,
# because test functions do typically not get optimized to TurboFan anyways.
"nooptimization": [[
"--disable-optimizing-compilers", "--no-wasm-lazy-compilation"
]],
"rehash_snapshot": [["--rehash-snapshot"]],
"slow_path": [["--force-slow-path"]],
"stress": [[
"--no-liftoff",
"--stress-lazy-source-positions",
"--no-wasm-generic-wrapper",
"--no-wasm-lazy-compilation",
]],
"stress_concurrent_allocation": [["--stress-concurrent-allocation"]],
"stress_concurrent_inlining": [["--stress-concurrent-inlining"]],
"stress_js_bg_compile_wasm_code_gc": [[
"--stress-background-compile", "--stress-wasm-code-gc"
]],
"stress_maglev_tests_with_turbofan": [[
"--turbofan", "--optimize-maglev-optimizes-to-turbofan"
]],
"stress_wasm_stack_switching": [["--stress-wasm-stack-switching"]],
"stress_incremental_marking": [["--stress-incremental-marking"]],
"stress_snapshot": [["--stress-snapshot"]],
# Trigger stress sampling allocation profiler with sample interval = 2^14
"stress_sampling": [["--stress-sampling-allocation-profiler=16384"]],
"no_wasm_traps": [["--no-wasm-trap-handler"]],
"instruction_scheduling": [["--turbo-instruction-scheduling"]],
"stress_instruction_scheduling": [["--turbo-stress-instruction-scheduling"]
],
# Google3 variants.
"google3_icu": [[]],
"google3_noicu": [[]],
}
# Note these are specifically for the case when Turbofan is either fully
# disabled (i.e. not part of the binary), or when all codegen is disallowed (in
# jitless mode).
kIncompatibleFlagsForNoTurbofan = [
"--turbofan", "--always-turbofan", "--liftoff", "--validate-asm",
"--maglev", "--stress-concurrent-inlining"
]
# Flags that lead to a contradiction with the flags provided by the respective
# variant. This depends on the flags specified in ALL_VARIANT_FLAGS and on the
# implications defined in flag-definitions.h.
INCOMPATIBLE_FLAGS_PER_VARIANT = {
"jitless":
kIncompatibleFlagsForNoTurbofan + [
"--track-field-types",
"--sparkplug",
"--concurrent-sparkplug",
"--always-sparkplug",
"--regexp-tier-up",
"--no-regexp-interpret-all",
"--interpreted-frames-native-stack",
"--script-context-mutable-heap-number",
],
"nooptimization": [
"--turbofan",
"--always-turbofan",
"--turboshaft",
"--turboshaft-wasm-in-js-inlining",
"--maglev",
"--no-liftoff",
"--wasm-tier-up",
"--wasm-dynamic-tiering",
"--validate-asm",
"--track-field-types",
"--stress-concurrent-inlining",
"--script-context-mutable-heap-number",
"--experimental-wasm-shared",
],
"slow_path": ["--no-force-slow-path"],
"stress_concurrent_allocation": [
"--single-threaded", "--single-threaded-gc", "--predictable"
],
"stress_concurrent_inlining": [
"--single-threaded", "--predictable", "--lazy-feedback-allocation",
"--assert-types", "--turboshaft-assert-types",
"--no-concurrent-recompilation", "--no-turbofan", "--jitless"
],
# The fast API tests initialize an embedder object that never needs to be
# serialized to the snapshot, so we don't have a
# SerializeInternalFieldsCallback for it, so they are incompatible with
# stress_snapshot.
"stress_snapshot": ["--expose-fast-api"],
"stress": [
# 'stress' disables Liftoff, which conflicts with flags that require
# Liftoff support.
"--liftoff-only",
"--wasm-dynamic-tiering"
],
"sparkplug": ["--jitless", "--no-sparkplug"],
"turboshaft": [
# 'turboshaft' disables Liftoff, which conflicts with flags that require
# Liftoff support.
"--liftoff-only",
"--wasm-dynamic-tiering"
],
"concurrent_sparkplug": ["--jitless"],
"maglev": ["--jitless", "--no-maglev"],
"maglev_future": ["--jitless", "--no-maglev", "--no-maglev-future"],
"maglev_no_turbofan": [
"--jitless",
"--no-maglev",
"--turbofan",
"--always-turbofan",
"--stress-concurrent-inlining",
],
"stress_maglev": ["--jitless"],
"stress_maglev_non_eager_inlining": ["--jitless"],
"stress_maglev_future": ["--jitless", "--no-maglev", "--no-maglev-future"],
"stress_maglev_no_turbofan": [
"--jitless",
"--no-maglev",
"--turbofan",
"--always-turbofan",
"--stress-concurrent-inlining",
],
"stress_maglev_tests_with_turbofan": ["--jitless"],
"always_sparkplug": ["--jitless", "--no-sparkplug"],
"always_sparkplug_and_stress_regexp_jit": ["--jitless", "--no-sparkplug"],
"code_serializer": [
"--cache=after-execute", "--cache=full-code-cache", "--cache=none"
],
"interpreted_regexp": ["--regexp-tier-up"],
"stress_regexp_jit": ["--regexp-interpret-all"],
"experimental_regexp": ["--no-enable-experimental-regexp-engine"],
"assert_types": [
"--concurrent-recompilation", "--stress_concurrent_inlining",
"--no-assert-types"
],
"stress_wasm_stack_switching": ["--no-stress-wasm-stack-switching"],
"--turboshaft-assert-types": [
"--concurrent-recompilation", "--stress_concurrent_inlining",
"--no-turboshaft-assert-types"
],
}
# Flags that lead to a contradiction under certain build variables.
# This corresponds to the build variables usable in status files as generated
# in _get_statusfile_variables in base_runner.py.
# The conflicts might be directly contradictory flags or be caused by the
# implications defined in flag-definitions.h.
# The keys of the following map support negation through '!', e.g. rule
#
# "!code_comments": [...]
#
# applies when the code_comments build variable is NOT set.
INCOMPATIBLE_FLAGS_PER_BUILD_VARIABLE = {
"!code_comments": ["--code-comments"],
"!DEBUG_defined": [
"--check_handle_count",
"--code_stats",
"--dump_wasm_module",
"--enable_testing_opcode_in_wasm",
"--gc_verbose",
"--no-wasm-opt",
"--print_ast",
"--print_break_location",
"--print_global_handles",
"--print_handles",
"--print_scopes",
"--regexp_possessive_quantifier",
"--trace_backing_store",
"--trace_contexts",
"--trace_isolates",
"--trace_lazy",
"--trace_liftoff",
"--trace_module_status",
"--trace_normalization",
"--trace_turbo_escape",
"--trace_wasm_compiler",
"--trace_wasm_decoder",
"--trace_wasm_instances",
"--trace_wasm_lazy_compilation",
"--trace_wasm_native_heap",
"--trace_wasm_serialization",
"--trace_wasm_stack_switching",
"--trace_wasm_streaming",
],
"!verify_heap": ["--verify-heap"],
"!debug_code": ["--debug-code"],
"!disassembler": [
"--print_all_code", "--print_code", "--print_opt_code",
"--print_code_verbose", "--print_builtin_code", "--print_regexp_code"
],
"!slow_dchecks": ["--enable-slow-asserts"],
"!gdbjit": ["--gdbjit", "--gdbjit_full", "--gdbjit_dump"],
"!has_maglev": ["--maglev"],
"!has_turbofan": kIncompatibleFlagsForNoTurbofan,
"has_jitless": INCOMPATIBLE_FLAGS_PER_VARIANT["jitless"],
"lite_mode": INCOMPATIBLE_FLAGS_PER_VARIANT["jitless"],
"verify_predictable": [
"--parallel-compile-tasks-for-eager-toplevel",
"--parallel-compile-tasks-for-lazy", "--concurrent-recompilation",
"--stress-concurrent-allocation", "--stress-concurrent-inlining"
],
"dict_property_const_tracking": ["--stress-concurrent-inlining"],
}
# Flags that lead to a contradiction when a certain extra-flag is present.
# Such extra-flags are defined for example in infra/testing/builders.pyl or in
# standard_runner.py.
# The conflicts might be directly contradictory flags or be caused by the
# implications defined in flag-definitions.h.
INCOMPATIBLE_FLAGS_PER_EXTRA_FLAG = {
"--concurrent-recompilation": [
"--predictable", "--assert-types", "--turboshaft-assert-types",
"--single-threaded"
],
"--parallel-compile-tasks-for-eager-toplevel": ["--predictable"],
"--parallel-compile-tasks-for-lazy": ["--predictable"],
"--gc-interval=*": ["--gc-interval=*"],
"--stress_concurrent_allocation":
INCOMPATIBLE_FLAGS_PER_VARIANT["stress_concurrent_allocation"],
"--stress-concurrent-inlining":
INCOMPATIBLE_FLAGS_PER_VARIANT["stress_concurrent_inlining"],
"--turboshaft-assert-types": [
"--concurrent-recompilation", "--stress-concurrent-inlining"
],
}
SLOW_VARIANTS = set([
'stress',
'stress_snapshot',
'nooptimization',
])
FAST_VARIANTS = set([
'default'
])
def _variant_order_key(v):
if v in SLOW_VARIANTS:
return 0
if v in FAST_VARIANTS:
return 100
return 50
ALL_VARIANTS = sorted(ALL_VARIANT_FLAGS.keys(),
key=_variant_order_key)
# For internal integrity checking.
REQUIRED_BUILD_VARIABLES = [
var.lstrip('!') for var in INCOMPATIBLE_FLAGS_PER_BUILD_VARIABLE.keys()
]
# Check {SLOW,FAST}_VARIANTS entries
for variants in [SLOW_VARIANTS, FAST_VARIANTS]:
for v in variants:
assert v in ALL_VARIANT_FLAGS

View File

@ -0,0 +1,103 @@
# Copyright 2012 the V8 project authors. All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import sys
import time
from . import statusfile
REPORT_TEMPLATE = (
"""Total: %(total)i tests
* %(skipped)4d tests will be skipped
* %(nocrash)4d tests are expected to be flaky but not crash
* %(pass)4d tests are expected to pass
* %(fail_ok)4d tests are expected to fail that we won't fix
* %(fail)4d tests are expected to fail that we should fix
* %(crash)4d tests are expected to crash
""")
# TODO(majeski): Turn it into an observer.
def PrintReport(tests):
total = len(tests)
skipped = nocrash = passes = fail_ok = fail = crash = 0
for t in tests:
if t.do_skip:
skipped += 1
elif t.is_pass_or_fail:
nocrash += 1
elif t.is_fail_ok:
fail_ok += 1
elif t.expected_outcomes == [statusfile.PASS]:
passes += 1
elif t.expected_outcomes == [statusfile.FAIL]:
fail += 1
elif t.expected_outcomes == [statusfile.CRASH]:
crash += 1
else:
assert False # Unreachable # TODO: check this in outcomes parsing phase.
print(REPORT_TEMPLATE % {
"total": total,
"skipped": skipped,
"nocrash": nocrash,
"pass": passes,
"fail_ok": fail_ok,
"fail": fail,
"crash": crash,
})
def PrintTestSource(tests):
for test in tests:
print("--- begin source: %s ---" % test)
if test.is_source_available():
print(test.get_source())
else:
print('(no source available)')
print("--- end source: %s ---" % test)
def FormatTime(d):
millis = round(d * 1000) % 1000
return time.strftime("%M:%S.", time.gmtime(d)) + ("%03i" % millis)
def PrintTestDurations(suites, outputs, overall_time):
# Write the times to stderr to make it easy to separate from the
# test output.
print()
sys.stderr.write("--- Total time: %s ---\n" % FormatTime(overall_time))
timed_tests = [(t, outputs[t].duration) for s in suites for t in s.tests
if t in outputs]
timed_tests.sort(key=lambda test_duration: test_duration[1], reverse=True)
index = 1
for test, duration in timed_tests[:20]:
t = FormatTime(duration)
sys.stderr.write("%4i (%s) %s\n" % (index, t, test))
index += 1