Add test support helpers + [integration] config/bwrap chain tests
tests/support/fixtures.{h,cpp}: find_busybox_fixture() (images/busybox.tar
relative to cwd, this project's own established manual-testing convention
-- nullopt if absent, so [net] tests can SKIP() rather than fail) and
ScratchXdgDirs, an RAII helper pointing XDG_CONFIG_HOME/XDG_STATE_HOME at a
fresh throwaway mkdtemp() directory for its lifetime, restoring the
previous environment and removing the directory on destruction -- so
integration tests that actually exercise config_file_path()/xdg_state_dir()
never touch the real developer's own config/state.
tests/integration/test_config_bwrap_chain.cpp: the user's own example --
write a config file, load it back, resolve a NamespaceConfig from it the
same way run_container() (commands.cpp) does, and confirm build_bwrap_args()'s
resulting argv actually reflects it (disabled unshare-net/unshare-uts never
requested; an all-default config matches the live host's own
detect_bwrap_unshare_args() probe exactly). Neither test mounts/runs
anything or needs privilege.
Real bug found via ~10-30 repeated combined [unit]+[integration] runs, not
assumed: ScratchXdgDirs's constructor built its mkdtemp() template vector
from two *separate* temporary std::string objects (`.begin()` off one,
`.end()` off the other) -- mixing iterators from different containers is
undefined behavior, here manifesting as an intermittent, heap-address-
dependent `std::length_error: cannot create std::vector larger than
max_size()` inside whichever test happened to run adjacent to it. Fixed by
using a single named string instance for both ends of the range; confirmed
clean across 30 repeated combined runs afterward, plus a full run as root.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
// [integration] (no net, no root): chains config_file.h's read/write with
|
||||
// bwrap.h's argv assembly -- write a config file, load it back, resolve a
|
||||
// NamespaceConfig from it the same way commands.cpp's run_container() does,
|
||||
// and confirm build_bwrap_args()'s resulting argv actually reflects it.
|
||||
// Neither step mounts/runs anything or needs any privilege -- build_bwrap_args()
|
||||
// is pure argv assembly, given a `root` that's just a string here, never
|
||||
// actually accessed.
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "bwrap.h"
|
||||
#include "config_file.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool contains(const std::vector<std::string>& argv, const std::string& flag) {
|
||||
return std::find(argv.begin(), argv.end(), flag) != argv.end();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("config file -> NamespaceConfig -> bwrap argv: disabled namespaces are never requested", "[integration]") {
|
||||
ScratchXdgDirs scratch;
|
||||
auto config_path = scratch.path() / "config.yaml";
|
||||
|
||||
AppConfig written;
|
||||
written.unshare_net = false;
|
||||
written.unshare_uts = false;
|
||||
REQUIRE(write_config_file(config_path, written));
|
||||
|
||||
auto loaded = load_config_file(config_path);
|
||||
REQUIRE(loaded.has_value());
|
||||
CHECK(loaded->unshare_net == std::optional<bool>(false));
|
||||
CHECK(loaded->unshare_uts == std::optional<bool>(false));
|
||||
|
||||
// Same resolution run_container() (commands.cpp) itself does: each
|
||||
// unshare-* key defaults to enabled when unset.
|
||||
NamespaceConfig namespace_config{
|
||||
loaded->unshare_user.value_or(true), loaded->unshare_ipc.value_or(true),
|
||||
loaded->unshare_pid.value_or(true), loaded->unshare_net.value_or(true),
|
||||
loaded->unshare_uts.value_or(true), loaded->unshare_cgroup.value_or(true),
|
||||
};
|
||||
CHECK(namespace_config.net == false);
|
||||
CHECK(namespace_config.uts == false);
|
||||
CHECK(namespace_config.user == true);
|
||||
|
||||
auto argv = build_bwrap_args("/fake/root", {"/bin/sh"}, {}, std::nullopt, std::nullopt, namespace_config, false);
|
||||
|
||||
CHECK_FALSE(contains(argv, "--unshare-net"));
|
||||
CHECK_FALSE(contains(argv, "--unshare-uts"));
|
||||
CHECK(contains(argv, "--bind"));
|
||||
CHECK(contains(argv, "/fake/root"));
|
||||
CHECK(contains(argv, "/bin/sh"));
|
||||
}
|
||||
|
||||
TEST_CASE("config file -> NamespaceConfig -> bwrap argv: default (unset) config matches real kernel support",
|
||||
"[integration]") {
|
||||
ScratchXdgDirs scratch;
|
||||
auto config_path = scratch.path() / "config.yaml";
|
||||
|
||||
// Nothing set -- write_config_file()/load_config_file() round-trip an
|
||||
// otherwise-empty AppConfig, so every unshare-* key comes back unset.
|
||||
REQUIRE(write_config_file(config_path, AppConfig{}));
|
||||
auto loaded = load_config_file(config_path);
|
||||
REQUIRE(loaded.has_value());
|
||||
CHECK_FALSE(loaded->unshare_net.has_value());
|
||||
|
||||
NamespaceConfig namespace_config{
|
||||
loaded->unshare_user.value_or(true), loaded->unshare_ipc.value_or(true),
|
||||
loaded->unshare_pid.value_or(true), loaded->unshare_net.value_or(true),
|
||||
loaded->unshare_uts.value_or(true), loaded->unshare_cgroup.value_or(true),
|
||||
};
|
||||
|
||||
auto argv = build_bwrap_args("/fake/root", {"true"}, {}, std::nullopt, std::nullopt, namespace_config, false);
|
||||
|
||||
// With every policy gate open, the only thing left restricting which
|
||||
// --unshare-xxx flags actually appear is real kernel support -- so the
|
||||
// resulting argv should exactly match detect_bwrap_unshare_args()'s
|
||||
// own live probe of this host.
|
||||
for (const auto& flag : detect_bwrap_unshare_args()) {
|
||||
CHECK(contains(argv, flag));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include "fixtures.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
std::optional<std::filesystem::path> find_busybox_fixture() {
|
||||
std::error_code ec;
|
||||
auto path = std::filesystem::current_path(ec) / "images" / "busybox.tar";
|
||||
if (ec || !std::filesystem::exists(path, ec) || ec) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<std::string> getenv_opt(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
if (!value) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::string(value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ScratchXdgDirs::ScratchXdgDirs() {
|
||||
// Real bug found by testing, not assumed: an earlier version built
|
||||
// this vector from `std::string("...").begin()` paired with a
|
||||
// *separate* `std::string("...").end()` -- two distinct temporary
|
||||
// objects, even though they held identical content. Mixing iterators
|
||||
// from two different containers when constructing a third is
|
||||
// undefined behavior; here it manifested as an intermittent (SSO/heap
|
||||
// address dependent, hence flaky) `std::length_error: cannot create
|
||||
// std::vector larger than max_size()` from a garbage begin/end
|
||||
// distance, confirmed via ~10 repeated runs of the [integration] tests
|
||||
// together before it reproduced. Fixed by using one string instance
|
||||
// for both ends of the range.
|
||||
std::string tmpl_str = "/tmp/slocker-lite-test-XXXXXX";
|
||||
std::vector<char> tmpl(tmpl_str.begin(), tmpl_str.end());
|
||||
tmpl.push_back('\0');
|
||||
char* created = mkdtemp(tmpl.data());
|
||||
path_ = created ? std::filesystem::path(created) : std::filesystem::temp_directory_path() / "slocker-lite-test";
|
||||
std::filesystem::create_directories(path_);
|
||||
|
||||
previous_config_home_ = getenv_opt("XDG_CONFIG_HOME");
|
||||
previous_state_home_ = getenv_opt("XDG_STATE_HOME");
|
||||
setenv("XDG_CONFIG_HOME", path_.c_str(), 1);
|
||||
setenv("XDG_STATE_HOME", path_.c_str(), 1);
|
||||
}
|
||||
|
||||
ScratchXdgDirs::~ScratchXdgDirs() {
|
||||
if (previous_config_home_) {
|
||||
setenv("XDG_CONFIG_HOME", previous_config_home_->c_str(), 1);
|
||||
} else {
|
||||
unsetenv("XDG_CONFIG_HOME");
|
||||
}
|
||||
if (previous_state_home_) {
|
||||
setenv("XDG_STATE_HOME", previous_state_home_->c_str(), 1);
|
||||
} else {
|
||||
unsetenv("XDG_STATE_HOME");
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(path_, ec);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
// 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
|
||||
// by the plain mount/unmount smoke test) for [integration][net] tests that
|
||||
// need to actually run a command inside a container. Searches
|
||||
// images/busybox.tar relative to the current working directory -- this
|
||||
// project's own established manual-testing convention (see images/ at the
|
||||
// repo root, gitignored). nullopt if not found; callers should SKIP()
|
||||
// rather than fail, since fetching one is optional -- see
|
||||
// tests/setup-tests.py.
|
||||
std::optional<std::filesystem::path> find_busybox_fixture();
|
||||
|
||||
// RAII scratch XDG_CONFIG_HOME/XDG_STATE_HOME: for its lifetime, both env
|
||||
// vars point at a fresh throwaway directory under /tmp, so
|
||||
// config_file_path()/xdg_state_dir() (config_file.cpp/pid_file.cpp)
|
||||
// resolve entirely under it instead of the real developer's own $HOME --
|
||||
// integration tests that actually mount/run something must never touch
|
||||
// real config/state. Restores whatever the two env vars were before (unset
|
||||
// if they were unset) and removes the scratch directory on destruction.
|
||||
class ScratchXdgDirs {
|
||||
public:
|
||||
ScratchXdgDirs();
|
||||
~ScratchXdgDirs();
|
||||
ScratchXdgDirs(const ScratchXdgDirs&) = delete;
|
||||
ScratchXdgDirs& operator=(const ScratchXdgDirs&) = delete;
|
||||
|
||||
const std::filesystem::path& path() const { return path_; }
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
std::optional<std::string> previous_config_home_;
|
||||
std::optional<std::string> previous_state_home_;
|
||||
};
|
||||
Reference in New Issue
Block a user