aboutsummaryrefslogtreecommitdiff
path: root/meson
diff options
context:
space:
mode:
authorQyriad <qyriad@qyriad.me>2024-03-25 12:12:56 -0600
committerQyriad <qyriad@qyriad.me>2024-03-27 18:37:50 -0600
commit038daad2182a22c81861ee7cbb5f0c85f6bb16ba (patch)
tree653f303e26bd99106a4d39d5cb9f880d4ba0a0a3 /meson
parentedba570664b952facde43fd0414e60f0a42851da (diff)
meson: implement functional tests
Functional tests can be run with `meson test -C build --suite installcheck`. Notably, functional tests must be run *after* running `meson install` (Lix's derivation runs the installcheck suite in installCheckPhase so it does this correctly), due to some quirks between Meson and the testing system. As far as I can tell the functional tests are meant to be run after installing anyway, but unfortunately I can't transparently make `meson test --suite installcheck` depend on the install targets. The script that runs the functional tests, meson/run-test.py, checks that `meson install` has happened and fails fast with a (hopefully) helpful error message if any of the functional tests are run before installing. TODO: this change needs reflection in developer documentation Change-Id: I8dcb5fdfc0b6cb17580973d24ad930abd57018f6
Diffstat (limited to 'meson')
-rwxr-xr-xmeson/run-test.py87
-rwxr-xr-xmeson/setup-functional-tests.py105
2 files changed, 192 insertions, 0 deletions
diff --git a/meson/run-test.py b/meson/run-test.py
new file mode 100755
index 000000000..a6551ea7a
--- /dev/null
+++ b/meson/run-test.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+
+"""
+This script is a helper for this project's Meson buildsystem to run Lix's
+functional tests. It is an analogue to mk/run-test.sh in the autoconf+Make
+buildsystem.
+
+These tests are run in the installCheckPhase in Lix's derivation, and as such
+expect to be run after the project has already been "installed" to some extent.
+Look at meson/setup-functional-tests.py for more details.
+"""
+
+import argparse
+from pathlib import Path
+import os
+import shutil
+import subprocess
+import sys
+
+name = 'run-test.py'
+
+if 'MESON_BUILD_ROOT' not in os.environ:
+ raise ValueError(f'{name}: this script must be run from the Meson build system')
+
+def main():
+
+ tests_dir = Path(os.path.join(os.environ['MESON_BUILD_ROOT'], 'tests/functional'))
+
+ parser = argparse.ArgumentParser(name)
+ parser.add_argument('target', help='the script path relative to tests/functional to run')
+ args = parser.parse_args()
+
+ target = Path(args.target)
+ # The test suite considers the test's name to be the path to the test relative to
+ # `tests/functional`, but without the file extension.
+ # e.g. for `tests/functional/flakes/develop.sh`, the test name is `flakes/develop`
+ test_name = target.with_suffix('').as_posix()
+ if not target.is_absolute():
+ target = tests_dir.joinpath(target).resolve()
+
+ assert target.exists(), f'{name}: test {target} does not exist; did you run `meson install`?'
+
+ bash = os.environ.get('BASH', shutil.which('bash'))
+ if bash is None:
+ raise ValueError(f'{name}: bash executable not found and BASH environment variable not set')
+
+ test_environment = os.environ | {
+ 'TEST_NAME': test_name,
+ # mk/run-test.sh did this, but I don't know if it has any effect since it seems
+ # like the tests that interact with remote stores set it themselves?
+ 'NIX_REMOTE': '',
+ }
+
+ # Initialize testing.
+ init_result = subprocess.run([bash, '-e', 'init.sh'], cwd=tests_dir, env=test_environment)
+ if init_result.returncode != 0:
+ print(f'{name}: internal error initializing {args.target}', file=sys.stderr)
+ print('[ERROR]')
+ # Meson interprets exit code 99 as indicating an *error* in the testing process.
+ return 99
+
+ # Run the test itself.
+ test_result = subprocess.run([bash, '-e', target.name], cwd=target.parent, env=test_environment)
+
+ if test_result.returncode == 0:
+ print('[PASS]')
+ elif test_result.returncode == 99:
+ print('[SKIP]')
+ # Meson interprets exit code 77 as indicating a skipped test.
+ return 77
+ else:
+ print('[FAIL]')
+
+ return test_result.returncode
+
+try:
+ sys.exit(main())
+except AssertionError as e:
+ # This should mean that this test was run not-from-Meson, probably without
+ # having run `meson install` first, which is not an bug in this script.
+ print(e, file=sys.stderr)
+ sys.exit(99)
+except Exception as e:
+ print(f'{name}: INTERNAL ERROR running test ({sys.argv}): {e}', file=sys.stderr)
+ print(f'this is a bug in {name}')
+ sys.exit(99)
+
diff --git a/meson/setup-functional-tests.py b/meson/setup-functional-tests.py
new file mode 100755
index 000000000..344bdc92e
--- /dev/null
+++ b/meson/setup-functional-tests.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+
+"""
+So like. This script is cursed.
+It's a helper for this project's Meson buildsystem for Lix's functional tests.
+The functional tests are a bunch of bash scripts, that each expect to be run from the
+directory from the directory that that script is in, and also expect modifications to have
+happened to the source tree, and even splork files around. The last is against the spirit
+of Meson (and personally annoying), to have build processes that aren't self-contained in the
+out-of-source build directory, but more problematically we need configured files in the test
+tree.
+So. We copy the tests tree into the build directory.
+Meson doesn't have a good way of doing this natively -- the best you could do is subdir()
+into every directory in the tests tree and configure_file(copy : true) on every file,
+but this won't copy symlinks as symlinks, which we need since the test suite has, well,
+tests on symlinks.
+However, the functional tests are normally run during Lix's derivation's installCheckPhase,
+after Lix has already been "installed" somewhere. So in Meson we setup add this file as an
+install script and copy everything in tests/functional to the build directory, preserving
+things like symlinks, even broken ones (which are intentional).
+
+TODO(Qyriad): when we remove the old build system entirely, we can instead fix the tests.
+"""
+
+from pathlib import Path
+import os, os.path
+import shutil
+import sys
+import traceback
+
+name = 'setup-functional-tests.py'
+
+if 'MESON_SOURCE_ROOT' not in os.environ or 'MESON_BUILD_ROOT' not in os.environ:
+ raise ValueError(f'{name}: this script must be run from the Meson build system')
+
+print(f'{name}: mirroring tests/functional to build directory')
+
+tests_source = Path(os.environ['MESON_SOURCE_ROOT']) / 'tests/functional'
+tests_build = Path(os.environ['MESON_BUILD_ROOT']) / 'tests/functional'
+
+def main():
+
+ os.chdir(tests_build)
+
+ for src_dirpath, src_dirnames, src_filenames in os.walk(tests_source):
+ src_dirpath = Path(src_dirpath)
+ assert src_dirpath.is_absolute(), f'{src_dirpath=} is not absolute'
+
+ # os.walk() gives us the absolute path to the directory we're currently in as src_dirpath.
+ # We want to mirror from the perspective of `tests_source`.
+ rel_subdir = src_dirpath.relative_to(tests_source)
+ assert (not rel_subdir.is_absolute()), f'{rel_subdir=} is not relative'
+
+ # And then join that relative path on `tests_build` to get the absolute
+ # path in the build directory that corresponds to `src_dirpath`.
+ build_dirpath = tests_build / rel_subdir
+ assert build_dirpath.is_absolute(), f'{build_dirpath=} is not absolute'
+
+ # More concretely, for the test file tests/functional/ca/build.sh:
+ # - src_dirpath is `$MESON_SOURCE_ROOT/tests/functional/ca`
+ # - rel_subidr is `ca`
+ # - build_dirpath is `$MESON_BUILD_ROOT/tests/functional/ca`
+
+ # `src_dirname` are directories underneath `src_dirpath`, and will be relative
+ # to `src_dirpath`.
+ for src_dirname in src_dirnames:
+ # Take the name of the directory in the tests source and join it on `src_dirpath`
+ # to get the full path to this specific directory in the tests source.
+ src = src_dirpath / src_dirname
+ # If there isn't *something* here, then our logic is wrong.
+ # Path.exists(follow_symlinks=False) wasn't added until Python 3.12, so we use
+ # os.path.lexists() here.
+ assert os.path.lexists(src), f'{src=} does not exist'
+
+ # Take the name of this directory and join it on `build_dirpath` to get the full
+ # path to the directory in `build/tests/functional` that we need to create.
+ dest = build_dirpath / src_dirname
+ if src.is_symlink():
+ src_target = src.readlink()
+ dest.unlink(missing_ok=True)
+ dest.symlink_to(src_target)
+ else:
+ dest.mkdir(parents=True, exist_ok=True)
+
+ for src_filename in src_filenames:
+ # os.walk() should be giving us relative filenames.
+ # If it isn't, our path joins will be veeeery wrong.
+ assert (not Path(src_filename).is_absolute()), f'{src_filename=} is not relative'
+
+ src = src_dirpath / src_filename
+ dst = build_dirpath / src_filename
+ # Mildly misleading name -- unlink removes ordinary files as well as symlinks.
+ dst.unlink(missing_ok=True)
+ # shutil.copy2() best-effort preserves metadata.
+ shutil.copy2(src, dst, follow_symlinks=False)
+
+
+try:
+ sys.exit(main())
+except Exception as e:
+ # Any error is likely a bug in this script.
+ print(f'{name}: INTERNAL ERROR setting up functional tests: {e}', file=sys.stderr)
+ print(traceback.format_exc())
+ print(f'this is a bug in {name}')
+ sys.exit(1)