Wire -c/--config-file's effective config into the self-test suite

run_self_tests() now takes the same effective AppConfig any other command
gets and stashes it into a new g_test_app_config global (tests/support/
fixtures.h) before Catch2 runs anything. test_rootless_run.cpp's own
run_in_fixture() reads a copy of it instead of a hardcoded default
AppConfig{}, so -c actually reaches that test's container creation.

Verified: a config with every unshare-*/with-* key set to false, used via
-c <file> -t -- "[integration][net]~[root]", flips all 3 of that file's
container-creating tests to failing -- including the nohup-straggler
regression test, since with neither a pid namespace nor a cgroup nothing
reaps the backgrounded process -- while [unit] and non-networking
[integration] tests are completely unaffected, as expected.
This commit is contained in:
2026-09-05 10:02:12 +00:00
parent a22024fcf8
commit 4dd0f462a5
7 changed files with 101 additions and 24 deletions
+55 -19
View File
@@ -355,28 +355,52 @@ Source layout (all under `src/`):
right after the `stop_tap_relay()` loop) for `--clean-processes`'s own right after the `stop_tap_relay()` loop) for `--clean-processes`'s own
crash-orphan sweep (`network_tap_relay.h`'s `clean_stale_tap_relays()`, crash-orphan sweep (`network_tap_relay.h`'s `clean_stale_tap_relays()`,
see below). see below).
- `self_test.{h,cpp}``run_self_tests(args)` implements `-t/--test` - `self_test.{h,cpp}``run_self_tests(args, config)` implements
(`args` is `ParsedArgs::test_args`, `cli_args.h` — everything on the `-t/--test` (`args` is `ParsedArgs::test_args`, `cli_args.h` — everything
command line after `-t`, captured the same trailing-argv way `-r`/`-x` on the command line after `-t`, captured the same trailing-argv way
capture their own command). Purely plumbing: builds a synthetic argv `-r`/`-x` capture their own command; `config` is the same effective,
(`{"slocker-lite -t"} + args`) and hands it straight to Catch2's `-c/--config-file`-resolved `AppConfig` any other command gets, threaded
`Catch::Session().run(argc, argv)` — no test logic of its own lives here through from `dispatch_command()`'s own `Mode::test` case). Mostly
at all, that's all under `tests/` (see below); this file is `#if plumbing: builds a synthetic argv (`{"slocker-lite -t"} + args`) and
hands it straight to Catch2's `Catch::Session().run(argc, argv)` — no
test logic of its own lives here at all, that's all under `tests/` (see
below) — but first stashes `config` into `tests/support/fixtures.h`'s
own `g_test_app_config` global, *before* Catch2 ever runs a single
`TEST_CASE`. **Why**: without this, test code that creates a real
container (`test_rootless_run.cpp`'s own `run_in_fixture()`, below) had
no way to reflect `-c`'s settings at all — it always built a fresh,
hardcoded default `AppConfig{}` for every container regardless of what
`-c`/the real config file said, since Catch2 `TEST_CASE`s are just plain
functions with no way to receive parameters from the harness that invoked
them. Confirmed by direct testing (per the user's own request): a
hand-written config with every `unshare-*`/`with-*` key set to `false`,
used via `-c <that file> -t -- "[integration][net]~[root]"`, correctly
flips all 3 of `test_rootless_run.cpp`'s container-creating tests to
failing (the two namespace-isolation checks, since nothing's actually
isolated anymore, and the nohup-straggler regression test, since with no
pid namespace *and* no cgroup — rootless, on this dev machine — nothing
reaps the backgrounded process) while leaving every other category
(`[unit]`, `[integration]~[net]`) completely unaffected, exactly as
expected. This file is `#if
ENABLE_TESTS`-guarded (`config.h`, from Meson's `enable_tests` option, ENABLE_TESTS`-guarded (`config.h`, from Meson's `enable_tests` option,
default on — the same macro that already gated whether `catch2_dep` gets default on — the same macro that already gated whether `catch2_dep` gets
linked at all) so a `-Denable_tests=false` build prints a clear "not linked at all) so a `-Denable_tests=false` build prints a clear "not
compiled into this build" message and returns nonzero instead of compiled into this build" message and returns nonzero instead of
failing to link. `-t`'s own leftover-args capture requires a literal failing to link — the `#include "fixtures.h"`/`g_test_app_config` usage
`--` before any Catch2 option that looks like one of slocker-lite's own is confined to the `#if ENABLE_TESTS` branch specifically because
(`-r/--reporter` collides with `-r/--run``-c/--section` no longer `tests/support/fixtures.cpp` (where it's defined) is itself only
collides with anything now that `--cleanup` dropped its own `-c` short compiled into the binary when `enable_tests` is on (`meson.build`'s
form) — a bare tag expression like `-t -- "[unit]"` needs it `test_sources`), so referencing it unconditionally would break that
too by convention, though `getopt_long`'s own permutation happens to let build with a link error. `-t`'s own leftover-args capture requires a
a `-t "[unit]"` without `--` work anyway, since `"[unit]"` doesn't start literal `--` before any Catch2 option that looks like one of
with `-`. Distinct from the Meson-driven fixture smoke test under slocker-lite's own (`-r/--reporter` collides with `-r/--run`;
`tests/` (`tests/gen_fixture.py`/`tests/run_test.py`, described in `-c/--section` again collides too, now with `-c/--config-file`) — a bare
"Build & test commands" below), which stays a separate, always-on, tag expression like `-t -- "[unit]"` needs it too by convention, though
Python-driven mount/unmount/cleanup check. `getopt_long`'s own permutation happens to let a `-t "[unit]"` without
`--` work anyway, since `"[unit]"` doesn't start with `-`. Distinct from
the Meson-driven fixture smoke test under `tests/` (`tests/gen_fixture.py`/
`tests/run_test.py`, described in "Build & test commands" below), which
stays a separate, always-on, Python-driven mount/unmount/cleanup check.
**Test organization under `tests/`** (all in-process — every category **Test organization under `tests/`** (all in-process — every category
calls this project's own already-header-exposed functions directly, no calls this project's own already-header-exposed functions directly, no
@@ -500,7 +524,19 @@ Source layout (all under `src/`):
expected shutdown `SIGTERM` via the inherited handler and report a expected shutdown `SIGTERM` via the inherited handler and report a
spurious failure) — fixed the same way, resetting `SIGTERM` to spurious failure) — fixed the same way, resetting `SIGTERM` to
`SIG_DFL` right before forking the straggler. `SIG_DFL` right before forking the straggler.
- `tests/support/fixtures.{h,cpp}``find_busybox_fixture()` (searches - `tests/support/fixtures.{h,cpp}``g_test_app_config` (a plain `AppConfig`
global, default-constructed) is set once by `run_self_tests()`
(`self_test.cpp`, see above) from the current `-t` run's own effective,
possibly `-c/--config-file`-resolved config, before Catch2 runs anything;
`test_rootless_run.cpp`'s own `run_in_fixture()` reads a copy of it for
every container it creates instead of a hardcoded default, so `-c`
actually reaches that test's own namespace-policy resolution. Declared
here (not `self_test.{h,cpp}`) specifically because `self_test.cpp` is
always compiled (test or not), while this file — and thus this
global's actual definition — only exists in the binary at all when
`enable_tests` is on; `self_test.cpp`'s own `#include "fixtures.h"`/
write to it is confined to its `#if ENABLE_TESTS` branch for exactly
that reason. `find_busybox_fixture()` (searches
`images/busybox.tar` relative to cwd, this project's own established `images/busybox.tar` relative to cwd, this project's own established
manual-testing convention; `nullopt` if absent, so `[net]` tests manual-testing convention; `nullopt` if absent, so `[net]` tests
`SKIP()` rather than fail — see `tests/setup-tests.py`, below), `SKIP()` rather than fail — see `tests/setup-tests.py`, below),
+1 -1
View File
@@ -889,7 +889,7 @@ int dispatch_command(const ParsedArgs& args, const std::filesystem::path& config
case Mode::clean_processes: case Mode::clean_processes:
return clean_processes_command(); return clean_processes_command();
case Mode::test: case Mode::test:
return run_self_tests(args.test_args); return run_self_tests(args.test_args, config);
case Mode::exec: case Mode::exec:
return exec_in_session(*args.exec_pid, args.command, args.user_flag, args.group_flag); return exec_in_session(*args.exec_pid, args.command, args.user_flag, args.group_flag);
case Mode::kill: case Mode::kill:
+11 -1
View File
@@ -20,12 +20,21 @@
#if ENABLE_TESTS #if ENABLE_TESTS
#include <catch2/catch_session.hpp> #include <catch2/catch_session.hpp>
#include "fixtures.h"
#else #else
#include <fmt/core.h> #include <fmt/core.h>
#endif #endif
int run_self_tests(const std::vector<std::string>& args) { int run_self_tests(const std::vector<std::string>& args, const AppConfig& config) {
#if ENABLE_TESTS #if ENABLE_TESTS
// Stashed into a shared test-support global (tests/support/fixtures.h)
// *before* Catch2 ever runs a TEST_CASE, so test code that creates a
// real container (test_rootless_run.cpp's own run_in_fixture()) can
// consult the same effective, -c/--config-file-resolved settings a
// real -r/--run invocation would, instead of hardcoded defaults.
g_test_app_config = config;
// args[0] is conventionally the program name in Catch2's eyes (used in // args[0] is conventionally the program name in Catch2's eyes (used in
// some of its own diagnostic/help output) -- it never sees the real // some of its own diagnostic/help output) -- it never sees the real
// argv[0], since ParsedArgs::test_args only ever holds what came after // argv[0], since ParsedArgs::test_args only ever holds what came after
@@ -45,6 +54,7 @@ int run_self_tests(const std::vector<std::string>& args) {
return session.run(static_cast<int>(argv.size()), argv.data()); return session.run(static_cast<int>(argv.size()), argv.data());
#else #else
(void)args; (void)args;
(void)config;
fmt::print(stderr, fmt::print(stderr,
"tests were not compiled into this build (reconfigure with " "tests were not compiled into this build (reconfigure with "
"-Denable_tests=true and rebuild)\n"); "-Denable_tests=true and rebuild)\n");
+12 -2
View File
@@ -19,16 +19,26 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "config_file.h"
// Implements -t/--test, this project's own built-in Catch2-driven test // Implements -t/--test, this project's own built-in Catch2-driven test
// suite (distinct from the Meson-driven fixture smoke test under tests/, // suite (distinct from the Meson-driven fixture smoke test under tests/,
// which stays a separate, always-on mount/unmount/cleanup check). // which stays a separate, always-on mount/unmount/cleanup check).
// `args` is `ParsedArgs::test_args` (cli_args.h) -- everything on the // `args` is `ParsedArgs::test_args` (cli_args.h) -- everything on the
// command line after `-t`, forwarded to Catch2's own Session::run() // command line after `-t`, forwarded to Catch2's own Session::run()
// unmodified (tag expressions, --list-tests, --reporter, etc.); a bare // unmodified (tag expressions, --list-tests, --reporter, etc.); a bare
// `-t` (empty `args`) runs every registered TEST_CASE. Only compiled to // `-t` (empty `args`) runs every registered TEST_CASE. `config` is the
// same effective (`-c/--config-file`-resolved) AppConfig any other command
// gets, given to test code so a test that creates a real container can
// reflect the same global settings a real `-r/--run` would (see
// `tests/support/fixtures.h`'s `g_test_app_config`, which this stashes it
// into) -- letting e.g. `-c <all-unshare-off.yaml> -t -- "[integration][net]"`
// exercise what happens with pid/cgroup isolation actually disabled,
// instead of every test always creating containers under hardcoded
// defaults regardless of `-c`. Only compiled to
// actually run Catch2 when this build has ENABLE_TESTS set (config.h, // actually run Catch2 when this build has ENABLE_TESTS set (config.h,
// from Meson's `enable_tests` option, default on) -- otherwise prints a // from Meson's `enable_tests` option, default on) -- otherwise prints a
// clear "not compiled into this build" message and returns nonzero, // clear "not compiled into this build" message and returns nonzero,
// since a -Denable_tests=false build has no TEST_CASEs (or Catch2 itself) // since a -Denable_tests=false build has no TEST_CASEs (or Catch2 itself)
// linked in at all. // linked in at all.
int run_self_tests(const std::vector<std::string>& args); int run_self_tests(const std::vector<std::string>& args, const AppConfig& config);
+6 -1
View File
@@ -125,7 +125,12 @@ std::string run_in_fixture(const std::filesystem::path& image, const std::vector
args.mode_arg = image.string(); args.mode_arg = image.string();
args.command = command; args.command = command;
AppConfig config; // A fresh copy of the current -t run's effective config (g_test_app_config,
// fixtures.h -- set once by run_self_tests() from whatever main() resolved,
// respecting -c/--config-file) rather than a hardcoded AppConfig{}, so this
// helper's containers reflect the same global settings a real -r/--run
// invocation would.
AppConfig config = g_test_app_config;
CapturedStdout capture; CapturedStdout capture;
// config_path is only ever consulted by modes that read/write a config // config_path is only ever consulted by modes that read/write a config
// file (-w/--write-config, -v/--volume, -n/--network); Mode::run // file (-w/--write-config, -v/--volume, -n/--network); Mode::run
+2
View File
@@ -26,6 +26,8 @@
#include <system_error> #include <system_error>
#include <vector> #include <vector>
AppConfig g_test_app_config;
std::optional<std::filesystem::path> find_busybox_fixture() { std::optional<std::filesystem::path> find_busybox_fixture() {
std::error_code ec; std::error_code ec;
auto path = std::filesystem::current_path(ec) / "images" / "busybox.tar"; auto path = std::filesystem::current_path(ec) / "images" / "busybox.tar";
+14
View File
@@ -20,6 +20,20 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include "config_file.h"
// The effective AppConfig for the current -t/--test run -- set once by
// run_self_tests() (self_test.cpp) from whatever main() resolved (respecting
// -c/--config-file, same as any other command), before Catch2 ever runs a
// TEST_CASE. Test code that creates a real container (test_rootless_run.cpp's
// own run_in_fixture()) reads this instead of hardcoding a fresh, default
// AppConfig{}, so e.g. `-c <all-unshare-off.yaml> -t -- "[integration][net]"`
// actually exercises what happens with pid/cgroup isolation disabled. Plain
// default-constructed AppConfig{} otherwise (matching every test's prior,
// unconditional behavior) -- a bare `-t` with no `-c` and no real config file
// changes nothing.
extern AppConfig g_test_app_config;
// Path to a real, runnable OCI Image Layout tar (something with an actual // Path to a real, runnable OCI Image Layout tar (something with an actual
// /bin/sh, unlike tests/gen_fixture.py's minimal single-file fixture used // /bin/sh, unlike tests/gen_fixture.py's minimal single-file fixture used
// by the plain mount/unmount smoke test) for [integration][net] tests that // by the plain mount/unmount smoke test) for [integration][net] tests that