Merge pull request #7902 from bonzini/mtest-build-depends-only

mtest: only build what is needed for the tests
pull/8106/head
Jussi Pakkanen 4 years ago committed by GitHub
commit d32d0d6b53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      docs/markdown/snippets/meson_test_depends.md
  2. 18
      mesonbuild/minstall.py
  3. 43
      mesonbuild/mintro.py
  4. 38
      mesonbuild/mtest.py

@ -0,0 +1,17 @@
## `meson test` only rebuilds test dependencies
Until now, `meson test` rebuilt the whole project independent of the
requested tests and their dependencies. With this release, `meson test`
will only rebuild what is needed for the tests or suites that will be run.
This feature can be used, for example, to speed up bisecting regressions
using commands like the following:
git bisect start <broken commit> <working commit>
git bisect run meson test <failing test name>
This would find the broken commit automatically while at each step
rebuilding only those pieces of code needed to run the test.
However, this change could cause failures if dependencies are not
specified correctly in `meson.build`.

@ -19,7 +19,6 @@ from glob import glob
from .scripts import depfixer from .scripts import depfixer
from .scripts import destdir_join from .scripts import destdir_join
from .mesonlib import is_windows, Popen_safe from .mesonlib import is_windows, Popen_safe
from .mtest import rebuild_all
from .backend.backends import InstallData from .backend.backends import InstallData
from .coredata import major_versions_differ, MesonVersionMismatchException from .coredata import major_versions_differ, MesonVersionMismatchException
from .coredata import version as coredata_version from .coredata import version as coredata_version
@ -532,6 +531,23 @@ class Installer:
else: else:
raise raise
def rebuild_all(wd: str) -> bool:
if not (Path(wd) / 'build.ninja').is_file():
print('Only ninja backend is supported to rebuild the project before installation.')
return True
ninja = environment.detect_ninja()
if not ninja:
print("Can't find ninja, can't rebuild test.")
return False
ret = subprocess.run(ninja + ['-C', wd]).returncode
if ret != 0:
print('Could not rebuild {}'.format(wd))
return False
return True
def run(opts): def run(opts):
datafilename = 'meson-private/install.dat' datafilename = 'meson-private/install.dat'
private_dir = os.path.dirname(datafilename) private_dir = os.path.dirname(datafilename)

@ -385,12 +385,25 @@ def print_results(options: argparse.Namespace, results: T.Sequence[T.Tuple[str,
print(json.dumps(out, indent=indent)) print(json.dumps(out, indent=indent))
return 0 return 0
def get_infodir(builddir: T.Optional[str] = None) -> str:
infodir = 'meson-info'
if builddir is not None:
infodir = os.path.join(builddir, infodir)
return infodir
def get_info_file(infodir: str, kind: T.Optional[str] = None) -> str:
return os.path.join(infodir,
'meson-info.json' if not kind else 'intro-{}.json'.format(kind))
def load_info_file(infodir: str, kind: T.Optional[str] = None) -> T.Any:
with open(get_info_file(infodir, kind), 'r') as fp:
return json.load(fp)
def run(options: argparse.Namespace) -> int: def run(options: argparse.Namespace) -> int:
datadir = 'meson-private' datadir = 'meson-private'
infodir = 'meson-info' infodir = get_infodir(options.builddir)
if options.builddir is not None: if options.builddir is not None:
datadir = os.path.join(options.builddir, datadir) datadir = os.path.join(options.builddir, datadir)
infodir = os.path.join(options.builddir, infodir)
indent = 4 if options.indent else None indent = 4 if options.indent else None
results = [] # type: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]] results = [] # type: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]]
sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11] sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11]
@ -411,18 +424,19 @@ def run(options: argparse.Namespace) -> int:
results += [(key, val.no_bd(intr))] results += [(key, val.no_bd(intr))]
return print_results(options, results, indent) return print_results(options, results, indent)
infofile = get_meson_info_file(infodir) try:
if not os.path.isdir(datadir) or not os.path.isdir(infodir) or not os.path.isfile(infofile): raw = load_info_file(infodir)
intro_vers = raw.get('introspection', {}).get('version', {}).get('full', '0.0.0')
except FileNotFoundError:
if not os.path.isdir(datadir) or not os.path.isdir(infodir):
print('Current directory is not a meson build directory.\n' print('Current directory is not a meson build directory.\n'
'Please specify a valid build dir or change the working directory to it.\n' 'Please specify a valid build dir or change the working directory to it.')
else:
print('Introspection file {} does not exist.\n'
'It is also possible that the build directory was generated with an old\n' 'It is also possible that the build directory was generated with an old\n'
'meson version. Please regenerate it in this case.') 'meson version. Please regenerate it in this case.'.format(get_info_file(infodir)))
return 1 return 1
with open(infofile, 'r') as fp:
raw = json.load(fp)
intro_vers = raw.get('introspection', {}).get('version', {}).get('full', '0.0.0')
vers_to_check = get_meson_introspection_required_version() vers_to_check = get_meson_introspection_required_version()
for i in vers_to_check: for i in vers_to_check:
if not mesonlib.version_compare(intro_vers, i): if not mesonlib.version_compare(intro_vers, i):
@ -437,12 +451,11 @@ def run(options: argparse.Namespace) -> int:
continue continue
if not options.all and not getattr(options, i, False): if not options.all and not getattr(options, i, False):
continue continue
curr = os.path.join(infodir, 'intro-{}.json'.format(i)) try:
if not os.path.isfile(curr): results += [(i, load_info_file(infodir, i))]
print('Introspection file {} does not exist.'.format(curr)) except FileNotFoundError:
print('Introspection file {} does not exist.'.format(get_info_file(infodir, i)))
return 1 return 1
with open(curr, 'r') as fp:
results += [(i, json.load(fp))]
return print_results(options, results, indent) return print_results(options, results, indent)

@ -45,6 +45,7 @@ from .coredata import major_versions_differ, MesonVersionMismatchException
from .coredata import version as coredata_version from .coredata import version as coredata_version
from .dependencies import ExternalProgram from .dependencies import ExternalProgram
from .mesonlib import MesonException, get_wine_shortpath, split_args, join_args from .mesonlib import MesonException, get_wine_shortpath, split_args, join_args
from .mintro import get_infodir, load_info_file
from .backend.backends import TestProtocol, TestSerialisation from .backend.backends import TestProtocol, TestSerialisation
# GNU autotools interprets a return code of 77 from tests it executes to # GNU autotools interprets a return code of 77 from tests it executes to
@ -1019,13 +1020,21 @@ class TestHarness:
def total_failure_count(self) -> int: def total_failure_count(self) -> int:
return self.fail_count + self.unexpectedpass_count + self.timeout_count return self.fail_count + self.unexpectedpass_count + self.timeout_count
def doit(self) -> int: def doit(self, options: argparse.Namespace) -> int:
if self.is_run: if self.is_run:
raise RuntimeError('Test harness object can only be used once.') raise RuntimeError('Test harness object can only be used once.')
self.is_run = True self.is_run = True
tests = self.get_tests() tests = self.get_tests()
if not tests: if not tests:
return 0 return 0
if not options.no_rebuild and not rebuild_deps(options.wd, tests):
# We return 125 here in case the build failed.
# The reason is that exit code 125 tells `git bisect run` that the current
# commit should be skipped. Thus users can directly use `meson test` to
# bisect without needing to handle the does-not-build case separately in a
# wrapper script.
sys.exit(125)
self.run_tests(tests) self.run_tests(tests)
return self.total_failure_count() return self.total_failure_count()
@ -1272,7 +1281,7 @@ def list_tests(th: TestHarness) -> bool:
print(th.get_pretty_suite(t)) print(th.get_pretty_suite(t))
return not tests return not tests
def rebuild_all(wd: str) -> bool: def rebuild_deps(wd: str, tests: T.List[TestSerialisation]) -> bool:
if not (Path(wd) / 'build.ninja').is_file(): if not (Path(wd) / 'build.ninja').is_file():
print('Only ninja backend is supported to rebuild tests before running them.') print('Only ninja backend is supported to rebuild tests before running them.')
return True return True
@ -1282,7 +1291,21 @@ def rebuild_all(wd: str) -> bool:
print("Can't find ninja, can't rebuild test.") print("Can't find ninja, can't rebuild test.")
return False return False
ret = subprocess.run(ninja + ['-C', wd]).returncode depends = set() # type: T.Set[str]
targets = set() # type: T.Set[str]
intro_targets = dict() # type: T.Dict[str, T.List[str]]
for target in load_info_file(get_infodir(wd), kind='targets'):
intro_targets[target['id']] = [
os.path.relpath(f, wd)
for f in target['filename']]
for t in tests:
for d in t.depends:
if d in depends:
continue
depends.update(d)
targets.update(intro_targets[d])
ret = subprocess.run(ninja + ['-C', wd] + sorted(targets)).returncode
if ret != 0: if ret != 0:
print('Could not rebuild {}'.format(wd)) print('Could not rebuild {}'.format(wd))
return False return False
@ -1318,18 +1341,11 @@ def run(options: argparse.Namespace) -> int:
print('Could not find requested program: {!r}'.format(check_bin)) print('Could not find requested program: {!r}'.format(check_bin))
return 1 return 1
if not options.list and not options.no_rebuild:
if not rebuild_all(options.wd):
# We return 125 here in case the build failed.
# The reason is that exit code 125 tells `git bisect run` that the current commit should be skipped.
# Thus users can directly use `meson test` to bisect without needing to handle the does-not-build case separately in a wrapper script.
return 125
with TestHarness(options) as th: with TestHarness(options) as th:
try: try:
if options.list: if options.list:
return list_tests(th) return list_tests(th)
return th.doit() return th.doit(options)
except TestException as e: except TestException as e:
print('Meson test encountered an error:\n') print('Meson test encountered an error:\n')
if os.environ.get('MESON_FORCE_BACKTRACE'): if os.environ.get('MESON_FORCE_BACKTRACE'):

Loading…
Cancel
Save