#![cfg_attr(
all(doc, feature = "document-features"),
doc = ::document_features::document_features!()
)]
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
#![deny(missing_docs)]
use std::{
collections::{BTreeMap, HashMap},
env,
ffi::{OsStr, OsString},
io::Read,
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};
pub use bstr;
use bstr::ByteSlice;
use io_close::Close;
pub use is_ci;
use parking_lot::Mutex;
use std::sync::LazyLock;
pub use tempfile;
const ARCHIVE_DIR_NAME: &str = "generated-archives";
pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
pub type PostResult<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
pub fn build_example_for_test(package: &str, example: &str, target_tmpdir: impl Into<PathBuf>) -> PathBuf {
let mut cargo = std::process::Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from(env!("CARGO"))));
let res = cargo
.args(["build", "-p", package, "--example", example])
.status()
.expect("cargo should run fine");
assert!(res.success(), "cargo invocation should be successful");
let target_tmpdir = target_tmpdir.into();
let shared_path = target_tmpdir
.ancestors()
.nth(1)
.expect("first parent in target dir")
.join("debug")
.join("examples")
.join(format!("{example}{}", std::env::consts::EXE_SUFFIX));
let stable_path = target_tmpdir.join(format!(
"{example}-{}{}",
std::process::id(),
std::env::consts::EXE_SUFFIX
));
let mut last_err = None;
for _ in 0..10 {
match std::fs::copy(&shared_path, &stable_path) {
Ok(_) => return stable_path,
Err(err) => {
last_err = Some(err);
std::thread::sleep(Duration::from_millis(50));
}
}
}
panic!(
"driver at {} could be copied for stable test execution: {last_err:?}",
shared_path.display()
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FixtureState<'a> {
Uninitialized(&'a Path),
Fresh(&'a Path),
}
impl FixtureState<'_> {
pub fn path(&self) -> &Path {
match self {
FixtureState::Uninitialized(path) | FixtureState::Fresh(path) => path,
}
}
pub fn is_uninitialized(&self) -> bool {
matches!(self, FixtureState::Uninitialized(_))
}
}
trait IsExcluded {
fn is_excluded(&self, archive: &Path) -> bool;
}
#[cfg(not(feature = "worktree-exclusions"))]
fn is_excluded_by_lines(lines: &str, archive: &Path) -> bool {
let archive = archive.to_string_lossy().replace('\\', "/");
let filename = archive.rsplit('/').next().unwrap_or(&archive);
lines.lines().any(|line| {
let pattern = line.trim();
if pattern.is_empty() || pattern.starts_with('#') {
return false;
}
let pattern = pattern.trim_start_matches('/');
let candidate = if pattern.contains('/') {
archive.as_str()
} else {
filename
};
wildcard_match(pattern, candidate)
})
}
#[cfg(not(feature = "worktree-exclusions"))]
fn wildcard_match(pattern: &str, text: &str) -> bool {
if !pattern.contains('*') {
return pattern == text;
}
let mut remainder = text;
let mut parts = pattern.split('*').peekable();
let first = parts.next().expect("split yields at least one item");
if !first.is_empty() {
let Some(stripped) = remainder.strip_prefix(first) else {
return false;
};
remainder = stripped;
}
while let Some(part) = parts.next() {
if part.is_empty() {
continue;
}
let Some(pos) = remainder.find(part) else {
return false;
};
remainder = &remainder[pos + part.len()..];
if parts.peek().is_none() && !pattern.ends_with('*') {
return remainder.is_empty();
}
}
pattern.ends_with('*') || remainder.is_empty()
}
pub struct GitDaemon {
process: GitDaemonProcess,
pub url: String,
}
enum GitDaemonProcess {
#[cfg(not(unix))]
Child(std::process::Child),
#[cfg(unix)]
Inetd {
shutdown: std::sync::Arc<std::sync::atomic::AtomicBool>,
server_addr: std::net::SocketAddr,
listener_thread: Option<std::thread::JoinHandle<()>>,
},
}
impl Drop for GitDaemon {
fn drop(&mut self) {
match &mut self.process {
#[cfg(not(unix))]
GitDaemonProcess::Child(child) => {
child.kill().ok();
}
#[cfg(unix)]
GitDaemonProcess::Inetd {
shutdown,
server_addr,
listener_thread,
} => {
shutdown.store(true, std::sync::atomic::Ordering::SeqCst);
std::net::TcpStream::connect(*server_addr).ok();
if let Some(listener_thread) = listener_thread.take() {
listener_thread.join().ok();
}
}
}
}
}
static SCRIPT_IDENTITY: LazyLock<Mutex<BTreeMap<PathBuf, u32>>> = LazyLock::new(|| Mutex::new(BTreeMap::new()));
#[cfg(feature = "worktree-exclusions")]
static EXCLUDE_LUT: LazyLock<Mutex<Option<gix_worktree::Stack>>> = LazyLock::new(|| {
let cache = (|| {
let (repo_path, _) = gix_discover::upwards(Path::new(".")).ok()?;
let (gix_dir, work_tree) = repo_path.into_repository_and_work_tree_directories();
let work_tree = work_tree?.canonicalize().ok()?;
let mut buf = Vec::with_capacity(512);
let case = if gix_fs::Capabilities::probe(&work_tree).ignore_case {
gix_worktree::ignore::glob::pattern::Case::Fold
} else {
Default::default()
};
let state = gix_worktree::stack::State::IgnoreStack(gix_worktree::stack::state::Ignore::new(
Default::default(),
gix_worktree::ignore::Search::from_git_dir(
&gix_dir,
None,
&mut buf,
gix_worktree::stack::state::ignore::ParseIgnore {
support_precious: false,
},
)
.ok()?,
None,
gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
Default::default(),
));
Some(gix_worktree::Stack::new(
work_tree,
state,
case,
buf,
Default::default(),
))
})();
Mutex::new(cache)
});
#[cfg(feature = "worktree-exclusions")]
struct WorktreeExclusions;
#[cfg(feature = "worktree-exclusions")]
impl IsExcluded for WorktreeExclusions {
fn is_excluded(&self, archive: &Path) -> bool {
let mut lut = EXCLUDE_LUT.lock();
lut.as_mut()
.and_then(|cache| {
let archive = env::current_dir().ok()?.join(archive);
let relative_path = archive.strip_prefix(cache.base()).ok()?;
cache
.at_path(
relative_path,
Some(gix_worktree::index::entry::Mode::FILE),
&gix_worktree::object::find::Never,
)
.ok()?
.is_excluded()
.into()
})
.unwrap_or(false)
}
}
#[cfg(feature = "worktree-exclusions")]
fn default_excludes() -> &'static dyn IsExcluded {
static WORKTREE_EXCLUSIONS: WorktreeExclusions = WorktreeExclusions;
&WORKTREE_EXCLUSIONS
}
#[cfg(not(feature = "worktree-exclusions"))]
struct GitignoreExclusions;
#[cfg(not(feature = "worktree-exclusions"))]
impl IsExcluded for GitignoreExclusions {
fn is_excluded(&self, archive: &Path) -> bool {
let Some(parent) = archive.parent() else {
return false;
};
std::fs::read_to_string(parent.join(".gitignore")).is_ok_and(|lines| is_excluded_by_lines(&lines, archive))
}
}
#[cfg(not(feature = "worktree-exclusions"))]
fn default_excludes() -> &'static dyn IsExcluded {
static GITIGNORE_EXCLUSIONS: GitignoreExclusions = GitignoreExclusions;
&GITIGNORE_EXCLUSIONS
}
#[cfg(windows)]
const GIT_PROGRAM: &str = "git.exe";
#[cfg(not(windows))]
const GIT_PROGRAM: &str = "git";
const DISABLE_AUTO_MAINTENANCE_CONFIG: &[(&str, &str)] = &[("maintenance.auto", "false"), ("gc.auto", "0")];
const ISOLATED_GIT_CONFIG: &[(&str, &str)] = &[
("commit.gpgsign", "false"),
("tag.gpgsign", "false"),
("init.defaultBranch", "main"),
("protocol.file.allow", "always"),
("maintenance.auto", "false"),
("gc.auto", "0"),
];
static GIT_CORE_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
let output = std::process::Command::new(GIT_PROGRAM)
.arg("--exec-path")
.output()
.expect("can execute `git --exec-path`");
assert!(output.status.success(), "`git --exec-path` failed");
output
.stdout
.strip_suffix(b"\n")
.expect("`git --exec-path` output to be well-formed")
.to_os_str()
.expect("no invalid UTF-8 in `--exec-path` except as OS allows")
.into()
});
pub static GIT_VERSION: LazyLock<(u8, u8, u8)> =
LazyLock::new(|| parse_git_version().expect("git version to be parsable"));
pub enum Creation {
CopyFromReadOnly,
Execute,
}
pub fn should_skip_as_git_version_is_smaller_than(major: u8, minor: u8, patch: u8) -> bool {
if is_ci::cached() {
return false; }
*GIT_VERSION < (major, minor, patch)
}
fn parse_git_version() -> Result<(u8, u8, u8)> {
let output = std::process::Command::new(GIT_PROGRAM).arg("--version").output()?;
git_version_from_bytes(&output.stdout)
}
fn git_version_from_bytes(bytes: &[u8]) -> Result<(u8, u8, u8)> {
let mut numbers = bytes
.split(|b| *b == b' ' || *b == b'\n')
.nth(2)
.expect("git version <version>")
.split(|b| *b == b'.')
.take(3)
.map(|n| std::str::from_utf8(n).expect("valid utf8 in version number"))
.map(u8::from_str);
Ok((|| -> Result<_> {
Ok((
numbers.next().expect("major")?,
numbers.next().expect("minor")?,
numbers.next().expect("patch")?,
))
})()
.map_err(|err| {
format!(
"Could not parse version from output of 'git --version' ({:?}) with error: {}",
bytes.to_str_lossy(),
err
)
})?)
}
pub fn set_current_dir(new_cwd: impl AsRef<Path>) -> std::io::Result<AutoRevertToPreviousCWD> {
let cwd = env::current_dir()?;
env::set_current_dir(new_cwd)?;
Ok(AutoRevertToPreviousCWD(cwd))
}
#[derive(Debug)]
#[must_use]
pub struct AutoRevertToPreviousCWD(PathBuf);
impl Drop for AutoRevertToPreviousCWD {
fn drop(&mut self) {
env::set_current_dir(&self.0).unwrap();
}
}
pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::process::ExitStatus> {
let mut cmd = std::process::Command::new(GIT_PROGRAM);
apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG)
.current_dir(working_dir)
.args(args)
.status()
}
pub fn invoke_bash(cwd: impl AsRef<Path>, script: &str) {
let mut cmd = std::process::Command::new(bash_program());
let status = apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG)
.current_dir(cwd)
.arg("-c")
.arg(script)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.expect("can run bash script");
assert!(status.success(), "bash script failed with {status}");
}
pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> {
#[cfg(unix)]
{
spawn_git_daemon_inetd(working_dir)
}
#[cfg(not(unix))]
{
spawn_git_daemon_process(working_dir)
}
}
#[cfg(not(unix))]
fn spawn_git_daemon_process(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> {
let mut ports: Vec<_> = (9419u16..9419 + 100).collect();
fastrand::shuffle(&mut ports);
let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port));
let free_port = {
let listener = std::net::TcpListener::bind(ports.into_iter().map(addr_at).collect::<Vec<_>>().as_slice())?;
listener.local_addr().expect("listener address is available").port()
};
let child = {
let mut cmd =
std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" }));
apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG)
.current_dir(working_dir)
.args(["--verbose", "--base-path=.", "--export-all", "--user-path"])
.arg(format!("--port={free_port}"))
.spawn()?
};
let server_addr = addr_at(free_port);
for time in gix_lock::backoff::Quadratic::default_with_random() {
std::thread::sleep(time);
if std::net::TcpStream::connect(server_addr).is_ok() {
break;
}
}
Ok(GitDaemon {
process: GitDaemonProcess::Child(child),
url: format!("git://{server_addr}"),
})
}
#[cfg(unix)]
fn spawn_git_daemon_inetd(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> {
use std::{
net::{TcpListener, TcpStream},
os::fd::{FromRawFd, IntoRawFd},
process::Stdio,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
fn stream_to_stdio(stream: TcpStream) -> Stdio {
unsafe { Stdio::from_raw_fd(stream.into_raw_fd()) }
}
let working_dir = working_dir.as_ref().to_owned();
let listener = TcpListener::bind(("127.0.0.1", 0))?;
let server_addr = listener.local_addr()?;
let shutdown = Arc::new(AtomicBool::new(false));
let listener_thread = std::thread::spawn({
let shutdown = shutdown.clone();
move || {
for incoming in listener.incoming() {
let stream = match incoming {
Ok(stream) => stream,
Err(_) => break,
};
if shutdown.load(Ordering::SeqCst) {
break;
}
let peer_addr = stream.peer_addr().ok();
let stdin = match stream.try_clone() {
Ok(stream) => stream_to_stdio(stream),
Err(_) => continue,
};
let stdout = stream_to_stdio(stream);
let mut cmd = std::process::Command::new(GIT_PROGRAM);
let Ok(mut child) = apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG)
.args([
"-c",
"uploadpack.allowrefinwant",
"daemon",
"--inetd",
"--verbose",
"--base-path=.",
"--export-all",
"--user-path",
])
.current_dir(&working_dir)
.stdin(stdin)
.stdout(stdout)
.stderr(Stdio::null())
.envs(remote_env(peer_addr))
.spawn()
else {
continue;
};
std::thread::spawn(move || {
let _ = child.wait();
});
}
}
});
Ok(GitDaemon {
process: GitDaemonProcess::Inetd {
shutdown,
server_addr,
listener_thread: Some(listener_thread),
},
url: format!("git://{server_addr}"),
})
}
#[cfg(unix)]
fn remote_env(peer_addr: Option<std::net::SocketAddr>) -> Vec<(&'static str, String)> {
peer_addr
.map(|addr| {
vec![
("REMOTE_ADDR", addr.ip().to_string()),
("REMOTE_PORT", addr.port().to_string()),
]
})
.unwrap_or_default()
}
#[derive(Copy, Clone)]
enum ArgsInHash {
Yes,
No,
}
pub fn fixture_path(path: impl AsRef<Path>) -> PathBuf {
fixture_base().join(path.as_ref())
}
fn fixture_base() -> PathBuf {
PathBuf::from("tests").join("fixtures")
}
pub fn fixture_bytes(path: impl AsRef<Path>) -> Vec<u8> {
match std::fs::read(fixture_path(path.as_ref())) {
Ok(res) => res,
Err(_) => panic!("File at '{}' not found", path.as_ref().display()),
}
}
pub fn scripted_fixture_read_only(script_name: impl AsRef<Path>) -> Result<PathBuf> {
scripted_fixture_read_only_with_args(script_name, None::<String>)
}
pub fn scripted_fixture_read_only_needs_archive(script_name: impl AsRef<Path>) -> Result<PathBuf> {
scripted_fixture_read_only_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
script_name,
None::<String>,
None,
ArgsInHash::Yes,
default_excludes(),
None::<(u32, _)>,
true,
)
.map(|(dir, _)| dir)
}
pub fn scripted_fixture_writable(script_name: impl AsRef<Path>) -> Result<tempfile::TempDir> {
scripted_fixture_writable_with_args(script_name, None::<String>, Creation::CopyFromReadOnly)
}
pub fn scripted_fixture_writable_with_args(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
mode: Creation,
) -> Result<tempfile::TempDir> {
scripted_fixture_writable_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
script_name,
args,
mode,
ArgsInHash::Yes,
default_excludes(),
None::<(u32, _)>,
)
.map(|(dir, _)| dir)
}
pub fn scripted_fixture_writable_with_args_single_archive(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
mode: Creation,
) -> Result<tempfile::TempDir> {
scripted_fixture_writable_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
script_name,
args,
mode,
ArgsInHash::No,
default_excludes(),
None::<(u32, _)>,
)
.map(|(dir, _)| dir)
}
fn scripted_fixture_writable_with_args_inner<F, T>(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
mode: Creation,
args_in_hash: ArgsInHash,
excludes: &dyn IsExcluded,
mut post_process: Option<(u32, F)>,
) -> Result<(tempfile::TempDir, Option<T>)>
where
F: FnMut(FixtureState<'_>) -> PostResult<T>,
{
let dst = tempfile::TempDir::new()?;
Ok(match mode {
Creation::CopyFromReadOnly => {
let (ro_dir, _res_ignored) = scripted_fixture_read_only_with_args_inner(
script_name,
args,
None,
args_in_hash,
excludes,
post_process.as_mut().map(|(v, f)| (*v, f)),
false,
)?;
copy_recursively_into_existing_dir(ro_dir, dst.path())?;
(dst, _res_ignored)
}
Creation::Execute => {
let (_, post_result) = scripted_fixture_read_only_with_args_inner(
script_name,
args,
dst.path().into(),
args_in_hash,
excludes,
post_process.as_mut().map(|(v, f)| (*v, f)),
false,
)?;
(dst, post_result)
}
})
}
pub fn copy_recursively_into_existing_dir(src_dir: impl AsRef<Path>, dst_dir: impl AsRef<Path>) -> std::io::Result<()> {
fs_extra::copy_items(
&std::fs::read_dir(src_dir)?
.map(|e| e.map(|e| e.path()))
.collect::<std::result::Result<Vec<_>, _>>()?,
dst_dir,
&fs_extra::dir::CopyOptions {
overwrite: false,
skip_exist: false,
copy_inside: false,
content_only: false,
..Default::default()
},
)
.map_err(std::io::Error::other)?;
Ok(())
}
pub fn scripted_fixture_read_only_with_args(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
) -> Result<PathBuf> {
scripted_fixture_read_only_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
script_name,
args,
None,
ArgsInHash::Yes,
default_excludes(),
None::<(u32, _)>,
false,
)
.map(|(dir, _)| dir)
}
pub fn scripted_fixture_read_only_with_args_single_archive(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
) -> Result<PathBuf> {
scripted_fixture_read_only_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
script_name,
args,
None,
ArgsInHash::No,
default_excludes(),
None::<(u32, _)>,
false,
)
.map(|(dir, _)| dir)
}
pub fn scripted_fixture_read_only_with_post<T>(
script_name: impl AsRef<Path>,
version: u32,
post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
) -> Result<(PathBuf, T)> {
scripted_fixture_read_only_with_args_inner(
script_name,
None::<String>,
None,
ArgsInHash::Yes,
default_excludes(),
Some((version, post_process)),
false,
)
.map(|(path, opt)| (path, opt.expect("post_process was provided")))
}
pub fn scripted_fixture_read_only_with_args_with_post<T>(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
version: u32,
post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
) -> Result<(PathBuf, T)> {
scripted_fixture_read_only_with_args_inner(
script_name,
args,
None,
ArgsInHash::Yes,
default_excludes(),
Some((version, post_process)),
false,
)
.map(|(path, opt)| (path, opt.expect("post_process was provided")))
}
pub fn scripted_fixture_read_only_with_args_single_archive_with_post<T>(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
version: u32,
post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
) -> Result<(PathBuf, T)> {
scripted_fixture_read_only_with_args_inner(
script_name,
args,
None,
ArgsInHash::No,
default_excludes(),
Some((version, post_process)),
false,
)
.map(|(path, opt)| (path, opt.expect("post_process was provided")))
}
pub fn scripted_fixture_writable_with_post<T>(
script_name: impl AsRef<Path>,
version: u32,
post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
) -> Result<(tempfile::TempDir, T)> {
scripted_fixture_writable_with_args_inner(
script_name,
None::<String>,
Creation::CopyFromReadOnly,
ArgsInHash::Yes,
default_excludes(),
Some((version, post_process)),
)
.map(|(tmp, opt)| (tmp, opt.expect("post_process was provided")))
}
pub fn scripted_fixture_writable_with_args_with_post<T>(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
mode: Creation,
version: u32,
post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
) -> Result<(tempfile::TempDir, T)> {
scripted_fixture_writable_with_args_inner(
script_name,
args,
mode,
ArgsInHash::Yes,
default_excludes(),
Some((version, post_process)),
)
.map(|(tmp, opt)| (tmp, opt.expect("post_process was provided")))
}
pub fn scripted_fixture_writable_with_args_single_archive_with_post<T>(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
mode: Creation,
version: u32,
post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
) -> Result<(tempfile::TempDir, T)> {
scripted_fixture_writable_with_args_inner(
script_name,
args,
mode,
ArgsInHash::No,
default_excludes(),
Some((version, post_process)),
)
.map(|(tmp, opt)| (tmp, opt.expect("post_process was provided")))
}
pub fn rust_fixture_read_only<T, F>(name: &str, version: u32, make_fixture: F) -> Result<(PathBuf, T)>
where
F: FnOnce(FixtureState<'_>) -> PostResult<T>,
{
rust_fixture_read_only_inner(name, version, None, make_fixture, None, default_excludes())
}
pub fn rust_fixture_writable<T, F>(
name: &str,
version: u32,
mode: Creation,
make_fixture: F,
) -> Result<(tempfile::TempDir, T)>
where
F: FnMut(FixtureState<'_>) -> PostResult<T>,
{
rust_fixture_writable_inner(name, version, None, make_fixture, mode, default_excludes())
}
fn rust_fixture_writable_inner<T, F>(
name: &str,
version: u32,
object_hash: Option<gix_hash::Kind>,
mut make_fixture: F,
mode: Creation,
excludes: &dyn IsExcluded,
) -> Result<(tempfile::TempDir, T)>
where
F: FnMut(FixtureState<'_>) -> PostResult<T>,
{
let dst = tempfile::TempDir::new()?;
let res = match mode {
Creation::CopyFromReadOnly => {
let (ro_dir, _res_ignored) =
rust_fixture_read_only_inner(name, version, object_hash, &mut make_fixture, None, excludes)?;
copy_recursively_into_existing_dir(ro_dir, dst.path())?;
make_fixture(FixtureState::Fresh(dst.path()))?
}
Creation::Execute => {
let (_, res) =
rust_fixture_read_only_inner(name, version, object_hash, make_fixture, Some(dst.path()), excludes)?;
res
}
};
Ok((dst, res))
}
fn rust_fixture_read_only_inner<T, F>(
name: &str,
version: u32,
object_hash: Option<gix_hash::Kind>,
make_fixture: F,
destination_dir: Option<&Path>,
excludes: &dyn IsExcluded,
) -> Result<(PathBuf, T)>
where
F: FnOnce(FixtureState<'_>) -> PostResult<T>,
{
gix_tempfile::signal::setup(
gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour,
);
let script_identity = version;
let archive_name = format!("rust-{name}");
let fixture_base = fixture_base();
let archive_file_path = fixture_base
.join(ARCHIVE_DIR_NAME)
.join(format!("{archive_name}.{}", tar_extension()));
let (force_run, script_result_directory) = force_and_dir(
destination_dir,
&fixture_base,
&archive_name,
object_hash,
&script_identity,
None,
);
let _marker = marker_if_needed(destination_dir, archive_name)?;
run_fixture_generator_with_marker_handling(
&archive_file_path,
&script_result_directory,
script_identity,
force_run,
false,
excludes,
&format!("using Rust closure '{name}'"),
make_fixture,
)
.map(|res| (script_result_directory, res))
}
fn marker_if_needed(
destination_dir: Option<&Path>,
archive_name: impl AsRef<Path>,
) -> Result<Option<gix_lock::Marker>> {
Ok(destination_dir
.is_none()
.then(|| {
gix_lock::Marker::acquire_to_hold_resource(
archive_name,
gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)),
None,
)
})
.transpose()?)
}
fn force_and_dir(
destination_dir: Option<&Path>,
fixture_base: &Path,
archive_name: impl AsRef<Path>,
object_hash: Option<gix_hash::Kind>,
script_identity: &dyn std::fmt::Display,
cache_variant: Option<&str>,
) -> (bool, PathBuf) {
destination_dir.map_or_else(
|| {
let mut dir = fixture_base.join(
Path::new("generated-do-not-edit")
.join(archive_name)
.join(object_hash.unwrap_or_else(self::object_hash).to_string()),
);
if let Some(cache_variant) = cache_variant {
dir = dir.join(cache_variant);
}
let dir = dir.join(format!("{}-{}", script_identity, family_name()));
(false, dir)
},
|d| (true, d.to_owned()),
)
}
#[expect(clippy::too_many_arguments)]
fn run_fixture_generator_with_marker_handling<T, F>(
archive_file_path: &Path,
script_result_directory: &Path,
script_identity: u32,
force_run: bool,
needs_archive: bool,
excludes: &dyn IsExcluded,
description: &str,
make_fixture: F,
) -> Result<T>
where
F: FnOnce(FixtureState<'_>) -> PostResult<T>,
{
let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_");
if force_run || !script_result_directory.is_dir() || failure_marker.is_file() {
if failure_marker.is_file() {
std::fs::remove_dir_all(script_result_directory).map_err(|err| {
format!(
"Failed to remove '{script_result_directory}', please try to do that by hand. Original error: {err}",
script_result_directory = script_result_directory.display()
)
})?;
}
std::fs::create_dir_all(script_result_directory)?;
match extract_archive(
archive_file_path,
script_result_directory,
script_identity,
needs_archive,
) {
Ok((archive_id, platform)) => {
eprintln!(
"Extracted fixture from archive '{}' ({}, {:?})",
archive_file_path.display(),
archive_id,
platform
);
make_fixture(FixtureState::Fresh(script_result_directory))
}
Err(err) => {
if err.kind() != std::io::ErrorKind::NotFound {
eprintln!("failed to extract '{}': {}", archive_file_path.display(), err);
std::fs::remove_dir_all(script_result_directory).map_err(|err| {
format!(
"Failed to remove '{script_result_directory}', please try to do that by hand. Original error: {err}",
script_result_directory = script_result_directory.display()
)
})?;
std::fs::create_dir_all(script_result_directory)?;
} else if !excludes.is_excluded(archive_file_path) {
eprintln!(
"Archive at '{}' not found, creating fixture {}",
archive_file_path.display(),
description
);
}
let res = match make_fixture(FixtureState::Uninitialized(script_result_directory)) {
Ok(value) => value,
Err(err) => {
write_failure_marker(&failure_marker);
return Err(err);
}
};
create_archive_if_we_should(script_result_directory, archive_file_path, script_identity, excludes)
.inspect_err(|_err| {
write_failure_marker(&failure_marker);
})?;
Ok(res)
}
}
} else {
make_fixture(FixtureState::Fresh(script_result_directory))
}
}
fn scripted_fixture_read_only_with_args_inner<F, T>(
script_name: impl AsRef<Path>,
args: impl IntoIterator<Item = impl Into<String>>,
destination_dir: Option<&Path>,
args_in_hash: ArgsInHash,
excludes: &dyn IsExcluded,
post_process: Option<(u32, F)>,
needs_archive: bool,
) -> Result<(PathBuf, Option<T>)>
where
F: FnMut(FixtureState<'_>) -> PostResult<T>,
{
gix_tempfile::signal::setup(
gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour,
);
let object_hash = object_hash();
let script_location = script_name.as_ref();
let fixture_base = fixture_base();
let script_path = fixture_path(script_location);
let args: Vec<String> = args.into_iter().map(Into::into).collect();
let post_version = post_process.as_ref().map(|(v, _)| *v);
let script_identity = {
let mut map = SCRIPT_IDENTITY.lock();
let init = if object_hash == gix_hash::Kind::Sha1 {
script_path.clone()
} else {
script_path.clone().join(object_hash.to_string())
};
let key = args.iter().fold(init, |p, a| p.join(a));
let key = if let Some(v) = post_version {
key.join(format!("post-v{v}"))
} else {
key
};
map.entry(key)
.or_insert_with(|| {
let crc_value = crc::Crc::<u32>::new(&crc::CRC_32_CKSUM);
let mut crc_digest = crc_value.digest();
crc_digest.update(&std::fs::read(&script_path).unwrap_or_else(|err| {
panic!(
"file {script_path} in CWD '{cwd}' could not be read: {err}",
cwd = env::current_dir().expect("valid cwd").display(),
script_path = script_path.display(),
)
}));
for arg in &args {
crc_digest.update(arg.as_bytes());
}
if let Some(v) = post_version {
crc_digest.update(&v.to_le_bytes());
}
crc_digest.finalize()
})
.to_owned()
};
let script_basename = script_location.file_stem().unwrap_or(script_location.as_os_str());
let archive_file_path = fixture_base.join(ARCHIVE_DIR_NAME).join({
let suffix = match args_in_hash {
ArgsInHash::Yes => {
let mut suffix = args.join("_");
if !suffix.is_empty() {
suffix.insert(0, '_');
}
suffix.replace(['\\', '/', ' ', '.'], "_")
}
ArgsInHash::No => "".into(),
};
let potential_hash_suffix = if object_hash == gix_hash::Kind::Sha1 {
"".into()
} else {
format!("_{object_hash}")
};
format!(
"{}{suffix}{potential_hash_suffix}.{}",
script_basename.to_str().expect("valid UTF-8"),
tar_extension()
)
});
let (force_run, script_result_directory) = force_and_dir(
destination_dir,
&fixture_base,
script_basename,
Some(object_hash),
&script_identity,
needs_archive.then_some("archive"),
);
let _marker = marker_if_needed(destination_dir, script_basename)?;
let script_identity_for_archive = match args_in_hash {
ArgsInHash::Yes => script_identity,
ArgsInHash::No => 0,
};
let script_absolute_path = env::current_dir()?.join(&script_path);
let post_process_closure = post_process.map(|(_, f)| f);
let res = run_fixture_generator_with_marker_handling(
&archive_file_path,
&script_result_directory,
script_identity_for_archive,
force_run,
needs_archive,
excludes,
&format!("using script '{}'", script_location.display()),
|fixture_state| {
if let FixtureState::Uninitialized(dir) = fixture_state {
let mut cmd = std::process::Command::new(&script_absolute_path);
let output = match configure_command(&mut cmd, object_hash, &args, dir).output() {
Ok(out) => out,
Err(err)
if err.kind() == std::io::ErrorKind::PermissionDenied
|| err.raw_os_error() == Some(193) =>
{
cmd = std::process::Command::new(bash_program());
configure_command(cmd.arg(&script_absolute_path), object_hash, &args, dir).output()?
}
Err(err) => return Err(err.into()),
};
if !output.status.success() {
eprintln!("stdout: {}", output.stdout.as_bstr());
eprintln!("stderr: {}", output.stderr.as_bstr());
return Err(format!("fixture script of {cmd:?} failed").into());
}
}
if let Some(mut f) = post_process_closure {
f(fixture_state).map(Some)
} else {
Ok(None)
}
},
)?;
Ok((script_result_directory, res))
}
pub fn object_hash_from_env() -> Option<gix_hash::Kind> {
static FIXTURE_HASH: LazyLock<Option<gix_hash::Kind>> = LazyLock::new(|| {
env::var_os("GIX_TEST_FIXTURE_HASH").and_then(|value| value.into_string().ok()).map(|object_kind| {
gix_hash::Kind::from_str(&object_kind).unwrap_or_else(|_| {
panic!(
"GIX_TEST_FIXTURE_HASH was set to {object_kind} which is an invalid value. Valid values are {}. Exiting.",
gix_hash::Kind::all().iter().map(std::string::ToString::to_string).collect::<Vec<_>>().join(", ")
)
})
})
});
*FIXTURE_HASH
}
pub fn object_hash() -> gix_hash::Kind {
object_hash_from_env().unwrap_or_default()
}
pub fn git(current_dir: impl AsRef<Path>, arguments: &str) -> Result<String> {
let args = split_git_arguments(arguments)?;
let cwd = current_dir.as_ref();
let mut cmd = std::process::Command::new(GIT_PROGRAM);
let output = configure_command(&mut cmd, object_hash(), args.iter().map(String::as_str), cwd)
.current_dir(cwd)
.output()?;
if !output.status.success() {
return Err(format!(
"{cmd:?} failed with status {}\nstdout: {}\nstderr: {}",
output.status,
output.stdout.as_bstr(),
output.stderr.as_bstr()
)
.into());
}
Ok(String::from_utf8(output.stdout)?)
}
fn split_git_arguments(input: &str) -> Result<Vec<String>> {
let mut args = Vec::new();
let mut arg = String::new();
let mut quote = None;
let mut has_arg = false;
let mut chars = input.chars();
while let Some(ch) = chars.next() {
match quote {
Some('\'') => {
if ch == '\'' {
quote = None;
} else {
arg.push(ch);
}
}
Some('"') => {
if ch == '"' {
quote = None;
} else if ch == '\\' {
if let Some(next) = chars.next() {
arg.push(next);
}
} else {
arg.push(ch);
}
}
Some(_) => unreachable!("only single and double quotes are set"),
None => {
if ch.is_whitespace() {
if has_arg {
args.push(std::mem::take(&mut arg));
has_arg = false;
}
} else if matches!(ch, '\'' | '"') {
quote = Some(ch);
has_arg = true;
} else if ch == '\\' {
if let Some(next) = chars.next() {
arg.push(next);
}
has_arg = true;
} else {
arg.push(ch);
has_arg = true;
}
}
}
}
if let Some(quote) = quote {
return Err(format!("unterminated {quote:?} quote in git arguments").into());
}
if has_arg {
args.push(arg);
}
Ok(args)
}
pub fn normalize_debug_snapshot(value: &dyn std::fmt::Debug) -> (String, Vec<gix_hash::ObjectId>) {
normalize_hashes(&format!("{value:#?}"))
}
pub fn normalize_hashes(input: &str) -> (String, Vec<gix_hash::ObjectId>) {
let mut out = String::with_capacity(input.len());
let mut seen = HashMap::<gix_hash::ObjectId, usize>::new();
let mut removed = Vec::<gix_hash::ObjectId>::new();
let mut chars = input.chars().peekable();
let mut hex = String::new();
while let Some(ch) = chars.next() {
if ch.is_ascii_hexdigit() {
hex.clear();
hex.push(ch);
while let Some(ch) = chars.next_if(char::is_ascii_hexdigit) {
hex.push(ch);
}
if let Some(oid) = raw_object_id(&hex) {
strip_debug_hash_wrapper(&mut out, &mut chars);
push_normalized_oid(oid, &mut seen, &mut removed, &mut out);
} else {
out.push_str(&hex);
}
} else {
out.push(ch);
}
}
(out, removed)
}
fn raw_object_id(input: &str) -> Option<gix_hash::ObjectId> {
if !matches!(input.len(), 40 | 64) {
return None;
}
gix_hash::ObjectId::from_hex(input.as_bytes()).ok()
}
fn strip_debug_hash_wrapper(out: &mut String, chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
if !matches!(chars.peek(), Some(')')) {
return;
}
let consume_closing_parenthesis = if out.ends_with("Sha1(") {
out.truncate(out.len() - "Sha1(".len());
true
} else if out.ends_with("Sha256(") {
out.truncate(out.len() - "Sha256(".len());
true
} else {
false
};
if consume_closing_parenthesis {
chars.next();
}
}
fn push_normalized_oid(
oid: gix_hash::ObjectId,
seen: &mut HashMap<gix_hash::ObjectId, usize>,
removed: &mut Vec<gix_hash::ObjectId>,
out: &mut String,
) {
let normalized = *seen.entry(oid).or_insert_with(|| {
let current = removed.len();
removed.push(oid);
current
});
out.push_str("Oid(");
out.push_str(&(normalized + 1).to_string());
out.push(')');
}
#[cfg(windows)]
const NULL_DEVICE: &str = "nul"; #[cfg(not(windows))]
const NULL_DEVICE: &str = "/dev/null";
fn configure_command<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
cmd: &'a mut std::process::Command,
object_hash: gix_hash::Kind,
args: I,
script_result_directory: &Path,
) -> &'a mut std::process::Command {
let mut msys_for_git_bash_on_windows = env::var_os("MSYS").unwrap_or_default();
msys_for_git_bash_on_windows.push(" winsymlinks:nativestrict");
cmd.args(args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.current_dir(script_result_directory)
.env_remove("GIT_DIR")
.env_remove("GIT_INDEX_FILE")
.env_remove("GIT_OBJECT_DIRECTORY")
.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_COMMON_DIR")
.env_remove("GIT_ASKPASS")
.env_remove("SSH_ASKPASS")
.env("MSYS", msys_for_git_bash_on_windows)
.env(
"XDG_CONFIG_HOME",
script_result_directory.join(".gix-testtools-xdg-config"),
)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", NULL_DEVICE)
.env("GIT_TERMINAL_PROMPT", "false")
.env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000")
.env("GIT_AUTHOR_EMAIL", "author@example.com")
.env("GIT_AUTHOR_NAME", "author")
.env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000")
.env("GIT_COMMITTER_EMAIL", "committer@example.com")
.env("GIT_COMMITTER_NAME", "committer")
.env("GIT_DEFAULT_HASH", object_hash.to_string());
apply_git_config_by_environment(cmd, ISOLATED_GIT_CONFIG)
}
pub fn apply_git_config_by_environment<'a>(
cmd: &'a mut std::process::Command,
config: &[(&str, &str)],
) -> &'a mut std::process::Command {
cmd.env("GIT_CONFIG_COUNT", config.len().to_string());
for (idx, (key, value)) in config.iter().enumerate() {
cmd.env(format!("GIT_CONFIG_KEY_{idx}"), key);
cmd.env(format!("GIT_CONFIG_VALUE_{idx}"), value);
}
cmd
}
pub fn bash_program() -> &'static Path {
static GIT_BASH: LazyLock<PathBuf> = LazyLock::new(|| {
if cfg!(windows) {
GIT_CORE_DIR
.ancestors()
.nth(3)
.map(OsStr::new)
.iter()
.flat_map(|prefix| {
["/bin/bash.exe", "/usr/bin/bash.exe"].into_iter().map(|suffix| {
let mut raw_path = (*prefix).to_owned();
raw_path.push(suffix);
raw_path
})
})
.map(PathBuf::from)
.find(|bash| bash.is_file())
.unwrap_or_else(|| "bash.exe".into())
} else {
"bash".into()
}
});
GIT_BASH.as_ref()
}
fn write_failure_marker(failure_marker: &Path) {
std::fs::write(failure_marker, []).ok();
}
fn should_skip_all_archive_creation() -> bool {
cfg!(windows) || (is_ci::cached() && env::var_os("GIX_TEST_CREATE_ARCHIVES_EVEN_ON_CI").is_none())
}
fn is_lfs_pointer_file(path: &Path) -> bool {
const PREFIX: &[u8] = b"version https://git-lfs";
let mut buf = [0_u8; PREFIX.len()];
std::fs::OpenOptions::new()
.read(true)
.open(path)
.is_ok_and(|mut f| f.read_exact(&mut buf).is_ok_and(|_| buf.starts_with(PREFIX)))
}
fn create_archive_if_we_should(
source_dir: &Path,
archive: &Path,
script_identity: u32,
excludes: &dyn IsExcluded,
) -> std::io::Result<()> {
if should_skip_all_archive_creation() || excludes.is_excluded(archive) {
return Ok(());
}
if is_lfs_pointer_file(archive) {
eprintln!(
"Refusing to overwrite `gix-lfs` pointer file at \"{}\" - git lfs might not be properly installed.",
archive.display()
);
return Ok(());
}
std::fs::create_dir_all(archive.parent().expect("archive is a file"))?;
let meta_dir = populate_meta_dir(source_dir, script_identity)?;
let res = (move || {
let mut buf = Vec::<u8>::new();
{
let mut ar = tar::Builder::new(&mut buf);
ar.mode(tar::HeaderMode::Deterministic);
ar.follow_symlinks(false);
ar.append_dir_all(".", source_dir)?;
ar.finish()?;
}
#[cfg_attr(feature = "xz", allow(unused_mut))]
let mut archive = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(archive)?;
#[cfg(feature = "xz")]
{
let mut xz_write = xz2::write::XzEncoder::new(archive, 3);
std::io::copy(&mut &*buf, &mut xz_write)?;
xz_write.finish()?.close()
}
#[cfg(not(feature = "xz"))]
{
use std::io::Write;
archive.write_all(&buf)?;
archive.close()
}
})();
#[cfg(not(windows))]
std::fs::remove_dir_all(meta_dir)?;
#[cfg(windows)]
std::fs::remove_dir_all(meta_dir).ok();
res
}
const META_DIR_NAME: &str = "__gitoxide_meta__";
const META_IDENTITY: &str = "identity";
const META_GIT_VERSION: &str = "git-version";
fn populate_meta_dir(destination_dir: &Path, script_identity: u32) -> std::io::Result<PathBuf> {
let meta_dir = destination_dir.join(META_DIR_NAME);
std::fs::create_dir_all(&meta_dir)?;
std::fs::write(
meta_dir.join(META_IDENTITY),
format!("{}-{}", script_identity, family_name()).as_bytes(),
)?;
std::fs::write(
meta_dir.join(META_GIT_VERSION),
std::process::Command::new(GIT_PROGRAM)
.arg("--version")
.output()?
.stdout,
)?;
Ok(meta_dir)
}
fn extract_archive(
archive: &Path,
destination_dir: &Path,
required_script_identity: u32,
needs_archive: bool,
) -> std::io::Result<(u32, Option<String>)> {
let archive_buf: Vec<u8> = {
let mut buf = Vec::new();
#[cfg_attr(feature = "xz", allow(unused_mut))]
let mut input_archive = std::fs::File::open(archive)?;
if !needs_archive && env::var_os("GIX_TEST_IGNORE_ARCHIVES").is_some() {
return Err(std::io::Error::other(format!(
"Ignoring archive at '{}' as GIX_TEST_IGNORE_ARCHIVES is set.",
archive.display()
)));
}
#[cfg(feature = "xz")]
{
let mut decoder = xz2::bufread::XzDecoder::new(std::io::BufReader::new(input_archive));
std::io::copy(&mut decoder, &mut buf)?;
}
#[cfg(not(feature = "xz"))]
{
input_archive.read_to_end(&mut buf)?;
}
buf
};
let mut entry_buf = Vec::<u8>::new();
let (archive_identity, platform): (u32, _) = tar::Archive::new(std::io::Cursor::new(&mut &*archive_buf))
.entries_with_seek()?
.filter_map(std::result::Result::ok)
.find_map(|mut e: tar::Entry<'_, _>| {
let path = e.path().ok()?;
if path.parent()?.file_name()? == META_DIR_NAME && path.file_name()? == META_IDENTITY {
entry_buf.clear();
e.read_to_end(&mut entry_buf).ok()?;
let mut tokens = entry_buf.to_str().ok()?.trim().splitn(2, '-');
match (tokens.next(), tokens.next()) {
(Some(id), platform) => Some((id.parse().ok()?, platform.map(ToOwned::to_owned))),
_ => None,
}
} else {
None
}
})
.ok_or_else(|| std::io::Error::other("BUG: Could not find meta directory in our own archive"))
.map_err(|err| {
std::io::Error::other(format!(
"Could not extract archive at '{archive}': {err}",
archive = archive.display()
))
})?;
if archive_identity != required_script_identity {
eprintln!(
"Ignoring archive at '{}' as its generating script changed",
archive.display()
);
return Err(std::io::ErrorKind::NotFound.into());
}
for entry in tar::Archive::new(&mut &*archive_buf).entries()? {
let mut entry = entry?;
let path = entry.path()?;
if path.to_str() == Some(META_DIR_NAME) || path.parent().and_then(Path::to_str) == Some(META_DIR_NAME) {
continue;
}
entry.unpack_in(destination_dir)?;
}
Ok((archive_identity, platform))
}
fn family_name() -> &'static str {
if cfg!(windows) { "windows" } else { "unix" }
}
#[derive(Default)]
pub struct Env<'a> {
altered_vars: Vec<(&'a str, Option<OsString>)>,
}
fn set_var(var: &str, value: impl AsRef<OsStr>) {
unsafe { env::set_var(var, value) };
}
fn remove_var(var: &str) {
unsafe { env::remove_var(var) };
}
impl<'a> Env<'a> {
pub fn new() -> Self {
Env {
altered_vars: Vec::new(),
}
}
pub fn set(mut self, var: &'a str, value: impl Into<String>) -> Self {
let prev = env::var_os(var);
set_var(var, value.into());
self.altered_vars.push((var, prev));
self
}
pub fn unset(mut self, var: &'a str) -> Self {
let prev = env::var_os(var);
remove_var(var);
self.altered_vars.push((var, prev));
self
}
}
impl Drop for Env<'_> {
fn drop(&mut self) {
for (var, prev_value) in self.altered_vars.iter().rev() {
match prev_value {
Some(value) => set_var(var, value),
None => remove_var(var),
}
}
}
}
pub fn size_ok(actual_size: usize, expected_64_bit_size: usize) -> bool {
#[cfg(target_pointer_width = "64")]
return actual_size == expected_64_bit_size;
#[cfg(target_pointer_width = "32")]
return actual_size <= expected_64_bit_size;
}
#[cfg(unix)]
pub fn umask() -> u32 {
let output = std::process::Command::new("/bin/sh")
.args(["-c", "umask"])
.output()
.expect("can execute `sh -c umask`");
assert!(output.status.success(), "`sh -c umask` failed");
assert_eq!(output.stderr.as_bstr(), "", "`sh -c umask` unexpected message");
let text = output.stdout.to_str().expect("valid Unicode").trim();
u32::from_str_radix(text, 8).expect("parses as octal number")
}
fn tar_extension() -> &'static str {
if cfg!(feature = "xz") { "tar.xz" } else { "tar" }
}
#[cfg(test)]
mod tests;