Repository: mesonbuild/meson

Issue:
access to exe_wrapper from test scripts
igt has some tests (in meson) that are shell scripts, but they need to invoke built binaries. So it needs some way to get at the exe_wrapper.


Discussion:
We could set `MESON_EXE_WRAPPER` to a value that is [shell-quoted](https://docs.python.org/3/library/shlex.html#shlex.quote). We already do this for `MESONINTROSPECT`, for instance.
in IGT's case the binaries are passed as arguments to the scripts, would it work to just wrap the binary in the exe_wrapper in that case?
We could allow you to get the `exe_wrapper` as a `ExternalProgram` object (which is represented as an array of strings), then the test target should see that in the list of args for the test and expand it correctly. So, f.ex.,

```meson
exe_wrapper = ['qemu-system-arm', '-vga', 'virtio']
```

```meson
exewrap = meson.get_exe_wrapper()
prog = find_program('test-script.sh')
test('name', prog, args : [exewrap, '-foo', '-bar'])
```

This will pass to `exe` the following arguments: `['qemu-system-arm', '-vga', 'virtio', '-foo', '-bar']`.

Would this work?
I think so. It would just be nice if it was transparent to avoid having to do:
```meson
exe_wrapper = []
if meson.has_exe_wrapper()
  exe_wrapper = meson.get_exe_wrapper()
endif
```
And then discovering that no one has added that to their meson script because they don't care about cross compiling. But doing it transparently would be hard to get right in every case I suspect.
Ah, so you want to detect when a built executable is passed to a test as an argument and automatically add the exe wrapper to it? That sounds like a good idea, and the implementation would be the same as the suggestion I commented above.

We should include a way to disable it somehow, though.
Another option is to always return the empty array in `get_exe_wrapper()` so you don't need to have that if condition.
yeah, but it still means that you have to write tests as:

```meson
test('foo', [meson.get_exe_wrapper(), binary])
```
to be cross safe. If we can we should make the barrier to writing tests that work with the exe_wrapper as low as we can safely.
> test('foo', [meson.get_exe_wrapper(), binary])

You don't need to do that, though. We automatically run cross binaries with the exe wrapper while running tests.
Err, there should have bine a `foo.sh` in there :)
Ah, yes, well we could make automatic exe wrapper addition a thing, yes. I think we should have a way to opt-in to changes like that, and a deprecation warning for people who don't opt-in, and then after a few releases we flip over.
Do we key things off minimum meson version? It would be very handy for deprocations
ie, opt in if minimum version >= version when this feature was added.
We could indeed do that. Now that I think of it, I suggested something like this a year ago but I never implemented it...
This would also be useful when using `add_test_setup()` or `meson test --wrap` while having tests that are invoked from a shell script (for example, I have a shell script that checks the exit code of my built binary is a certain value, if I wrap the tests with valgrind then I want to check for leaks in my built program and not my shell)
(Although we'd also need some way to tell Meson "don't wrap this test, I'll apply the wrapper inside my script", maybe an optional argument to `test()`)

Resolving patch:
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -119,7 +119,8 @@ def __init__(self, name: str, project: str, suite: str, fname: T.List[str],
                  needs_exe_wrapper: bool, is_parallel: bool, cmd_args: T.List[str],
                  env: build.EnvironmentVariables, should_fail: bool,
                  timeout: T.Optional[int], workdir: T.Optional[str],
-                 extra_paths: T.List[str], protocol: TestProtocol, priority: int):
+                 extra_paths: T.List[str], protocol: TestProtocol, priority: int,
+                 cmd_is_built: bool):
         self.name = name
         self.project_name = project
         self.suite = suite
@@ -138,6 +139,8 @@ def __init__(self, name: str, project: str, suite: str, fname: T.List[str],
         self.protocol = protocol
         self.priority = priority
         self.needs_exe_wrapper = needs_exe_wrapper
+        self.cmd_is_built = cmd_is_built
+
 
 def get_backend_from_name(backend: str, build: T.Optional[build.Build] = None, interpreter: T.Optional['Interpreter'] = None) -> T.Optional['Backend']:
     if backend == 'ninja':
@@ -788,6 +791,15 @@ def create_test_serialisation(self, tests):
                 # E.g. an external verifier or simulator program run on a generated executable.
                 # Can always be run without a wrapper.
                 test_for_machine = MachineChoice.BUILD
+
+            # we allow passing compiled executables to tests, which may be cross built.
+            # We need to consider these as well when considering whether the target is cross or not.
+            for a in t.cmd_args:
+                if isinstance(a, build.BuildTarget):
+                    if a.for_machine is MachineChoice.HOST:
+                        test_for_machine = MachineChoice.HOST
+                        break
+
             is_cross = self.environment.is_cross_build(test_for_machine)
             if is_cross and self.environment.need_exe_wrapper():
                 exe_wrapper = self.environment.get_exe_wrapper()
@@ -801,6 +813,7 @@ def create_test_serialisation(self, tests):
                 extra_paths = self.determine_windows_extra_paths(exe, extra_bdeps)
             else:
                 extra_paths = []
+
             cmd_args = []
             for a in unholder(t.cmd_args):
                 if isinstance(a, build.BuildTarget):
@@ -810,6 +823,11 @@ def create_test_serialisation(self, tests):
                     cmd_args.append(a)
                 elif isinstance(a, str):
                     cmd_args.append(a)
+                elif isinstance(a, build.Executable):
+                    p = self.construct_target_rel_path(a, t.workdir)
+                    if p == a.get_filename():
+                        p = './' + p
+                    cmd_args.append(p)
                 elif isinstance(a, build.Target):
                     cmd_args.append(self.construct_target_rel_path(a, t.workdir))
                 else:
@@ -818,7 +836,8 @@ def create_test_serialisation(self, tests):
                                    exe_wrapper, self.environment.need_exe_wrapper(),
                                    t.is_parallel, cmd_args, t.env,
                                    t.should_fail, t.timeout, t.workdir,
-                                   extra_paths, t.protocol, t.priority)
+                                   extra_paths, t.protocol, t.priority,
+                                   isinstance(exe, build.Executable))
             arr.append(ts)
         return arr
 
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
--- a/mesonbuild/mtest.py
+++ b/mesonbuild/mtest.py
@@ -42,7 +42,7 @@
 from . import environment
 from . import mlog
 from .dependencies import ExternalProgram
-from .mesonlib import MesonException, get_wine_shortpath, split_args
+from .mesonlib import MesonException, get_wine_shortpath, split_args, join_args
 from .backend.backends import TestProtocol
 
 if T.TYPE_CHECKING:
@@ -311,7 +311,7 @@ def parse(self) -> T.Generator[T.Union['TAPParser.Test', 'TAPParser.Error', 'TAP
                     yield self.Version(version=version)
                 continue
 
-            if len(line) == 0:
+            if not line:
                 continue
 
             yield self.Error('unexpected input at line {}'.format((lineno,)))
@@ -609,20 +609,20 @@ def _get_cmd(self) -> T.Optional[T.List[str]]:
             return ['java', '-jar'] + self.test.fname
         elif not self.test.is_cross_built and run_with_mono(self.test.fname[0]):
             return ['mono'] + self.test.fname
-        else:
-            if self.test.is_cross_built and self.test.needs_exe_wrapper:
-                if self.test.exe_runner is None:
-                    # Can not run test on cross compiled executable
-                    # because there is no execute wrapper.
-                    return None
-                else:
-                    if not self.test.exe_runner.found():
-                        msg = 'The exe_wrapper defined in the cross file {!r} was not ' \
-                              'found. Please check the command and/or add it to PATH.'
-                        raise TestException(msg.format(self.test.exe_runner.name))
-                    return self.test.exe_runner.get_command() + self.test.fname
-            else:
-                return self.test.fname
+        elif self.test.cmd_is_built and self.test.needs_exe_wrapper:
+            if self.test.exe_runner is None:
+                # Can not run test on cross compiled executable
+                # because there is no execute wrapper.
+                return None
+            elif self.test.cmd_is_built:
+                # If the command is not built (ie, its a python script),
+                # then we don't check for the exe-wrapper
+                if not self.test.exe_runner.found():
+                    msg = ('The exe_wrapper defined in the cross file {!r} was not '
+                           'found. Please check the command and/or add it to PATH.')
+                    raise TestException(msg.format(self.test.exe_runner.name))
+                return self.test.exe_runner.get_command() + self.test.fname
+        return self.test.fname
 
     def run(self) -> TestRun:
         cmd = self._get_cmd()
@@ -638,7 +638,7 @@ def run(self) -> TestRun:
     def _run_cmd(self, cmd: T.List[str]) -> TestRun:
         starttime = time.time()
 
-        if len(self.test.extra_paths) > 0:
+        if self.test.extra_paths:
             self.env['PATH'] = os.pathsep.join(self.test.extra_paths + ['']) + self.env['PATH']
             winecmd = []
             for c in cmd:
@@ -867,6 +867,9 @@ def get_test_runner(self, test: 'TestSerialisation') -> SingleTestRunner:
             env = os.environ.copy()
         test_env = test.env.get_env(env)
         env.update(test_env)
+        if (test.is_cross_built and test.needs_exe_wrapper and
+                test.exe_runner and test.exe_runner.found()):
+            env['MESON_EXE_WRAPPER'] = join_args(test.exe_runner.get_command())
         return SingleTestRunner(test, test_env, env, options)
 
     def process_test_result(self, result: TestRun) -> None:
@@ -941,7 +944,7 @@ def print_summary(self) -> None:
             self.junit.write()
 
     def print_collected_logs(self) -> None:
-        if len(self.collected_logs) > 0:
+        if self.collected_logs:
             if len(self.collected_logs) > 10:
                 print('\nThe output from 10 first failed tests:\n')
             else:
@@ -1023,7 +1026,7 @@ def get_tests(self) -> T.List['TestSerialisation']:
             print('No tests defined.')
             return []
 
-        if len(self.options.include_suites) or len(self.options.exclude_suites):
+        if self.options.include_suites or self.options.exclude_suites:
             tests = []
             for tst in self.tests:
                 if self.test_suitable(tst):
@@ -1085,7 +1088,7 @@ def get_pretty_suite(self, test: 'TestSerialisation') -> str:
         if len(self.suites) > 1 and test.suite:
             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):
+            if s:
                 rv += ":"
             return rv + s + " / " + test.name
         else:
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -40,6 +40,7 @@
 from glob import glob
 from pathlib import (PurePath, Path)
 from distutils.dir_util import copy_tree
+import typing
 
 import mesonbuild.mlog
 import mesonbuild.depfile
@@ -3420,67 +3421,6 @@ def test_templates(self):
                         f.write('public class Foo { public static void main() {} }')
                     self._run(self.meson_command + ['init', '-b'], workdir=tmpdir)
 
-    # The test uses mocking and thus requires that
-    # the current process is the one to run the Meson steps.
-    # If we are using an external test executable (most commonly
-    # in Debian autopkgtests) then the mocking won't work.
-    @unittest.skipIf('MESON_EXE' in os.environ, 'MESON_EXE is defined, can not use mocking.')
-    def test_cross_file_system_paths(self):
-        if is_windows():
-            raise unittest.SkipTest('system crossfile paths not defined for Windows (yet)')
-        if is_sunos():
-            cc = 'gcc'
-        else:
-            cc = 'cc'
-
-        testdir = os.path.join(self.common_test_dir, '1 trivial')
-        cross_content = textwrap.dedent("""\
-            [binaries]
-            c = '/usr/bin/{}'
-            ar = '/usr/bin/ar'
-            strip = '/usr/bin/ar'
-
-            [properties]
-
-            [host_machine]
-            system = 'linux'
-            cpu_family = 'x86'
-            cpu = 'i686'
-            endian = 'little'
-            """.format(cc))
-
-        with tempfile.TemporaryDirectory() as d:
-            dir_ = os.path.join(d, 'meson', 'cross')
-            os.makedirs(dir_)
-            with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f:
-                f.write(cross_content)
-            name = os.path.basename(f.name)
-
-            with mock.patch.dict(os.environ, {'XDG_DATA_HOME': d}):
-                self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
-                self.wipe()
-
-            with mock.patch.dict(os.environ, {'XDG_DATA_DIRS': d}):
-                os.environ.pop('XDG_DATA_HOME', None)
-                self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
-                self.wipe()
-
-        with tempfile.TemporaryDirectory() as d:
-            dir_ = os.path.join(d, '.local', 'share', 'meson', 'cross')
-            os.makedirs(dir_)
-            with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f:
-                f.write(cross_content)
-            name = os.path.basename(f.name)
-
-            # If XDG_DATA_HOME is set in the environment running the
-            # tests this test will fail, os mock the environment, pop
-            # it, then test
-            with mock.patch.dict(os.environ):
-                os.environ.pop('XDG_DATA_HOME', None)
-                with mock.patch('mesonbuild.coredata.os.path.expanduser', lambda x: x.replace('~', d)):
-                    self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
-                    self.wipe()
-
     def test_compiler_run_command(self):
         '''
         The test checks that the compiler object can be passed to
@@ -4536,7 +4476,7 @@ def test_configure(self):
         self._run(self.mconf_command + [self.builddir])
 
     def test_summary(self):
-        testdir = os.path.join(self.unit_test_dir, '72 summary')
+        testdir = os.path.join(self.unit_test_dir, '73 summary')
         out = self.init(testdir)
         expected = textwrap.dedent(r'''
             Some Subproject 2.0
@@ -4590,7 +4530,7 @@ def test_meson_compile(self):
         self.assertPathDoesNotExist(os.path.join(self.builddir, prog))
 
     def test_spurious_reconfigure_built_dep_file(self):
-        testdir = os.path.join(self.unit_test_dir, '74 dep files')
+        testdir = os.path.join(self.unit_test_dir, '75 dep files')
 
         # Regression test: Spurious reconfigure was happening when build
         # directory is inside source directory.
@@ -6690,7 +6630,7 @@ def compute_sha256(self, filename):
             return hashlib.sha256(f.read()).hexdigest()
 
     def test_wrap_with_file_url(self):
-        testdir = os.path.join(self.unit_test_dir, '73 wrap file url')
+        testdir = os.path.join(self.unit_test_dir, '74 wrap file url')
         source_filename = os.path.join(testdir, 'subprojects', 'foo.tar.xz')
         patch_filename = os.path.join(testdir, 'subprojects', 'foo-patch.tar.xz')
         wrap_filename = os.path.join(testdir, 'subprojects', 'foo.wrap')
@@ -6781,7 +6721,7 @@ def test_cross_libdir(self):
     def test_cross_libdir_subproject(self):
         # Guard against a regression where calling "subproject"
         # would reset the value of libdir to its default value.
-        testdir = os.path.join(self.unit_test_dir, '75 subdir libdir')
+        testdir = os.path.join(self.unit_test_dir, '76 subdir libdir')
         self.init(testdir, extra_args=['--libdir=fuf'])
         for i in self.introspect('--buildoptions'):
             if i['name'] == 'libdir':
@@ -7606,6 +7546,134 @@ class CrossFileTests(BasePlatformTests):
     This is mainly aimed to testing overrides from cross files.
     """
 
+    def _cross_file_generator(self, *, needs_exe_wrapper: bool = False,
+                              exe_wrapper: typing.Optional[typing.List[str]] = None) -> str:
+        if is_windows():
+            raise unittest.SkipTest('Cannot run this test on non-mingw/non-cygwin windows')
+        if is_sunos():
+            cc = 'gcc'
+        else:
+            cc = 'cc'
+
+        return textwrap.dedent("""\
+            [binaries]
+            c = '/usr/bin/{}'
+            ar = '/usr/bin/ar'
+            strip = '/usr/bin/ar'
+            {}
+
+            [properties]
+            needs_exe_wrapper = {}
+
+            [host_machine]
+            system = 'linux'
+            cpu_family = 'x86'
+            cpu = 'i686'
+            endian = 'little'
+            """.format(cc,
+                       'exe_wrapper = {}'.format(str(exe_wrapper)) if exe_wrapper is not None else '',
+                       needs_exe_wrapper))
+
+    def _stub_exe_wrapper(self) -> str:
+        return textwrap.dedent('''\
+            #!/usr/bin/env python3
+            import subprocess
+            import sys
+
+            sys.exit(subprocess.run(sys.argv[1:]).returncode)
+            ''')
+
+    def test_needs_exe_wrapper_true(self):
+        testdir = os.path.join(self.unit_test_dir, '72 cross test passed')
+        with tempfile.TemporaryDirectory() as d:
+            p = Path(d) / 'crossfile'
+            with p.open('wt') as f:
+                f.write(self._cross_file_generator(needs_exe_wrapper=True))
+            self.init(testdir, extra_args=['--cross-file=' + str(p)])
+            out = self.run_target('test')
+            self.assertRegex(out, r'Skipped:\s*1\s*\n')
+
+    def test_needs_exe_wrapper_false(self):
+        testdir = os.path.join(self.unit_test_dir, '72 cross test passed')
+        with tempfile.TemporaryDirectory() as d:
+            p = Path(d) / 'crossfile'
+            with p.open('wt') as f:
+                f.write(self._cross_file_generator(needs_exe_wrapper=False))
+            self.init(testdir, extra_args=['--cross-file=' + str(p)])
+            out = self.run_target('test')
+            self.assertNotRegex(out, r'Skipped:\s*1\n')
+
+    def test_needs_exe_wrapper_true_wrapper(self):
+        testdir = os.path.join(self.unit_test_dir, '72 cross test passed')
+        with tempfile.TemporaryDirectory() as d:
+            s = Path(d) / 'wrapper.py'
+            with s.open('wt') as f:
+                f.write(self._stub_exe_wrapper())
+            s.chmod(0o774)
+            p = Path(d) / 'crossfile'
+            with p.open('wt') as f:
+                f.write(self._cross_file_generator(
+                    needs_exe_wrapper=True,
+                    exe_wrapper=[str(s)]))
+
+            self.init(testdir, extra_args=['--cross-file=' + str(p), '-Dexpect=true'])
+            out = self.run_target('test')
+            self.assertRegex(out, r'Ok:\s*3\s*\n')
+
+    def test_cross_exe_passed_no_wrapper(self):
+        testdir = os.path.join(self.unit_test_dir, '72 cross test passed')
+        with tempfile.TemporaryDirectory() as d:
+            p = Path(d) / 'crossfile'
+            with p.open('wt') as f:
+                f.write(self._cross_file_generator(needs_exe_wrapper=True))
+
+            self.init(testdir, extra_args=['--cross-file=' + str(p)])
+            self.build()
+            out = self.run_target('test')
+            self.assertRegex(out, r'Skipped:\s*1\s*\n')
+
+    # The test uses mocking and thus requires that the current process is the
+    # one to run the Meson steps. If we are using an external test executable
+    # (most commonly in Debian autopkgtests) then the mocking won't work.
+    @unittest.skipIf('MESON_EXE' in os.environ, 'MESON_EXE is defined, can not use mocking.')
+    def test_cross_file_system_paths(self):
+        if is_windows():
+            raise unittest.SkipTest('system crossfile paths not defined for Windows (yet)')
+
+        testdir = os.path.join(self.common_test_dir, '1 trivial')
+        cross_content = self._cross_file_generator()
+        with tempfile.TemporaryDirectory() as d:
+            dir_ = os.path.join(d, 'meson', 'cross')
+            os.makedirs(dir_)
+            with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f:
+                f.write(cross_content)
+            name = os.path.basename(f.name)
+
+            with mock.patch.dict(os.environ, {'XDG_DATA_HOME': d}):
+                self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
+                self.wipe()
+
+            with mock.patch.dict(os.environ, {'XDG_DATA_DIRS': d}):
+                os.environ.pop('XDG_DATA_HOME', None)
+                self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
+                self.wipe()
+
+        with tempfile.TemporaryDirectory() as d:
+            dir_ = os.path.join(d, '.local', 'share', 'meson', 'cross')
+            os.makedirs(dir_)
+            with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f:
+                f.write(cross_content)
+            name = os.path.basename(f.name)
+
+            # If XDG_DATA_HOME is set in the environment running the
+            # tests this test will fail, os mock the environment, pop
+            # it, then test
+            with mock.patch.dict(os.environ):
+                os.environ.pop('XDG_DATA_HOME', None)
+                with mock.patch('mesonbuild.coredata.os.path.expanduser', lambda x: x.replace('~', d)):
+                    self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
+                    self.wipe()
+
     def test_cross_file_dirs(self):
         testcase = os.path.join(self.unit_test_dir, '60 native file override')
         self.init(testcase, default_args=False,


Test patch:
diff --git a/docs/markdown/snippets/exe_wrapper_for_cross_built_tests.md b/docs/markdown/snippets/exe_wrapper_for_cross_built_tests.md
new file mode 100644
--- /dev/null
+++ b/docs/markdown/snippets/exe_wrapper_for_cross_built_tests.md
@@ -0,0 +1,9 @@
+## Test scripts are given the exe wrapper if needed
+
+Meson will now set the `MESON_EXE_WRAPPER` as the properly wrapped and joined
+representation. For Unix-like OSes this means python's shelx.join, on Windows
+an implementation that attempts to properly quote windows argument is used.
+This allow wrapper scripts to run test binaries, instead of just skipping.
+
+for example, if the wrapper is `['emulator', '--script']`, it will be passed
+as `MESON_EXE_WRAPPER="emulator --script"`.
diff --git a/test cases/unit/72 cross test passed/exewrapper.py b/test cases/unit/72 cross test passed/exewrapper.py
new file mode 100755
--- /dev/null
+++ b/test cases/unit/72 cross test passed/exewrapper.py	
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+# Test that the MESON_EXE_WRAPPER environment variable is set
+
+import argparse
+import os
+import sys
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('binary')  # unused, but needed for test behavior
+    parser.add_argument('--expected', action='store_true')
+    args = parser.parse_args()
+
+    defined = 'MESON_EXE_WRAPPER' in os.environ
+
+    if args.expected != defined:
+        print(os.environ, file=sys.stderr)
+        return 1
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/test cases/unit/72 cross test passed/meson.build b/test cases/unit/72 cross test passed/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/unit/72 cross test passed/meson.build	
@@ -0,0 +1,19 @@
+project(
+  'cross test passed',
+  'c',
+  version : '>= 0.51'
+)
+
+e = executable('exec', 'src/main.c')
+
+py = import('python').find_installation()
+
+test('root', e)
+test('main', py, args : [meson.current_source_dir() / 'script.py', e])
+
+wrapper_args = []
+if get_option('expect')
+  wrapper_args += '--expected'
+endif
+
+test('exe_wrapper in env', py, args : [meson.current_source_dir() / 'exewrapper.py', e, wrapper_args])
diff --git a/test cases/unit/72 cross test passed/meson_options.txt b/test cases/unit/72 cross test passed/meson_options.txt
new file mode 100644
--- /dev/null
+++ b/test cases/unit/72 cross test passed/meson_options.txt	
@@ -0,0 +1,5 @@
+option(
+    'expect',
+    type : 'boolean',
+    value : false,
+)
diff --git a/test cases/unit/72 cross test passed/script.py b/test cases/unit/72 cross test passed/script.py
new file mode 100644
--- /dev/null
+++ b/test cases/unit/72 cross test passed/script.py	
@@ -0,0 +1,7 @@
+#!/usr/bin/env python3
+
+import subprocess
+import sys
+
+if __name__ == "__main__":
+    sys.exit(subprocess.run(sys.argv[1:]).returncode)
diff --git a/test cases/unit/72 cross test passed/src/main.c b/test cases/unit/72 cross test passed/src/main.c
new file mode 100644
--- /dev/null
+++ b/test cases/unit/72 cross test passed/src/main.c	
@@ -0,0 +1,6 @@
+#include <stdio.h>
+
+int main(int argc, char const *argv[])
+{
+    return 0;
+}
diff --git a/test cases/unit/72 summary/meson.build b/test cases/unit/73 summary/meson.build
similarity index 100%
rename from test cases/unit/72 summary/meson.build
rename to test cases/unit/73 summary/meson.build
diff --git a/test cases/unit/72 summary/subprojects/sub/meson.build b/test cases/unit/73 summary/subprojects/sub/meson.build
similarity index 100%
rename from test cases/unit/72 summary/subprojects/sub/meson.build
rename to test cases/unit/73 summary/subprojects/sub/meson.build
diff --git a/test cases/unit/72 summary/subprojects/sub2/meson.build b/test cases/unit/73 summary/subprojects/sub2/meson.build
similarity index 100%
rename from test cases/unit/72 summary/subprojects/sub2/meson.build
rename to test cases/unit/73 summary/subprojects/sub2/meson.build
diff --git a/test cases/unit/73 wrap file url/meson.build b/test cases/unit/74 wrap file url/meson.build
similarity index 100%
rename from test cases/unit/73 wrap file url/meson.build
rename to test cases/unit/74 wrap file url/meson.build
diff --git a/test cases/unit/73 wrap file url/subprojects/foo-patch.tar.xz b/test cases/unit/74 wrap file url/subprojects/foo-patch.tar.xz
similarity index 100%
rename from test cases/unit/73 wrap file url/subprojects/foo-patch.tar.xz
rename to test cases/unit/74 wrap file url/subprojects/foo-patch.tar.xz
diff --git a/test cases/unit/73 wrap file url/subprojects/foo.tar.xz b/test cases/unit/74 wrap file url/subprojects/foo.tar.xz
similarity index 100%
rename from test cases/unit/73 wrap file url/subprojects/foo.tar.xz
rename to test cases/unit/74 wrap file url/subprojects/foo.tar.xz
diff --git a/test cases/unit/74 dep files/foo.c b/test cases/unit/75 dep files/foo.c
similarity index 100%
rename from test cases/unit/74 dep files/foo.c
rename to test cases/unit/75 dep files/foo.c
diff --git a/test cases/unit/74 dep files/meson.build b/test cases/unit/75 dep files/meson.build
similarity index 100%
rename from test cases/unit/74 dep files/meson.build
rename to test cases/unit/75 dep files/meson.build
diff --git a/test cases/unit/75 subdir libdir/meson.build b/test cases/unit/76 subdir libdir/meson.build
similarity index 100%
rename from test cases/unit/75 subdir libdir/meson.build
rename to test cases/unit/76 subdir libdir/meson.build
diff --git a/test cases/unit/75 subdir libdir/subprojects/flub/meson.build b/test cases/unit/76 subdir libdir/subprojects/flub/meson.build
similarity index 100%
rename from test cases/unit/75 subdir libdir/subprojects/flub/meson.build
rename to test cases/unit/76 subdir libdir/subprojects/flub/meson.build