Skip to content

Commit c69b686

Browse files
committed
mtest: add option to slice tests
Executing tests can take a very long time. As an example, the Git test suite on Windows takes around 4 hours to execute. The Git project has been working around the issue by splitting up CI jobs into multiple slices: one job creates the build artifacts, and then we spawn N test jobs with those artifacts, where each test job executes 1/Nth of the tests. This can be scripted rather easily by using `meson test --list`, selecting every Nth line, but there may be other projects that have a similar need. Wire up a new option "--slice i/n" to `meson test` that does implements this logic. Signed-off-by: Patrick Steinhardt <[email protected]>
1 parent 6299f18 commit c69b686

File tree

5 files changed

+73
-1
lines changed

5 files changed

+73
-1
lines changed

Diff for: docs/markdown/Unit-tests.md

+5
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ name(s), the test name(s) must be contained in the suite(s). This
206206
however is redundant-- it would be more useful to specify either
207207
specific test names or suite(s).
208208

209+
Since version *1.7.0*, you can pass `--slice i/n` to split up the set of tests
210+
into `n` slices and execute the `ith` such slice. This allows you to distribute
211+
a set of long-running tests across multiple machines to decrease the overall
212+
runtime of tests.
213+
209214
### Other test options
210215

211216
Sometimes you need to run the tests multiple times, which is done like this:

Diff for: docs/markdown/snippets/test-slicing.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
## New option to execute a slice of tests
2+
3+
When tests take a long time to run a common strategy is to slice up the tests
4+
into multiple sets, where each set is executed on a separate machine. You can
5+
now use the `--slice i/n` argument for `meson test` to create `n` slices and
6+
execute the `ith` slice.

Diff for: man/meson.1

+3
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ a multiplier to use for test timeout values (usually something like 100 for Valg
328328
.TP
329329
\fB\-\-setup\fR
330330
use the specified test setup
331+
.Tp
332+
\fB\-\-slice SLICE/NUM_SLICES\fR
333+
Split tests into NUM_SLICES slices and execute slice number SLICE. (Since 1.7.0)
331334

332335
.SH The wrap command
333336

Diff for: mesonbuild/mtest.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,29 @@ def uniwidth(s: str) -> int:
9999
result += UNIWIDTH_MAPPING[w]
100100
return result
101101

102+
def test_slice(arg: str) -> T.Tuple[int, int]:
103+
values = arg.split('/')
104+
if len(values) != 2:
105+
raise argparse.ArgumentTypeError("value does not conform to format 'SLICE/NUM_SLICES'")
106+
107+
try:
108+
nrslices = int(values[1])
109+
except ValueError:
110+
raise argparse.ArgumentTypeError("NUM_SLICES is not an integer")
111+
if nrslices <= 0:
112+
raise argparse.ArgumentTypeError("NUM_SLICES is not a positive integer")
113+
114+
try:
115+
subslice = int(values[0])
116+
except ValueError:
117+
raise argparse.ArgumentTypeError("SLICE is not an integer")
118+
if subslice <= 0:
119+
raise argparse.ArgumentTypeError("SLICE is not a positive integer")
120+
if subslice > nrslices:
121+
raise argparse.ArgumentTypeError("SLICE exceeds NUM_SLICES")
122+
123+
return subslice, nrslices
124+
102125
# Note: when adding arguments, please also add them to the completion
103126
# scripts in $MESONSRC/data/shell-completions/
104127
def add_arguments(parser: argparse.ArgumentParser) -> None:
@@ -149,12 +172,13 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
149172
help='Arguments to pass to the specified test(s) or all tests')
150173
parser.add_argument('--max-lines', default=100, dest='max_lines', type=int,
151174
help='Maximum number of lines to show from a long test log. Since 1.5.0.')
175+
parser.add_argument('--slice', default=None, type=test_slice, metavar='SLICE/NUM_SLICES',
176+
help='Split tests into NUM_SLICES slices and execute slice SLICE. Since 1.7.0.')
152177
parser.add_argument('args', nargs='*',
153178
help='Optional list of test names to run. "testname" to run all tests with that name, '
154179
'"subprojname:testname" to specifically run "testname" from "subprojname", '
155180
'"subprojname:" to run all tests defined by "subprojname".')
156181

157-
158182
def print_safe(s: str) -> None:
159183
end = '' if s[-1] == '\n' else '\n'
160184
try:
@@ -1976,6 +2000,9 @@ def get_tests(self, errorfile: T.Optional[T.IO] = None) -> T.List[TestSerialisat
19762000
tests = [t for t in self.tests if self.test_suitable(t)]
19772001
if self.options.args:
19782002
tests = list(self.tests_from_args(tests))
2003+
if self.options.slice:
2004+
our_slice, nslices = self.options.slice
2005+
tests = tests[our_slice - 1::nslices]
19792006

19802007
if not tests:
19812008
print('No suitable tests defined.', file=errorfile)

Diff for: unittests/allplatformstests.py

+31
Original file line numberDiff line numberDiff line change
@@ -5076,6 +5076,37 @@ def test_c_cpp_stds(self):
50765076
self.setconf('-Dcpp_std=c++11,gnu++11,vc++11')
50775077
self.assertEqual(self.getconf('cpp_std'), 'c++11')
50785078

5079+
def test_slice(self):
5080+
testdir = os.path.join(self.unit_test_dir, '124 test slice')
5081+
self.init(testdir)
5082+
self.build()
5083+
5084+
for arg, expectation in {
5085+
'1/1': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
5086+
'1/2': [1, 3, 5, 7, 9],
5087+
'2/2': [2, 4, 6, 8, 10],
5088+
'1/10': [1],
5089+
'2/20': [2],
5090+
'10/20': [10],
5091+
'11/20': [ ],
5092+
}.items():
5093+
output = self._run(self.mtest_command + ['--slice=' + arg])
5094+
tests = sorted([ int(x[5:]) for x in re.findall(r'test-[0-9]*', output) ])
5095+
self.assertEqual(tests, expectation)
5096+
5097+
for arg, expectation in {
5098+
'': 'value does not conform to format \'SLICE/NUM_SLICES\'',
5099+
'0': 'value does not conform to format \'SLICE/NUM_SLICES\'',
5100+
'0/1': 'SLICE is not a positive integer',
5101+
'a/1': 'SLICE is not an integer',
5102+
'1/0': 'NUM_SLICES is not a positive integer',
5103+
'1/a': 'NUM_SLICES is not an integer',
5104+
'2/1': 'SLICE exceeds NUM_SLICES',
5105+
}.items():
5106+
with self.assertRaises(subprocess.CalledProcessError) as cm:
5107+
self._run(self.mtest_command + ['--slice=' + arg])
5108+
self.assertIn('error: argument --slice: ' + expectation, cm.exception.output)
5109+
50795110
def test_rsp_support(self):
50805111
env = get_fake_env()
50815112
cc = detect_c_compiler(env, MachineChoice.HOST)

0 commit comments

Comments
 (0)