Skip to content

Commit

Permalink
mtest: only build what is needed for the tests
Browse files Browse the repository at this point in the history
It is a usual workflow to fix something and retest to see if it is fixed using a
particular test.  When tests start to become numerous, it becomes time consuming
for "meson test" to relink all of them (and in fact rebuild the whole project)
where the user has already specified the tests they want to run, as well as
the tests' dependencies.

Teach meson to be smart and only build what is needed for the test (or suite)
that were specified.

Fixes: mesonbuild#7473
Related: mesonbuild#7830
  • Loading branch information
bonzini committed Dec 14, 2020
1 parent 879e9d1 commit 79e2c52
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 12 deletions.
17 changes: 17 additions & 0 deletions docs/markdown/snippets/meson_test_depends.md
Original file line number Diff line number Diff line change
@@ -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`.

18 changes: 17 additions & 1 deletion mesonbuild/minstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from .scripts import depfixer
from .scripts import destdir_join
from .mesonlib import is_windows, Popen_safe
from .mtest import rebuild_all
from .backend.backends import InstallData
from .coredata import major_versions_differ, MesonVersionMismatchException
from .coredata import version as coredata_version
Expand Down Expand Up @@ -532,6 +531,23 @@ def install_targets(self, d):
else:
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):
datafilename = 'meson-private/install.dat'
private_dir = os.path.dirname(datafilename)
Expand Down
38 changes: 27 additions & 11 deletions mesonbuild/mtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from .coredata import version as coredata_version
from .dependencies import ExternalProgram
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

# GNU autotools interprets a return code of 77 from tests it executes to
Expand Down Expand Up @@ -1019,13 +1020,21 @@ def print_collected_logs(self) -> None:
def total_failure_count(self) -> int:
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:
raise RuntimeError('Test harness object can only be used once.')
self.is_run = True
tests = self.get_tests()
if not tests:
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)
return self.total_failure_count()

Expand Down Expand Up @@ -1272,7 +1281,7 @@ def list_tests(th: TestHarness) -> bool:
print(th.get_pretty_suite(t))
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():
print('Only ninja backend is supported to rebuild tests before running them.')
return True
Expand All @@ -1282,7 +1291,21 @@ def rebuild_all(wd: str) -> bool:
print("Can't find ninja, can't rebuild test.")
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:
print('Could not rebuild {}'.format(wd))
return False
Expand Down Expand Up @@ -1318,18 +1341,11 @@ def run(options: argparse.Namespace) -> int:
print('Could not find requested program: {!r}'.format(check_bin))
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:
try:
if options.list:
return list_tests(th)
return th.doit()
return th.doit(options)
except TestException as e:
print('Meson test encountered an error:\n')
if os.environ.get('MESON_FORCE_BACKTRACE'):
Expand Down

0 comments on commit 79e2c52

Please sign in to comment.