Files
ceamac f9e68d48d9 Add intern-network IP ping tests (veth + tap+relay variants)
First of a planned series of end-to-end -n/--network join tests
(tests/integration/test_network_join_scenarios.cpp, [integration][root][net]):
two containers joined to the same intern network ping each other by IP,
run once with a real veth pair and once forced onto the tap+relay
fallback, since the two are genuinely different implementations. IPv6 is
deliberately excluded pending a known device-specific peculiarity.

split_lines_trimmed()/extract_marked_lines() moved from
test_rootless_run.cpp into tests/support/fixtures.{h,cpp} for reuse here.

wait_for_eth0_then() wraps a sandboxed command's own network-touching
script in a poll for eth0 to exist first: join_networks() runs
concurrently with, not before, the sandboxed command starting, so a
near-instant command can otherwise exit before its own join finishes --
the exact limitation already documented in network_join.{h,cpp}'s own
CLAUDE.md entry. Confirmed by testing (not assumed): without this, the
container's own immediate ping-and-exit sometimes raced ahead of the
veth-move step, which then failed outright ("Invalid netns value")
against an already-exited pid.
2026-09-05 11:38:21 +00:00

150 lines
4.8 KiB
C++

// 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 <fcntl.h>
#include <unistd.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <system_error>
#include <vector>
AppConfig g_test_app_config;
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);
}
CapturedStdout::CapturedStdout() {
std::string tmpl_str = "/tmp/slocker-lite-test-stdout-XXXXXX";
std::vector<char> tmpl(tmpl_str.begin(), tmpl_str.end());
tmpl.push_back('\0');
int fd = mkstemp(tmpl.data());
temp_path_ = tmpl.data();
fflush(stdout);
saved_fd_ = dup(STDOUT_FILENO);
dup2(fd, STDOUT_FILENO);
close(fd);
}
CapturedStdout::~CapturedStdout() {
fflush(stdout);
if (saved_fd_ >= 0) {
dup2(saved_fd_, STDOUT_FILENO);
close(saved_fd_);
}
std::error_code ec;
std::filesystem::remove(temp_path_, ec);
}
std::string CapturedStdout::contents() {
fflush(stdout);
std::ifstream in(temp_path_);
std::ostringstream out;
out << in.rdbuf();
return out.str();
}
std::vector<std::string> split_lines_trimmed(const std::string& text) {
std::vector<std::string> lines;
std::istringstream iss(text);
std::string line;
while (std::getline(iss, line)) {
while (!line.empty() && std::isspace(static_cast<unsigned char>(line.back()))) {
line.pop_back();
}
if (!line.empty()) {
lines.push_back(line);
}
}
return lines;
}
std::vector<std::string> extract_marked_lines(const std::string& text) {
auto lines = split_lines_trimmed(text);
auto begin = std::find(lines.begin(), lines.end(), "BEGIN-TEST-OUTPUT");
auto end = std::find(lines.begin(), lines.end(), "END-TEST-OUTPUT");
if (begin == lines.end() || end == lines.end() || end <= begin) {
return {};
}
return std::vector<std::string>(begin + 1, end);
}