aboutsummaryrefslogtreecommitdiff
path: root/run_project_tests.py
diff options
context:
space:
mode:
Diffstat (limited to 'run_project_tests.py')
-rwxr-xr-xrun_project_tests.py294
1 files changed, 142 insertions, 152 deletions
diff --git a/run_project_tests.py b/run_project_tests.py
index 1194abf..86db599 100755
--- a/run_project_tests.py
+++ b/run_project_tests.py
@@ -24,7 +24,6 @@ import sys
import signal
import shlex
from io import StringIO
-from ast import literal_eval
from enum import Enum
import tempfile
from pathlib import Path, PurePath
@@ -34,7 +33,7 @@ from mesonbuild import compilers
from mesonbuild import mesonlib
from mesonbuild import mlog
from mesonbuild import mtest
-from mesonbuild.mesonlib import MachineChoice, stringlistify, Popen_safe
+from mesonbuild.mesonlib import MachineChoice, Popen_safe
from mesonbuild.coredata import backendlist
import argparse
import json
@@ -90,6 +89,53 @@ class TestResult:
def fail(self, msg):
self.msg = msg
+class InstalledFile:
+ def __init__(self, raw: T.Dict[str, str]):
+ self.path = raw['file']
+ self.typ = raw['type']
+ self.platform = raw.get('platform', None)
+
+ def get_path(self, compiler: str, env: environment.Environment) -> T.Optional[Path]:
+ p = Path(self.path)
+ canonical_compiler = compiler
+ if (compiler in ['clang-cl', 'intel-cl']) or (env.machines.host.is_windows() and compiler == 'pgi'):
+ canonical_compiler = 'msvc'
+
+ # Abort if the platform does not match
+ matches = {
+ 'msvc': canonical_compiler == 'msvc',
+ 'gcc': canonical_compiler != 'msvc',
+ 'cygwin': env.machines.host.is_cygwin(),
+ '!cygwin': not env.machines.host.is_cygwin(),
+ }.get(self.platform or '', True)
+ if not matches:
+ return None
+
+ # Handle the different types
+ if self.typ == 'file':
+ return p
+ elif self.typ == 'exe':
+ if env.machines.host.is_windows() or env.machines.host.is_cygwin():
+ return p.with_suffix('.exe')
+ elif self.typ == 'pdb':
+ return p.with_suffix('.pdb') if canonical_compiler == 'msvc' else None
+ elif self.typ == 'implib' or self.typ == 'implibempty':
+ if env.machines.host.is_windows() and canonical_compiler == 'msvc':
+ # only MSVC doesn't generate empty implibs
+ if self.typ == 'implibempty' and compiler == 'msvc':
+ return None
+ return p.parent / (re.sub(r'^lib', '', p.name) + '.lib')
+ elif env.machines.host.is_windows() or env.machines.host.is_cygwin():
+ return p.with_suffix('.dll.a')
+ else:
+ return None
+ elif self.typ == 'expr':
+ return Path(platform_fix_name(p.as_posix(), canonical_compiler, env))
+ else:
+ raise RuntimeError('Invalid installed file type {}'.format(self.typ))
+
+ return p
+
@functools.total_ordering
class TestDef:
def __init__(self, path: Path, name: T.Optional[str], args: T.List[str], skip: bool = False):
@@ -97,6 +143,9 @@ class TestDef:
self.name = name
self.args = args
self.skip = skip
+ self.env = os.environ.copy()
+ self.installed_files = [] # type: T.List[InstalledFile]
+ self.do_not_set_opts = [] # type: T.List[str]
def __repr__(self) -> str:
return '<{}: {:<48} [{}: {}] -- {}>'.format(type(self).__name__, str(self.path), self.name, self.args, self.skip)
@@ -106,10 +155,12 @@ class TestDef:
return '{} ({})'.format(self.path.as_posix(), self.name)
return self.path.as_posix()
- def __lt__(self, other: T.Any) -> T.Union[bool, type(NotImplemented)]:
+ def __lt__(self, other: T.Any) -> bool:
if isinstance(other, TestDef):
# None is not sortable, so replace it with an empty string
- return (self.path, self.name or '') < (other.path, other.name or '')
+ s_id = int(self.path.name.split(' ')[0])
+ o_id = int(other.path.name.split(' ')[0])
+ return (s_id, self.path, self.name or '') < (o_id, other.path, other.name or '')
return NotImplemented
class AutoDeletedDir:
@@ -156,17 +207,8 @@ def setup_commands(optbackend):
compile_commands, clean_commands, test_commands, install_commands, \
uninstall_commands = get_backend_commands(backend, do_debug)
-def get_relative_files_list_from_dir(fromdir: Path) -> T.List[Path]:
- return [file.relative_to(fromdir) for file in fromdir.rglob('*') if file.is_file()]
-
-def platform_fix_name(fname: str, compiler, env) -> str:
- # canonicalize compiler
- if (compiler in {'clang-cl', 'intel-cl'} or
- (env.machines.host.is_windows() and compiler == 'pgi')):
- canonical_compiler = 'msvc'
- else:
- canonical_compiler = compiler
-
+# TODO try to eliminate or at least reduce this function
+def platform_fix_name(fname: str, canonical_compiler: str, env: environment.EnvironmentException) -> str:
if '?lib' in fname:
if env.machines.host.is_windows() and canonical_compiler == 'msvc':
fname = re.sub(r'lib/\?lib(.*)\.', r'bin/\1.', fname)
@@ -183,31 +225,6 @@ def platform_fix_name(fname: str, compiler, env) -> str:
else:
fname = re.sub(r'\?lib', 'lib', fname)
- if fname.endswith('?exe'):
- fname = fname[:-4]
- if env.machines.host.is_windows() or env.machines.host.is_cygwin():
- return fname + '.exe'
-
- if fname.startswith('?msvc:'):
- fname = fname[6:]
- if canonical_compiler != 'msvc':
- return None
-
- if fname.startswith('?gcc:'):
- fname = fname[5:]
- if canonical_compiler == 'msvc':
- return None
-
- if fname.startswith('?cygwin:'):
- fname = fname[8:]
- if not env.machines.host.is_cygwin():
- return None
-
- if fname.startswith('?!cygwin:'):
- fname = fname[9:]
- if env.machines.host.is_cygwin():
- return None
-
if fname.endswith('?so'):
if env.machines.host.is_windows() and canonical_compiler == 'msvc':
fname = re.sub(r'lib/([^/]*)\?so$', r'bin/\1.dll', fname)
@@ -227,49 +244,28 @@ def platform_fix_name(fname: str, compiler, env) -> str:
else:
return fname[:-3] + '.so'
- if fname.endswith('?implib') or fname.endswith('?implibempty'):
- if env.machines.host.is_windows() and canonical_compiler == 'msvc':
- # only MSVC doesn't generate empty implibs
- if fname.endswith('?implibempty') and compiler == 'msvc':
- return None
- return re.sub(r'/(?:lib|)([^/]*?)\?implib(?:empty|)$', r'/\1.lib', fname)
- elif env.machines.host.is_windows() or env.machines.host.is_cygwin():
- return re.sub(r'\?implib(?:empty|)$', r'.dll.a', fname)
- else:
- return None
-
return fname
-def validate_install(srcdir: str, installdir: Path, compiler, env) -> str:
- # List of installed files
- info_file = Path(srcdir) / 'installed_files.txt'
- installdir = Path(installdir)
- expected = {} # type: T.Dict[Path, bool]
+def validate_install(test: TestDef, installdir: Path, compiler: str, env: environment.Environment) -> str:
+ expected_raw = [x.get_path(compiler, env) for x in test.installed_files]
+ expected = {Path(x): False for x in expected_raw if x}
+ found = [x.relative_to(installdir) for x in installdir.rglob('*') if x.is_file() or x.is_symlink()]
ret_msg = ''
- # Generate list of expected files
- if info_file.is_file():
- with info_file.open() as f:
- for line in f:
- line = platform_fix_name(line.strip(), compiler, env)
- if line:
- expected[Path(line)] = False
- # Check if expected files were found
- for fname in expected:
- file_path = installdir / fname
- if file_path.is_file() or file_path.is_symlink():
- expected[fname] = True
- for (fname, found) in expected.items():
- if not found:
- ret_msg += 'Expected file {} missing.\n'.format(fname)
- # Check if there are any unexpected files
- found = get_relative_files_list_from_dir(installdir)
+ # Mark all found files as found and detect unexpected files
for fname in found:
if fname not in expected:
ret_msg += 'Extra file {} found.\n'.format(fname)
+ continue
+ expected[fname] = True
+ # Check if expected files were found
+ for p, f in expected.items():
+ if not f:
+ ret_msg += 'Expected file {} missing.\n'.format(p)
+ # List dir content on error
if ret_msg != '':
ret_msg += '\nInstall dir contents:\n'
for i in found:
- ret_msg += ' - {}'.format(i)
+ ret_msg += ' - {}\n'.format(i)
return ret_msg
def log_text_file(logfile, testdir, stdo, stde):
@@ -361,83 +357,45 @@ def run_test_inprocess(testdir):
os.chdir(old_cwd)
return max(returncode_test, returncode_benchmark), mystdout.getvalue(), mystderr.getvalue(), test_log
-def parse_test_args(testdir):
- args = []
- try:
- with open(os.path.join(testdir, 'test_args.txt'), 'r') as f:
- content = f.read()
- try:
- args = literal_eval(content)
- except Exception:
- raise Exception('Malformed test_args file.')
- args = stringlistify(args)
- except FileNotFoundError:
- pass
- return args
-
# Build directory name must be the same so Ccache works over
# consecutive invocations.
-def create_deterministic_builddir(src_dir, name):
+def create_deterministic_builddir(test: TestDef) -> str:
import hashlib
- if name:
- src_dir += name
+ src_dir = test.path.as_posix()
+ if test.name:
+ src_dir += test.name
rel_dirname = 'b ' + hashlib.sha256(src_dir.encode(errors='ignore')).hexdigest()[0:10]
os.mkdir(rel_dirname)
abs_pathname = os.path.join(os.getcwd(), rel_dirname)
return abs_pathname
-def run_test(skipped, testdir, name, extra_args, compiler, backend, flags, commands, should_fail):
- if skipped:
+def run_test(test: TestDef, extra_args, compiler, backend, flags, commands, should_fail):
+ if test.skip:
return None
- with AutoDeletedDir(create_deterministic_builddir(testdir, name)) as build_dir:
+ with AutoDeletedDir(create_deterministic_builddir(test)) as build_dir:
with AutoDeletedDir(tempfile.mkdtemp(prefix='i ', dir=os.getcwd())) as install_dir:
try:
- return _run_test(testdir, build_dir, install_dir, extra_args, compiler, backend, flags, commands, should_fail)
+ return _run_test(test, build_dir, install_dir, extra_args, compiler, backend, flags, commands, should_fail)
finally:
mlog.shutdown() # Close the log file because otherwise Windows wets itself.
-def pass_prefix_to_test(dirname):
- if '39 prefix absolute' in dirname:
- return False
- return True
-
-def pass_libdir_to_test(dirname):
- if '8 install' in dirname:
- return False
- if '38 libdir must be inside prefix' in dirname:
- return False
- if '195 install_mode' in dirname:
- return False
- return True
-
-def _run_test(testdir, test_build_dir, install_dir, extra_args, compiler, backend, flags, commands, should_fail):
+def _run_test(test: TestDef, test_build_dir: str, install_dir: str, extra_args, compiler, backend, flags, commands, should_fail):
compile_commands, clean_commands, install_commands, uninstall_commands = commands
- test_args = parse_test_args(testdir)
gen_start = time.time()
- setup_env = None
# Configure in-process
- if pass_prefix_to_test(testdir):
- gen_args = ['--prefix', 'x:/usr'] if mesonlib.is_windows() else ['--prefix', '/usr']
- else:
- gen_args = []
- if pass_libdir_to_test(testdir):
+ gen_args = [] # type: T.List[str]
+ if 'prefix' not in test.do_not_set_opts:
+ gen_args += ['--prefix', 'x:/usr'] if mesonlib.is_windows() else ['--prefix', '/usr']
+ if 'libdir' not in test.do_not_set_opts:
gen_args += ['--libdir', 'lib']
- gen_args += [testdir, test_build_dir] + flags + test_args + extra_args
- nativefile = os.path.join(testdir, 'nativefile.ini')
- if os.path.exists(nativefile):
- gen_args.extend(['--native-file', nativefile])
- crossfile = os.path.join(testdir, 'crossfile.ini')
- if os.path.exists(crossfile):
- gen_args.extend(['--cross-file', crossfile])
- setup_env_file = os.path.join(testdir, 'setup_env.json')
- if os.path.exists(setup_env_file):
- setup_env = os.environ.copy()
- with open(setup_env_file, 'r') as fp:
- data = json.load(fp)
- for key, val in data.items():
- val = val.replace('@ROOT@', os.path.abspath(testdir))
- setup_env[key] = val
- (returncode, stdo, stde) = run_configure(gen_args, env=setup_env)
+ gen_args += [test.path.as_posix(), test_build_dir] + flags + extra_args
+ nativefile = test.path / 'nativefile.ini'
+ crossfile = test.path / 'crossfile.ini'
+ if nativefile.exists():
+ gen_args.extend(['--native-file', nativefile.as_posix()])
+ if crossfile.exists():
+ gen_args.extend(['--cross-file', crossfile.as_posix()])
+ (returncode, stdo, stde) = run_configure(gen_args, env=test.env)
try:
logfile = Path(test_build_dir, 'meson-logs', 'meson-log.txt')
mesonlog = logfile.open(errors='ignore', encoding='utf-8').read()
@@ -462,7 +420,7 @@ def _run_test(testdir, test_build_dir, install_dir, extra_args, compiler, backen
# Touch the meson.build file to force a regenerate so we can test that
# regeneration works before a build is run.
ensure_backend_detects_changes(backend)
- os.utime(os.path.join(testdir, 'meson.build'))
+ os.utime(str(test.path / 'meson.build'))
# Build with subprocess
dir_args = get_backend_args_for_dir(backend, test_build_dir)
build_start = time.time()
@@ -479,7 +437,7 @@ def _run_test(testdir, test_build_dir, install_dir, extra_args, compiler, backen
# Touch the meson.build file to force a regenerate so we can test that
# regeneration works after a build is complete.
ensure_backend_detects_changes(backend)
- os.utime(os.path.join(testdir, 'meson.build'))
+ os.utime(str(test.path / 'meson.build'))
test_start = time.time()
# Test in-process
(returncode, tstdo, tstde, test_log) = run_test_inprocess(test_build_dir)
@@ -515,27 +473,54 @@ def _run_test(testdir, test_build_dir, install_dir, extra_args, compiler, backen
testresult.add_step(BuildStep.install, '', '')
if not install_commands:
return testresult
- install_msg = validate_install(testdir, install_dir, compiler, builddata.environment)
+ install_msg = validate_install(test, Path(install_dir), compiler, builddata.environment)
if install_msg:
- testresult.fail(install_msg)
+ testresult.fail('\n' + install_msg)
return testresult
return testresult
-def gather_tests(testdir: Path) -> T.Iterator[TestDef]:
- tests = [t.name for t in testdir.glob('*') if t.is_dir()]
+def gather_tests(testdir: Path) -> T.List[TestDef]:
+ tests = [t.name for t in testdir.iterdir() if t.is_dir()]
tests = [t for t in tests if not t.startswith('.')] # Filter non-tests files (dot files, etc)
- tests = [TestDef(testdir / t, None, []) for t in tests]
- all_tests = []
- for t in tests:
- matrix_file = t.path / 'test_matrix.json'
- if not matrix_file.is_file():
+ test_defs = [TestDef(testdir / t, None, []) for t in tests]
+ all_tests = [] # type: T.List[TestDef]
+ for t in test_defs:
+ test_def_file = t.path / 'test.json'
+ if not test_def_file.is_file():
+ all_tests += [t]
+ continue
+
+ test_def = json.loads(test_def_file.read_text())
+
+ # Handle additional environment variables
+ env = {} # type: T.Dict[str, str]
+ if 'env' in test_def:
+ assert isinstance(test_def['env'], dict)
+ env = test_def['env']
+ for key, val in env.items():
+ val = val.replace('@ROOT@', t.path.resolve().as_posix())
+ env[key] = val
+
+ # Handle installed files
+ installed = [] # type: T.List[InstalledFile]
+ if 'installed' in test_def:
+ installed = [InstalledFile(x) for x in test_def['installed']]
+
+ # Handle the do_not_set_opts list
+ do_not_set_opts = test_def.get('do_not_set_opts', []) # type: T.List[str]
+
+ # Skip the matrix code and just update the existing test
+ if 'matrix' not in test_def:
+ t.env.update(env)
+ t.installed_files = installed
+ t.do_not_set_opts = do_not_set_opts
all_tests += [t]
continue
- # Build multiple tests from matrix definition
+ # 'matrix; entry is present, so build multiple tests from matrix definition
opt_list = [] # type: T.List[T.List[T.Tuple[str, bool]]]
- matrix = json.loads(matrix_file.read_text())
+ matrix = test_def['matrix']
assert "options" in matrix
for key, val in matrix["options"].items():
assert isinstance(val, list)
@@ -552,8 +537,8 @@ def gather_tests(testdir: Path) -> T.Iterator[TestDef]:
# Skip the matrix entry if environment variable is present
if 'skip_on_env' in i:
- for env in i['skip_on_env']:
- if env in os.environ:
+ for skip_env_var in i['skip_on_env']:
+ if skip_env_var in os.environ:
skip = True
# Only run the test if all compiler ID's match
@@ -596,7 +581,11 @@ def gather_tests(testdir: Path) -> T.Iterator[TestDef]:
name = ' '.join([x[0] for x in i if x[0] is not None])
opts = ['-D' + x[0] for x in i if x[0] is not None]
skip = any([x[1] for x in i])
- all_tests += [TestDef(t.path, name, opts, skip)]
+ test = TestDef(t.path, name, opts, skip)
+ test.env.update(env)
+ test.installed_files = installed
+ test.do_not_set_opts = do_not_set_opts
+ all_tests += [test]
return sorted(all_tests)
@@ -880,7 +869,8 @@ def _run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]],
suite_args = ['--fatal-meson-warnings']
should_fail = name.split('warning-')[1]
- result = executor.submit(run_test, skipped or t.skip, t.path.as_posix(), t.name, extra_args + suite_args + t.args,
+ t.skip = skipped or t.skip
+ result = executor.submit(run_test, t, extra_args + suite_args + t.args,
system_compiler, backend, backend_flags, commands, should_fail)
futures.append((testname, t, result))
for (testname, t, result) in futures:
@@ -1033,11 +1023,11 @@ def detect_system_compiler(options):
for lang in sorted(compilers.all_languages):
try:
comp = env.compiler_from_language(lang, MachineChoice.HOST)
- details = '{} {} [{}]'.format(' '.join(comp.get_exelist()), comp.get_version_string(), comp.get_id())
+ details = '{:<10} {} {}'.format('[' + comp.get_id() + ']', ' '.join(comp.get_exelist()), comp.get_version_string())
compiler_id_map[lang] = comp.get_id()
except mesonlib.MesonException:
comp = None
- details = 'not found'
+ details = '[not found]'
print('%-7s: %s' % (lang, details))
# note C compiler for later use by platform_fix_name()