Files
shadow/tests/common/mod.rs
T
Pierre Warnier b516b87be8 docs: the upstream suite is not GNU, and is mostly BSD-licensed (#322)
* docs: the upstream suite is not GNU, and is mostly BSD-licensed

The README called the original shadow-utils a GNU project and GPL-2.0+,
and CONTRIBUTING grounded the clean-room policy on that. Neither holds,
as #320 points out: shadow-maint/shadow is not a GNU project, and its
tree is mostly BSD-3-Clause with GPL-2.0-or-later files among it --
su.c, vipw.c, contributed scripts, some translated manuals. Checked
against Debian's copyright file for the package and Fedora's licence
tag, BSD-3-Clause AND GPL-2.0-or-later, not against the source.

The policy stands, on the accurate ground: this is an independent MIT
implementation that copies from neither part. Copying from the BSD part
would carry attribution terms this project does not take on; from the
GPL part, a derivative work; sorting files by licence would mean reading
them. So the rule is unchanged: do not read that tree.

Every "GNU" that meant upstream is now "upstream" or "the distribution's
package": README, CONTRIBUTING, Makefile, the man pages' section heading,
the e2e image and suite, test comments. make test-gnu-compat is make
test-upstream-compat and its script is renamed with it. The 2026-05-04
audit record gets an erratum at the top and is otherwise left as it was
run.

* docs: the last upstream-as-GNU mentions in the compat suite and the tool table

* tests: say upstream, not GNU, in the test comments and skip messages too

* tests: one more

* docs: keep the README's clean-room line as it was, reworded around it
2026-09-21 15:52:51 +02:00

139 lines
4.5 KiB
Rust

// This file is part of the shadow-rs package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Shared test helpers for shadow-rs integration tests.
//!
//! Import with `#[path = "../common/mod.rs"] mod common;` in test files.
/// The variable that turns a root-only skip into a failure.
pub const REQUIRE_ROOT: &str = "SHADOW_TEST_REQUIRE_ROOT";
/// Skip the test when not running as root (euid != 0).
///
/// Returns `true` if the test should be skipped.
///
/// Roughly three quarters of this suite needs root, and a skipped test is
/// reported `ok`, so a run that exercised almost nothing looked exactly like a
/// run that exercised everything. Setting `SHADOW_TEST_REQUIRE_ROOT=1` makes
/// the skip a failure instead; the Docker images set it, so the root suite
/// cannot quietly stop running there.
#[must_use]
pub fn skip_unless_root() -> bool {
if rustix::process::geteuid().is_root() {
return false;
}
assert!(
!require_root(),
"{REQUIRE_ROOT} is set, but this test needs root and the suite is not \
running as root"
);
true
}
/// Whether the run demands that root-only tests actually run.
///
/// An empty value counts as unset, so `-e SHADOW_TEST_REQUIRE_ROOT=` turns the
/// requirement off rather than leaving it mysteriously on.
#[must_use]
pub fn require_root() -> bool {
std::env::var_os(REQUIRE_ROOT).is_some_and(|v| !v.is_empty())
}
/// A `Command` that runs the multicall binary as `argv[0] = <tool>`.
///
/// Tests that call `uumain` in-process assert on an exit code and nothing
/// else: no test in this suite has ever checked a byte of output, so
/// "bit-for-bit identical to upstream" was unverified. Running the real binary also
/// keeps `harden_process`, `setuid`, Landlock and umask changes from leaking
/// from one test into the next through a shared process.
#[must_use]
pub fn tool(name: &str) -> std::process::Command {
let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_shadow-rs"));
cmd.arg(name);
// A test must not inherit the developer's locale or PATH and then assert
// on output shaped by them.
cmd.env_clear();
cmd.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin");
if require_root() {
cmd.env(REQUIRE_ROOT, "1");
}
cmd
}
/// What a spawned tool did.
pub struct Output {
/// Exit status, or 1 if the process was killed by a signal.
pub code: i32,
pub stdout: String,
pub stderr: String,
}
impl Output {
/// Assert the exit code, showing both streams when it does not match.
pub fn assert_code(&self, expected: i32) -> &Self {
assert_eq!(
self.code, expected,
"expected exit {expected}, got {}\n--- stdout ---\n{}\n--- stderr ---\n{}",
self.code, self.stdout, self.stderr
);
self
}
/// Assert that stdout contains `needle`.
pub fn assert_stdout_contains(&self, needle: &str) -> &Self {
assert!(
self.stdout.contains(needle),
"stdout does not contain {needle:?}\n--- stdout ---\n{}",
self.stdout
);
self
}
/// Assert that stderr contains `needle`.
pub fn assert_stderr_contains(&self, needle: &str) -> &Self {
assert!(
self.stderr.contains(needle),
"stderr does not contain {needle:?}\n--- stderr ---\n{}",
self.stderr
);
self
}
/// Assert that stdout is exactly `expected`.
pub fn assert_stdout_is(&self, expected: &str) -> &Self {
assert_eq!(self.stdout, expected, "stderr was:\n{}", self.stderr);
self
}
}
/// Run a tool and capture what it produced.
///
/// # Panics
///
/// Panics if the binary cannot be spawned, which means the test setup is
/// broken rather than the tool.
#[must_use]
pub fn run(name: &str, args: &[&str]) -> Output {
run_cmd(tool(name).args(args))
}
/// Run an already-configured command and capture what it produced.
///
/// # Panics
///
/// Panics if the binary cannot be spawned.
pub fn run_cmd(cmd: &mut std::process::Command) -> Output {
let out = cmd
.output()
.unwrap_or_else(|e| panic!("cannot run the shadow-rs binary: {e}"));
Output {
// A signal leaves no exit code; report it as a generic failure rather
// than unwrapping None.
code: out.status.code().unwrap_or(1),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
}
}