aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mesonbuild/mtest.py624
-rwxr-xr-xmesontest.py616
-rwxr-xr-xrun_project_tests.py6
3 files changed, 633 insertions, 613 deletions
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
new file mode 100644
index 0000000..2520ae8
--- /dev/null
+++ b/mesonbuild/mtest.py
@@ -0,0 +1,624 @@
+# Copyright 2016-2017 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A tool to run tests in many different ways.
+
+import shlex
+import subprocess, sys, os, argparse
+import pickle
+from mesonbuild import build
+from mesonbuild import environment
+from mesonbuild.dependencies import ExternalProgram
+from mesonbuild import mesonlib
+from mesonbuild import mlog
+
+import time, datetime, multiprocessing, json
+import concurrent.futures as conc
+import platform
+import signal
+import random
+
+# GNU autotools interprets a return code of 77 from tests it executes to
+# mean that the test should be skipped.
+GNU_SKIP_RETURNCODE = 77
+
+def is_windows():
+ platname = platform.system().lower()
+ return platname == 'windows' or 'mingw' in platname
+
+def is_cygwin():
+ platname = platform.system().lower()
+ return 'cygwin' in platname
+
+def determine_worker_count():
+ varname = 'MESON_TESTTHREADS'
+ if varname in os.environ:
+ try:
+ num_workers = int(os.environ[varname])
+ except ValueError:
+ print('Invalid value in %s, using 1 thread.' % varname)
+ num_workers = 1
+ else:
+ try:
+ # Fails in some weird environments such as Debian
+ # reproducible build.
+ num_workers = multiprocessing.cpu_count()
+ except Exception:
+ num_workers = 1
+ return num_workers
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--repeat', default=1, dest='repeat', type=int,
+ help='Number of times to run the tests.')
+parser.add_argument('--no-rebuild', default=False, action='store_true',
+ help='Do not rebuild before running tests.')
+parser.add_argument('--gdb', default=False, dest='gdb', action='store_true',
+ help='Run test under gdb.')
+parser.add_argument('--list', default=False, dest='list', action='store_true',
+ help='List available tests.')
+parser.add_argument('--wrapper', default=None, dest='wrapper', type=shlex.split,
+ help='wrapper to run tests with (e.g. Valgrind)')
+parser.add_argument('-C', default='.', dest='wd',
+ help='directory to cd into before running')
+parser.add_argument('--suite', default=[], dest='include_suites', action='append', metavar='SUITE',
+ help='Only run tests belonging to the given suite.')
+parser.add_argument('--no-suite', default=[], dest='exclude_suites', action='append', metavar='SUITE',
+ help='Do not run tests belonging to the given suite.')
+parser.add_argument('--no-stdsplit', default=True, dest='split', action='store_false',
+ help='Do not split stderr and stdout in test logs.')
+parser.add_argument('--print-errorlogs', default=False, action='store_true',
+ help="Whether to print failing tests' logs.")
+parser.add_argument('--benchmark', default=False, action='store_true',
+ help="Run benchmarks instead of tests.")
+parser.add_argument('--logbase', default='testlog',
+ help="Base name for log file.")
+parser.add_argument('--num-processes', default=determine_worker_count(), type=int,
+ help='How many parallel processes to use.')
+parser.add_argument('-v', '--verbose', default=False, action='store_true',
+ help='Do not redirect stdout and stderr')
+parser.add_argument('-q', '--quiet', default=False, action='store_true',
+ help='Produce less output to the terminal.')
+parser.add_argument('-t', '--timeout-multiplier', type=float, default=None,
+ help='Define a multiplier for test timeout, for example '
+ ' when running tests in particular conditions they might take'
+ ' more time to execute.')
+parser.add_argument('--setup', default=None, dest='setup',
+ help='Which test setup to use.')
+parser.add_argument('--test-args', default=[], type=shlex.split,
+ help='Arguments to pass to the specified test(s) or all tests')
+parser.add_argument('args', nargs='*',
+ help='Optional list of tests to run')
+
+
+class TestException(mesonlib.MesonException):
+ pass
+
+
+class TestRun:
+ def __init__(self, res, returncode, should_fail, duration, stdo, stde, cmd,
+ env):
+ self.res = res
+ self.returncode = returncode
+ self.duration = duration
+ self.stdo = stdo
+ self.stde = stde
+ self.cmd = cmd
+ self.env = env
+ self.should_fail = should_fail
+
+ def get_log(self):
+ res = '--- command ---\n'
+ if self.cmd is None:
+ res += 'NONE\n'
+ else:
+ res += "%s%s\n" % (''.join(["%s='%s' " % (k, v) for k, v in self.env.items()]), ' ' .join(self.cmd))
+ if self.stdo:
+ res += '--- stdout ---\n'
+ res += self.stdo
+ if self.stde:
+ if res[-1:] != '\n':
+ res += '\n'
+ res += '--- stderr ---\n'
+ res += self.stde
+ if res[-1:] != '\n':
+ res += '\n'
+ res += '-------\n\n'
+ return res
+
+def decode(stream):
+ if stream is None:
+ return ''
+ try:
+ return stream.decode('utf-8')
+ except UnicodeDecodeError:
+ return stream.decode('iso-8859-1', errors='ignore')
+
+def write_json_log(jsonlogfile, test_name, result):
+ jresult = {'name': test_name,
+ 'stdout': result.stdo,
+ 'result': result.res,
+ 'duration': result.duration,
+ 'returncode': result.returncode,
+ 'command': result.cmd}
+ if isinstance(result.env, dict):
+ jresult['env'] = result.env
+ else:
+ jresult['env'] = result.env.get_env(os.environ)
+ if result.stde:
+ jresult['stderr'] = result.stde
+ jsonlogfile.write(json.dumps(jresult) + '\n')
+
+def run_with_mono(fname):
+ if fname.endswith('.exe') and not (is_windows() or is_cygwin()):
+ return True
+ return False
+
+class TestHarness:
+ def __init__(self, options):
+ self.options = options
+ self.collected_logs = []
+ self.fail_count = 0
+ self.success_count = 0
+ self.skip_count = 0
+ self.timeout_count = 0
+ self.is_run = False
+ self.tests = None
+ self.suites = None
+ self.logfilename = None
+ self.logfile = None
+ self.jsonlogfile = None
+ if self.options.benchmark:
+ datafile = os.path.join(options.wd, 'meson-private', 'meson_benchmark_setup.dat')
+ else:
+ datafile = os.path.join(options.wd, 'meson-private', 'meson_test_setup.dat')
+ if not os.path.isfile(datafile):
+ raise TestException('Directory %s does not seem to be a Meson build directory.' % options.wd)
+ self.load_datafile(datafile)
+
+ def __del__(self):
+ if self.logfile:
+ self.logfile.close()
+ if self.jsonlogfile:
+ self.jsonlogfile.close()
+
+ def run_single_test(self, wrap, test):
+ if test.fname[0].endswith('.jar'):
+ cmd = ['java', '-jar'] + test.fname
+ elif not test.is_cross and run_with_mono(test.fname[0]):
+ cmd = ['mono'] + test.fname
+ else:
+ if test.is_cross:
+ if test.exe_runner is None:
+ # Can not run test on cross compiled executable
+ # because there is no execute wrapper.
+ cmd = None
+ else:
+ cmd = [test.exe_runner] + test.fname
+ else:
+ cmd = test.fname
+
+ if cmd is None:
+ res = 'SKIP'
+ duration = 0.0
+ stdo = 'Not run because can not execute cross compiled binaries.'
+ stde = None
+ returncode = GNU_SKIP_RETURNCODE
+ else:
+ cmd = wrap + cmd + test.cmd_args + self.options.test_args
+ starttime = time.time()
+ child_env = os.environ.copy()
+ child_env.update(self.options.global_env.get_env(child_env))
+ if isinstance(test.env, build.EnvironmentVariables):
+ test.env = test.env.get_env(child_env)
+
+ child_env.update(test.env)
+ if len(test.extra_paths) > 0:
+ child_env['PATH'] += os.pathsep.join([''] + test.extra_paths)
+
+ # If MALLOC_PERTURB_ is not set, or if it is set to an empty value,
+ # (i.e., the test or the environment don't explicitly set it), set
+ # it ourselves. We do this unconditionally because it is extremely
+ # useful to have in tests.
+ # Setting MALLOC_PERTURB_="0" will completely disable this feature.
+ if 'MALLOC_PERTURB_' not in child_env or not child_env['MALLOC_PERTURB_']:
+ child_env['MALLOC_PERTURB_'] = str(random.randint(1, 255))
+
+ setsid = None
+ stdout = None
+ stderr = None
+ if not self.options.verbose:
+ stdout = subprocess.PIPE
+ stderr = subprocess.PIPE if self.options and self.options.split else subprocess.STDOUT
+
+ if not is_windows():
+ setsid = os.setsid
+
+ p = subprocess.Popen(cmd,
+ stdout=stdout,
+ stderr=stderr,
+ env=child_env,
+ cwd=test.workdir,
+ preexec_fn=setsid)
+ timed_out = False
+ if test.timeout is None:
+ timeout = None
+ else:
+ timeout = test.timeout * self.options.timeout_multiplier
+ try:
+ (stdo, stde) = p.communicate(timeout=timeout)
+ except subprocess.TimeoutExpired:
+ if self.options.verbose:
+ print("%s time out (After %d seconds)" % (test.name, timeout))
+ timed_out = True
+ # Python does not provide multiplatform support for
+ # killing a process and all its children so we need
+ # to roll our own.
+ if is_windows():
+ subprocess.call(['taskkill', '/F', '/T', '/PID', str(p.pid)])
+ else:
+ os.killpg(os.getpgid(p.pid), signal.SIGKILL)
+ (stdo, stde) = p.communicate()
+ endtime = time.time()
+ duration = endtime - starttime
+ stdo = decode(stdo)
+ if stde:
+ stde = decode(stde)
+ if timed_out:
+ res = 'TIMEOUT'
+ self.timeout_count += 1
+ self.fail_count += 1
+ elif p.returncode == GNU_SKIP_RETURNCODE:
+ res = 'SKIP'
+ self.skip_count += 1
+ elif test.should_fail == bool(p.returncode):
+ res = 'OK'
+ self.success_count += 1
+ else:
+ res = 'FAIL'
+ self.fail_count += 1
+ returncode = p.returncode
+ result = TestRun(res, returncode, test.should_fail, duration, stdo, stde, cmd, test.env)
+
+ return result
+
+ def print_stats(self, numlen, tests, name, result, i):
+ startpad = ' ' * (numlen - len('%d' % (i + 1)))
+ num = '%s%d/%d' % (startpad, i + 1, len(tests))
+ padding1 = ' ' * (38 - len(name))
+ padding2 = ' ' * (8 - len(result.res))
+ result_str = '%s %s %s%s%s%5.2f s' % \
+ (num, name, padding1, result.res, padding2, result.duration)
+ if not self.options.quiet or result.res != 'OK':
+ if result.res != 'OK' and mlog.colorize_console:
+ if result.res == 'FAIL' or result.res == 'TIMEOUT':
+ decorator = mlog.red
+ elif result.res == 'SKIP':
+ decorator = mlog.yellow
+ else:
+ sys.exit('Unreachable code was ... well ... reached.')
+ print(decorator(result_str).get_text(True))
+ else:
+ print(result_str)
+ result_str += "\n\n" + result.get_log()
+ if (result.returncode != GNU_SKIP_RETURNCODE) \
+ and (result.returncode != 0) != result.should_fail:
+ if self.options.print_errorlogs:
+ self.collected_logs.append(result_str)
+ if self.logfile:
+ self.logfile.write(result_str)
+ if self.jsonlogfile:
+ write_json_log(self.jsonlogfile, name, result)
+
+ def print_summary(self):
+ msg = '''
+OK: %4d
+FAIL: %4d
+SKIP: %4d
+TIMEOUT: %4d
+''' % (self.success_count, self.fail_count, self.skip_count, self.timeout_count)
+ print(msg)
+ if self.logfile:
+ self.logfile.write(msg)
+
+ def print_collected_logs(self):
+ if len(self.collected_logs) > 0:
+ if len(self.collected_logs) > 10:
+ print('\nThe output from 10 first failed tests:\n')
+ else:
+ print('\nThe output from the failed tests:\n')
+ for log in self.collected_logs[:10]:
+ lines = log.splitlines()
+ if len(lines) > 104:
+ print('\n'.join(lines[0:4]))
+ print('--- Listing only the last 100 lines from a long log. ---')
+ lines = lines[-100:]
+ for line in lines:
+ print(line)
+
+ def doit(self):
+ if self.is_run:
+ raise RuntimeError('Test harness object can only be used once.')
+ if not os.path.isfile(self.datafile):
+ print('Test data file. Probably this means that you did not run this in the build directory.')
+ return 1
+ self.is_run = True
+ tests = self.get_tests()
+ if not tests:
+ return 0
+ self.run_tests(tests)
+ return self.fail_count
+
+ @staticmethod
+ def split_suite_string(suite):
+ if ':' in suite:
+ return suite.split(':', 1)
+ else:
+ return suite, ""
+
+ @staticmethod
+ def test_in_suites(test, suites):
+ for suite in suites:
+ (prj_match, st_match) = TestHarness.split_suite_string(suite)
+ for prjst in test.suite:
+ (prj, st) = TestHarness.split_suite_string(prjst)
+ if prj_match and prj != prj_match:
+ continue
+ if st_match and st != st_match:
+ continue
+ return True
+ return False
+
+ def test_suitable(self, test):
+ return (not self.options.include_suites or TestHarness.test_in_suites(test, self.options.include_suites)) \
+ and not TestHarness.test_in_suites(test, self.options.exclude_suites)
+
+ def load_suites(self):
+ ss = set()
+ for t in self.tests:
+ for s in t.suite:
+ ss.add(s)
+ self.suites = list(ss)
+
+ def load_tests(self):
+ with open(self.datafile, 'rb') as f:
+ self.tests = pickle.load(f)
+
+ def load_datafile(self, datafile):
+ self.datafile = datafile
+ self.load_tests()
+ self.load_suites()
+
+ def get_tests(self):
+ if not self.tests:
+ print('No tests defined.')
+ return []
+
+ if len(self.options.include_suites) or len(self.options.exclude_suites):
+ tests = []
+ for tst in self.tests:
+ if self.test_suitable(tst):
+ tests.append(tst)
+ else:
+ tests = self.tests
+
+ if self.options.args:
+ tests = [t for t in tests if t.name in self.options.args]
+
+ if not tests:
+ print('No suitable tests defined.')
+ return []
+
+ for test in tests:
+ test.rebuilt = False
+
+ return tests
+
+ def open_log_files(self):
+ if not self.options.logbase or self.options.verbose:
+ return None, None, None, None
+
+ namebase = None
+ logfile_base = os.path.join(self.options.wd, 'meson-logs', self.options.logbase)
+
+ if self.options.wrapper:
+ namebase = os.path.split(self.get_wrapper()[0])[1]
+ elif self.options.setup:
+ namebase = self.options.setup
+
+ if namebase:
+ logfile_base += '-' + namebase.replace(' ', '_')
+ self.logfilename = logfile_base + '.txt'
+ self.jsonlogfilename = logfile_base + '.json'
+
+ self.jsonlogfile = open(self.jsonlogfilename, 'w')
+ self.logfile = open(self.logfilename, 'w')
+
+ self.logfile.write('Log of Meson test suite run on %s\n\n'
+ % datetime.datetime.now().isoformat())
+
+ def get_wrapper(self):
+ wrap = []
+ if self.options.gdb:
+ wrap = ['gdb', '--quiet', '--nh']
+ if self.options.repeat > 1:
+ wrap += ['-ex', 'run', '-ex', 'quit']
+ # Signal the end of arguments to gdb
+ wrap += ['--args']
+ if self.options.wrapper:
+ wrap += self.options.wrapper
+ assert(isinstance(wrap, list))
+ return wrap
+
+ def get_pretty_suite(self, test):
+ if len(self.suites) > 1:
+ rv = TestHarness.split_suite_string(test.suite[0])[0]
+ s = "+".join(TestHarness.split_suite_string(s)[1] for s in test.suite)
+ if len(s):
+ rv += ":"
+ return rv + s + " / " + test.name
+ else:
+ return test.name
+
+ def run_tests(self, tests):
+ executor = None
+ futures = []
+ numlen = len('%d' % len(tests))
+ self.open_log_files()
+ wrap = self.get_wrapper()
+
+ for _ in range(self.options.repeat):
+ for i, test in enumerate(tests):
+ visible_name = self.get_pretty_suite(test)
+
+ if self.options.gdb:
+ test.timeout = None
+
+ if not test.is_parallel or self.options.gdb:
+ self.drain_futures(futures)
+ futures = []
+ res = self.run_single_test(wrap, test)
+ self.print_stats(numlen, tests, visible_name, res, i)
+ else:
+ if not executor:
+ executor = conc.ThreadPoolExecutor(max_workers=self.options.num_processes)
+ f = executor.submit(self.run_single_test, wrap, test)
+ futures.append((f, numlen, tests, visible_name, i))
+ if self.options.repeat > 1 and self.fail_count:
+ break
+ if self.options.repeat > 1 and self.fail_count:
+ break
+
+ self.drain_futures(futures)
+ self.print_summary()
+ self.print_collected_logs()
+
+ if self.logfilename:
+ print('Full log written to %s' % self.logfilename)
+
+ def drain_futures(self, futures):
+ for i in futures:
+ (result, numlen, tests, name, i) = i
+ if self.options.repeat > 1 and self.fail_count:
+ result.cancel()
+ if self.options.verbose:
+ result.result()
+ self.print_stats(numlen, tests, name, result.result(), i)
+
+ def run_special(self):
+ 'Tests run by the user, usually something like "under gdb 1000 times".'
+ if self.is_run:
+ raise RuntimeError('Can not use run_special after a full run.')
+ tests = self.get_tests()
+ if not tests:
+ return 0
+ self.run_tests(tests)
+ return self.fail_count
+
+
+def list_tests(th):
+ tests = th.get_tests()
+ for t in tests:
+ print(th.get_pretty_suite(t))
+
+def merge_suite_options(options):
+ buildfile = os.path.join(options.wd, 'meson-private/build.dat')
+ with open(buildfile, 'rb') as f:
+ build = pickle.load(f)
+ setups = build.test_setups
+ if options.setup not in setups:
+ sys.exit('Unknown test setup: %s' % options.setup)
+ current = setups[options.setup]
+ if not options.gdb:
+ options.gdb = current.gdb
+ if options.timeout_multiplier is None:
+ options.timeout_multiplier = current.timeout_multiplier
+# if options.env is None:
+# options.env = current.env # FIXME, should probably merge options here.
+ if options.wrapper is not None and current.exe_wrapper is not None:
+ sys.exit('Conflict: both test setup and command line specify an exe wrapper.')
+ if options.wrapper is None:
+ options.wrapper = current.exe_wrapper
+ return current.env
+
+def rebuild_all(wd):
+ if not os.path.isfile(os.path.join(wd, 'build.ninja')):
+ print("Only ninja backend is supported to rebuild tests before running them.")
+ return True
+
+ ninja = environment.detect_ninja()
+ if not ninja:
+ print("Can't find ninja, can't rebuild test.")
+ return False
+
+ p = subprocess.Popen([ninja, '-C', wd])
+ p.communicate()
+
+ if p.returncode != 0:
+ print("Could not rebuild")
+ return False
+
+ return True
+
+def run(args):
+ options = parser.parse_args(args)
+
+ if options.benchmark:
+ options.num_processes = 1
+
+ if options.setup is not None:
+ global_env = merge_suite_options(options)
+ else:
+ global_env = build.EnvironmentVariables()
+ if options.timeout_multiplier is None:
+ options.timeout_multiplier = 1
+
+ setattr(options, 'global_env', global_env)
+
+ if options.verbose and options.quiet:
+ print('Can not be both quiet and verbose at the same time.')
+ return 1
+
+ check_bin = None
+ if options.gdb:
+ options.verbose = True
+ if options.wrapper:
+ print('Must not specify both a wrapper and gdb at the same time.')
+ return 1
+ check_bin = 'gdb'
+
+ if options.wrapper:
+ check_bin = options.wrapper[0]
+
+ if check_bin is not None:
+ exe = ExternalProgram(check_bin, silent=True)
+ if not exe.found():
+ sys.exit("Could not find requested program: %s" % check_bin)
+ options.wd = os.path.abspath(options.wd)
+
+ if not options.list and not options.no_rebuild:
+ if not rebuild_all(options.wd):
+ sys.exit(-1)
+
+ try:
+ th = TestHarness(options)
+ if options.list:
+ list_tests(th)
+ return 0
+ if not options.args:
+ return th.doit()
+ return th.run_special()
+ except TestException as e:
+ print('Mesontest encountered an error:\n')
+ print(e)
+ return 1
diff --git a/mesontest.py b/mesontest.py
index 83044aa..c2d39d6 100755
--- a/mesontest.py
+++ b/mesontest.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-# Copyright 2016 The Meson development team
+# Copyright 2016-2017 The Meson development team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,614 +16,10 @@
# A tool to run tests in many different ways.
-import shlex
-import subprocess, sys, os, argparse
-import pickle
-from mesonbuild import build
-from mesonbuild import environment
-from mesonbuild.dependencies import ExternalProgram
-from mesonbuild import mesonlib
-from mesonbuild import mlog
-
-import time, datetime, multiprocessing, json
-import concurrent.futures as conc
-import platform
-import signal
-import random
-
-# GNU autotools interprets a return code of 77 from tests it executes to
-# mean that the test should be skipped.
-GNU_SKIP_RETURNCODE = 77
-
-def is_windows():
- platname = platform.system().lower()
- return platname == 'windows' or 'mingw' in platname
-
-def is_cygwin():
- platname = platform.system().lower()
- return 'cygwin' in platname
-
-def determine_worker_count():
- varname = 'MESON_TESTTHREADS'
- if varname in os.environ:
- try:
- num_workers = int(os.environ[varname])
- except ValueError:
- print('Invalid value in %s, using 1 thread.' % varname)
- num_workers = 1
- else:
- try:
- # Fails in some weird environments such as Debian
- # reproducible build.
- num_workers = multiprocessing.cpu_count()
- except Exception:
- num_workers = 1
- return num_workers
-
-parser = argparse.ArgumentParser()
-parser.add_argument('--repeat', default=1, dest='repeat', type=int,
- help='Number of times to run the tests.')
-parser.add_argument('--no-rebuild', default=False, action='store_true',
- help='Do not rebuild before running tests.')
-parser.add_argument('--gdb', default=False, dest='gdb', action='store_true',
- help='Run test under gdb.')
-parser.add_argument('--list', default=False, dest='list', action='store_true',
- help='List available tests.')
-parser.add_argument('--wrapper', default=None, dest='wrapper', type=shlex.split,
- help='wrapper to run tests with (e.g. Valgrind)')
-parser.add_argument('-C', default='.', dest='wd',
- help='directory to cd into before running')
-parser.add_argument('--suite', default=[], dest='include_suites', action='append', metavar='SUITE',
- help='Only run tests belonging to the given suite.')
-parser.add_argument('--no-suite', default=[], dest='exclude_suites', action='append', metavar='SUITE',
- help='Do not run tests belonging to the given suite.')
-parser.add_argument('--no-stdsplit', default=True, dest='split', action='store_false',
- help='Do not split stderr and stdout in test logs.')
-parser.add_argument('--print-errorlogs', default=False, action='store_true',
- help="Whether to print failing tests' logs.")
-parser.add_argument('--benchmark', default=False, action='store_true',
- help="Run benchmarks instead of tests.")
-parser.add_argument('--logbase', default='testlog',
- help="Base name for log file.")
-parser.add_argument('--num-processes', default=determine_worker_count(), type=int,
- help='How many parallel processes to use.')
-parser.add_argument('-v', '--verbose', default=False, action='store_true',
- help='Do not redirect stdout and stderr')
-parser.add_argument('-q', '--quiet', default=False, action='store_true',
- help='Produce less output to the terminal.')
-parser.add_argument('-t', '--timeout-multiplier', type=float, default=None,
- help='Define a multiplier for test timeout, for example '
- ' when running tests in particular conditions they might take'
- ' more time to execute.')
-parser.add_argument('--setup', default=None, dest='setup',
- help='Which test setup to use.')
-parser.add_argument('--test-args', default=[], type=shlex.split,
- help='Arguments to pass to the specified test(s) or all tests')
-parser.add_argument('args', nargs='*',
- help='Optional list of tests to run')
-
-
-class TestException(mesonlib.MesonException):
- pass
-
-
-class TestRun:
- def __init__(self, res, returncode, should_fail, duration, stdo, stde, cmd,
- env):
- self.res = res
- self.returncode = returncode
- self.duration = duration
- self.stdo = stdo
- self.stde = stde
- self.cmd = cmd
- self.env = env
- self.should_fail = should_fail
-
- def get_log(self):
- res = '--- command ---\n'
- if self.cmd is None:
- res += 'NONE\n'
- else:
- res += "%s%s\n" % (''.join(["%s='%s' " % (k, v) for k, v in self.env.items()]), ' ' .join(self.cmd))
- if self.stdo:
- res += '--- stdout ---\n'
- res += self.stdo
- if self.stde:
- if res[-1:] != '\n':
- res += '\n'
- res += '--- stderr ---\n'
- res += self.stde
- if res[-1:] != '\n':
- res += '\n'
- res += '-------\n\n'
- return res
-
-def decode(stream):
- if stream is None:
- return ''
- try:
- return stream.decode('utf-8')
- except UnicodeDecodeError:
- return stream.decode('iso-8859-1', errors='ignore')
-
-def write_json_log(jsonlogfile, test_name, result):
- jresult = {'name': test_name,
- 'stdout': result.stdo,
- 'result': result.res,
- 'duration': result.duration,
- 'returncode': result.returncode,
- 'command': result.cmd}
- if isinstance(result.env, dict):
- jresult['env'] = result.env
- else:
- jresult['env'] = result.env.get_env(os.environ)
- if result.stde:
- jresult['stderr'] = result.stde
- jsonlogfile.write(json.dumps(jresult) + '\n')
-
-def run_with_mono(fname):
- if fname.endswith('.exe') and not (is_windows() or is_cygwin()):
- return True
- return False
-
-class TestHarness:
- def __init__(self, options):
- self.options = options
- self.collected_logs = []
- self.fail_count = 0
- self.success_count = 0
- self.skip_count = 0
- self.timeout_count = 0
- self.is_run = False
- self.tests = None
- self.suites = None
- self.logfilename = None
- self.logfile = None
- self.jsonlogfile = None
- if self.options.benchmark:
- datafile = os.path.join(options.wd, 'meson-private', 'meson_benchmark_setup.dat')
- else:
- datafile = os.path.join(options.wd, 'meson-private', 'meson_test_setup.dat')
- if not os.path.isfile(datafile):
- raise TestException('Directory %s does not seem to be a Meson build directory.' % options.wd)
- self.load_datafile(datafile)
-
- def __del__(self):
- if self.logfile:
- self.logfile.close()
- if self.jsonlogfile:
- self.jsonlogfile.close()
-
- def run_single_test(self, wrap, test):
- if test.fname[0].endswith('.jar'):
- cmd = ['java', '-jar'] + test.fname
- elif not test.is_cross and run_with_mono(test.fname[0]):
- cmd = ['mono'] + test.fname
- else:
- if test.is_cross:
- if test.exe_runner is None:
- # Can not run test on cross compiled executable
- # because there is no execute wrapper.
- cmd = None
- else:
- cmd = [test.exe_runner] + test.fname
- else:
- cmd = test.fname
-
- if cmd is None:
- res = 'SKIP'
- duration = 0.0
- stdo = 'Not run because can not execute cross compiled binaries.'
- stde = None
- returncode = GNU_SKIP_RETURNCODE
- else:
- cmd = wrap + cmd + test.cmd_args + self.options.test_args
- starttime = time.time()
- child_env = os.environ.copy()
- child_env.update(self.options.global_env.get_env(child_env))
- if isinstance(test.env, build.EnvironmentVariables):
- test.env = test.env.get_env(child_env)
-
- child_env.update(test.env)
- if len(test.extra_paths) > 0:
- child_env['PATH'] += os.pathsep.join([''] + test.extra_paths)
-
- # If MALLOC_PERTURB_ is not set, or if it is set to an empty value,
- # (i.e., the test or the environment don't explicitly set it), set
- # it ourselves. We do this unconditionally because it is extremely
- # useful to have in tests.
- # Setting MALLOC_PERTURB_="0" will completely disable this feature.
- if 'MALLOC_PERTURB_' not in child_env or not child_env['MALLOC_PERTURB_']:
- child_env['MALLOC_PERTURB_'] = str(random.randint(1, 255))
-
- setsid = None
- stdout = None
- stderr = None
- if not self.options.verbose:
- stdout = subprocess.PIPE
- stderr = subprocess.PIPE if self.options and self.options.split else subprocess.STDOUT
-
- if not is_windows():
- setsid = os.setsid
-
- p = subprocess.Popen(cmd,
- stdout=stdout,
- stderr=stderr,
- env=child_env,
- cwd=test.workdir,
- preexec_fn=setsid)
- timed_out = False
- if test.timeout is None:
- timeout = None
- else:
- timeout = test.timeout * self.options.timeout_multiplier
- try:
- (stdo, stde) = p.communicate(timeout=timeout)
- except subprocess.TimeoutExpired:
- if self.options.verbose:
- print("%s time out (After %d seconds)" % (test.name, timeout))
- timed_out = True
- # Python does not provide multiplatform support for
- # killing a process and all its children so we need
- # to roll our own.
- if is_windows():
- subprocess.call(['taskkill', '/F', '/T', '/PID', str(p.pid)])
- else:
- os.killpg(os.getpgid(p.pid), signal.SIGKILL)
- (stdo, stde) = p.communicate()
- endtime = time.time()
- duration = endtime - starttime
- stdo = decode(stdo)
- if stde:
- stde = decode(stde)
- if timed_out:
- res = 'TIMEOUT'
- self.timeout_count += 1
- self.fail_count += 1
- elif p.returncode == GNU_SKIP_RETURNCODE:
- res = 'SKIP'
- self.skip_count += 1
- elif test.should_fail == bool(p.returncode):
- res = 'OK'
- self.success_count += 1
- else:
- res = 'FAIL'
- self.fail_count += 1
- returncode = p.returncode
- result = TestRun(res, returncode, test.should_fail, duration, stdo, stde, cmd, test.env)
-
- return result
-
- def print_stats(self, numlen, tests, name, result, i):
- startpad = ' ' * (numlen - len('%d' % (i + 1)))
- num = '%s%d/%d' % (startpad, i + 1, len(tests))
- padding1 = ' ' * (38 - len(name))
- padding2 = ' ' * (8 - len(result.res))
- result_str = '%s %s %s%s%s%5.2f s' % \
- (num, name, padding1, result.res, padding2, result.duration)
- if not self.options.quiet or result.res != 'OK':
- if result.res != 'OK' and mlog.colorize_console:
- if result.res == 'FAIL' or result.res == 'TIMEOUT':
- decorator = mlog.red
- elif result.res == 'SKIP':
- decorator = mlog.yellow
- else:
- sys.exit('Unreachable code was ... well ... reached.')
- print(decorator(result_str).get_text(True))
- else:
- print(result_str)
- result_str += "\n\n" + result.get_log()
- if (result.returncode != GNU_SKIP_RETURNCODE) \
- and (result.returncode != 0) != result.should_fail:
- if self.options.print_errorlogs:
- self.collected_logs.append(result_str)
- if self.logfile:
- self.logfile.write(result_str)
- if self.jsonlogfile:
- write_json_log(self.jsonlogfile, name, result)
-
- def print_summary(self):
- msg = '''
-OK: %4d
-FAIL: %4d
-SKIP: %4d
-TIMEOUT: %4d
-''' % (self.success_count, self.fail_count, self.skip_count, self.timeout_count)
- print(msg)
- if self.logfile:
- self.logfile.write(msg)
-
- def print_collected_logs(self):
- if len(self.collected_logs) > 0:
- if len(self.collected_logs) > 10:
- print('\nThe output from 10 first failed tests:\n')
- else:
- print('\nThe output from the failed tests:\n')
- for log in self.collected_logs[:10]:
- lines = log.splitlines()
- if len(lines) > 104:
- print('\n'.join(lines[0:4]))
- print('--- Listing only the last 100 lines from a long log. ---')
- lines = lines[-100:]
- for line in lines:
- print(line)
-
- def doit(self):
- if self.is_run:
- raise RuntimeError('Test harness object can only be used once.')
- if not os.path.isfile(self.datafile):
- print('Test data file. Probably this means that you did not run this in the build directory.')
- return 1
- self.is_run = True
- tests = self.get_tests()
- if not tests:
- return 0
- self.run_tests(tests)
- return self.fail_count
-
- @staticmethod
- def split_suite_string(suite):
- if ':' in suite:
- return suite.split(':', 1)
- else:
- return suite, ""
-
- @staticmethod
- def test_in_suites(test, suites):
- for suite in suites:
- (prj_match, st_match) = TestHarness.split_suite_string(suite)
- for prjst in test.suite:
- (prj, st) = TestHarness.split_suite_string(prjst)
- if prj_match and prj != prj_match:
- continue
- if st_match and st != st_match:
- continue
- return True
- return False
-
- def test_suitable(self, test):
- return (not self.options.include_suites or TestHarness.test_in_suites(test, self.options.include_suites)) \
- and not TestHarness.test_in_suites(test, self.options.exclude_suites)
-
- def load_suites(self):
- ss = set()
- for t in self.tests:
- for s in t.suite:
- ss.add(s)
- self.suites = list(ss)
-
- def load_tests(self):
- with open(self.datafile, 'rb') as f:
- self.tests = pickle.load(f)
-
- def load_datafile(self, datafile):
- self.datafile = datafile
- self.load_tests()
- self.load_suites()
-
- def get_tests(self):
- if not self.tests:
- print('No tests defined.')
- return []
-
- if len(self.options.include_suites) or len(self.options.exclude_suites):
- tests = []
- for tst in self.tests:
- if self.test_suitable(tst):
- tests.append(tst)
- else:
- tests = self.tests
-
- if self.options.args:
- tests = [t for t in tests if t.name in self.options.args]
-
- if not tests:
- print('No suitable tests defined.')
- return []
-
- for test in tests:
- test.rebuilt = False
-
- return tests
-
- def open_log_files(self):
- if not self.options.logbase or self.options.verbose:
- return None, None, None, None
-
- namebase = None
- logfile_base = os.path.join(self.options.wd, 'meson-logs', self.options.logbase)
-
- if self.options.wrapper:
- namebase = os.path.split(self.get_wrapper()[0])[1]
- elif self.options.setup:
- namebase = self.options.setup
-
- if namebase:
- logfile_base += '-' + namebase.replace(' ', '_')
- self.logfilename = logfile_base + '.txt'
- self.jsonlogfilename = logfile_base + '.json'
-
- self.jsonlogfile = open(self.jsonlogfilename, 'w')
- self.logfile = open(self.logfilename, 'w')
-
- self.logfile.write('Log of Meson test suite run on %s\n\n'
- % datetime.datetime.now().isoformat())
-
- def get_wrapper(self):
- wrap = []
- if self.options.gdb:
- wrap = ['gdb', '--quiet', '--nh']
- if self.options.repeat > 1:
- wrap += ['-ex', 'run', '-ex', 'quit']
- # Signal the end of arguments to gdb
- wrap += ['--args']
- if self.options.wrapper:
- wrap += self.options.wrapper
- assert(isinstance(wrap, list))
- return wrap
-
- def get_pretty_suite(self, test):
- if len(self.suites) > 1:
- rv = TestHarness.split_suite_string(test.suite[0])[0]
- s = "+".join(TestHarness.split_suite_string(s)[1] for s in test.suite)
- if len(s):
- rv += ":"
- return rv + s + " / " + test.name
- else:
- return test.name
-
- def run_tests(self, tests):
- executor = None
- futures = []
- numlen = len('%d' % len(tests))
- self.open_log_files()
- wrap = self.get_wrapper()
-
- for _ in range(self.options.repeat):
- for i, test in enumerate(tests):
- visible_name = self.get_pretty_suite(test)
-
- if self.options.gdb:
- test.timeout = None
-
- if not test.is_parallel or self.options.gdb:
- self.drain_futures(futures)
- futures = []
- res = self.run_single_test(wrap, test)
- self.print_stats(numlen, tests, visible_name, res, i)
- else:
- if not executor:
- executor = conc.ThreadPoolExecutor(max_workers=self.options.num_processes)
- f = executor.submit(self.run_single_test, wrap, test)
- futures.append((f, numlen, tests, visible_name, i))
- if self.options.repeat > 1 and self.fail_count:
- break
- if self.options.repeat > 1 and self.fail_count:
- break
-
- self.drain_futures(futures)
- self.print_summary()
- self.print_collected_logs()
-
- if self.logfilename:
- print('Full log written to %s' % self.logfilename)
-
- def drain_futures(self, futures):
- for i in futures:
- (result, numlen, tests, name, i) = i
- if self.options.repeat > 1 and self.fail_count:
- result.cancel()
- if self.options.verbose:
- result.result()
- self.print_stats(numlen, tests, name, result.result(), i)
-
- def run_special(self):
- 'Tests run by the user, usually something like "under gdb 1000 times".'
- if self.is_run:
- raise RuntimeError('Can not use run_special after a full run.')
- tests = self.get_tests()
- if not tests:
- return 0
- self.run_tests(tests)
- return self.fail_count
-
-
-def list_tests(th):
- tests = th.get_tests()
- for t in tests:
- print(th.get_pretty_suite(t))
-
-def merge_suite_options(options):
- buildfile = os.path.join(options.wd, 'meson-private/build.dat')
- with open(buildfile, 'rb') as f:
- build = pickle.load(f)
- setups = build.test_setups
- if options.setup not in setups:
- sys.exit('Unknown test setup: %s' % options.setup)
- current = setups[options.setup]
- if not options.gdb:
- options.gdb = current.gdb
- if options.timeout_multiplier is None:
- options.timeout_multiplier = current.timeout_multiplier
-# if options.env is None:
-# options.env = current.env # FIXME, should probably merge options here.
- if options.wrapper is not None and current.exe_wrapper is not None:
- sys.exit('Conflict: both test setup and command line specify an exe wrapper.')
- if options.wrapper is None:
- options.wrapper = current.exe_wrapper
- return current.env
-
-def rebuild_all(wd):
- if not os.path.isfile(os.path.join(wd, 'build.ninja')):
- print("Only ninja backend is supported to rebuild tests before running them.")
- return True
-
- ninja = environment.detect_ninja()
- if not ninja:
- print("Can't find ninja, can't rebuild test.")
- return False
-
- p = subprocess.Popen([ninja, '-C', wd])
- p.communicate()
-
- if p.returncode != 0:
- print("Could not rebuild")
- return False
-
- return True
-
-def run(args):
- options = parser.parse_args(args)
-
- if options.benchmark:
- options.num_processes = 1
-
- if options.setup is not None:
- global_env = merge_suite_options(options)
- else:
- global_env = build.EnvironmentVariables()
- if options.timeout_multiplier is None:
- options.timeout_multiplier = 1
-
- setattr(options, 'global_env', global_env)
-
- if options.verbose and options.quiet:
- print('Can not be both quiet and verbose at the same time.')
- return 1
-
- check_bin = None
- if options.gdb:
- options.verbose = True
- if options.wrapper:
- print('Must not specify both a wrapper and gdb at the same time.')
- return 1
- check_bin = 'gdb'
-
- if options.wrapper:
- check_bin = options.wrapper[0]
-
- if check_bin is not None:
- exe = ExternalProgram(check_bin, silent=True)
- if not exe.found():
- sys.exit("Could not find requested program: %s" % check_bin)
- options.wd = os.path.abspath(options.wd)
-
- if not options.list and not options.no_rebuild:
- if not rebuild_all(options.wd):
- sys.exit(-1)
-
- try:
- th = TestHarness(options)
- if options.list:
- list_tests(th)
- return 0
- if not options.args:
- return th.doit()
- return th.run_special()
- except TestException as e:
- print('Mesontest encountered an error:\n')
- print(e)
- return 1
+from mesonbuild import mesonmain
+import sys
if __name__ == '__main__':
- sys.exit(run(sys.argv[1:]))
+ print('Warning: This executable is deprecated. Use "meson test" instead.',
+ file=sys.stderr)
+ sys.exit(mesonmain.run(['test'] + sys.argv[1:]))
diff --git a/run_project_tests.py b/run_project_tests.py
index d7ad257..58a0fd5 100755
--- a/run_project_tests.py
+++ b/run_project_tests.py
@@ -21,7 +21,7 @@ from io import StringIO
from ast import literal_eval
from enum import Enum
import tempfile
-import mesontest
+from mesonbuild import mtest
from mesonbuild import environment
from mesonbuild import mesonlib
from mesonbuild import mlog
@@ -287,12 +287,12 @@ def run_test_inprocess(testdir):
os.chdir(testdir)
test_log_fname = 'meson-logs/testlog.txt'
try:
- returncode_test = mesontest.run(['--no-rebuild'])
+ returncode_test = mtest.run(['--no-rebuild'])
if os.path.exists(test_log_fname):
test_log = open(test_log_fname, errors='ignore').read()
else:
test_log = ''
- returncode_benchmark = mesontest.run(['--no-rebuild', '--benchmark', '--logbase', 'benchmarklog'])
+ returncode_benchmark = mtest.run(['--no-rebuild', '--benchmark', '--logbase', 'benchmarklog'])
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr