blob: a48d4cf457bb166a32ebb242116e6d73e991e021 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Helpers for writing test cases.
use {
anyhow::{Context, Result},
std::{
ffi::OsString,
fs::{create_dir_all, metadata, set_permissions, OpenOptions},
io::Write,
os::unix::fs::PermissionsExt,
path::PathBuf,
process,
},
tempfile::TempDir,
};
const USER_READ_EXECUTE: u32 = 0o500;
/// Provide temporary HOME, PATH env settings, removing the directory and
/// restoring the env on drop.
///
/// It's strongly advised to use with some form of test serialization such as
/// the serialize_test crate. The env is global to the process, so running tests
/// with env changes in parallel will be confusing.
#[cfg(not(target_os = "fuchsia"))]
pub struct TempTestEnv {
/// Path to the original bash (outside of this temp env).
pub bash_shell: PathBuf,
/// Path to the user home directory (outside of this temp env).
pub old_home: Option<OsString>,
/// Original $PATH env var (outside of this temp env).
pub old_path: Option<OsString>,
/// Path to the original bin directory (outside of this temp env).
pub bin: PathBuf,
/// Path to the original etc directory (outside of this temp env).
pub etc: PathBuf,
/// Path fake home directory (inside of this temp env).
pub home: PathBuf,
/// Path to the root directory of the test env (this temp env).
pub root: TempDir,
/// Whether to keep the temp directory contents after execution.
pub keep: bool,
}
impl TempTestEnv {
/// Create a temp directory and override the HOME and PATH env vars.
///
/// Env settings restored on drop, but the temp directory (and contents)
/// will be kept (not removed on drop).
/// Intended for debugging.
pub fn keep() -> Result<Self> {
TempTestEnv::new_with_options(true)
}
/// Create a temp directory and override the HOME and PATH env vars.
///
/// Env settings restored on drop.
pub fn new() -> Result<Self> {
TempTestEnv::new_with_options(false)
}
/// Create a temp directory and override the HOME and PATH env vars.
///
/// Env settings restored on drop.
///
/// `keep` - if true temp dir will not be removed on drop.
pub fn new_with_options(keep: bool) -> Result<Self> {
let bash_shell = which("bash")?;
let old_home = std::env::var_os("HOME");
let old_path = std::env::var_os("PATH");
let root = TempDir::new()?;
let bin = root.path().join("bin");
create_dir_all(&bin)?;
let etc = root.path().join("etc");
create_dir_all(&etc)?;
let home = root.path().join("home").join("test_user");
create_dir_all(&home)?;
std::env::set_var("HOME", &home.as_path());
std::env::set_var("PATH", &bin.as_path());
Ok(Self { bash_shell, old_home, old_path, bin, etc, home, root, keep })
}
/// Create a test exe in the temp bin directory with `contents`.
///
/// The shebang line (e.g. `#!/usr/bin/env bash\n`) is added automatically.
pub fn create_bash_exe(&self, name: &str, contents: &str) -> Result<()> {
let path = self.bin.join(name);
OpenOptions::new()
.create(true)
.write(true)
.open(&path)
.context("create_no_op_exe open")?
.write(format!("#!{}\n{}", &self.bash_shell.display(), &contents).as_bytes())
.context("create_no_op_exe file")?;
let mut permissions = metadata(&path).context("create_no_op_exe metadata")?.permissions();
permissions.set_mode(USER_READ_EXECUTE);
set_permissions(&path, permissions).context("create_no_op_exe set permissions")?;
Ok(())
}
/// Create a test exe (that does nothing) in the temp bin directory.
pub fn create_no_op_exe(&self, name: &str) -> Result<()> {
self.create_bash_exe(name, &"")
}
/// executable, based on the original (non-test) PATH.
pub fn original(&self, cmd: &str) -> Result<String> {
let old_path: OsString = self.old_path.as_ref().unwrap().into();
let output = process::Command::new("/usr/bin/env")
.arg("which")
.arg(cmd)
.env("PATH", old_path)
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
}
impl Drop for TempTestEnv {
fn drop(&mut self) {
if let Some(s) = &self.old_home {
std::env::set_var("HOME", s);
} else {
std::env::remove_var("HOME");
}
if let Some(s) = &self.old_path {
std::env::set_var("PATH", s);
} else {
std::env::remove_var("PATH");
}
if self.keep {
let owned = std::mem::replace(
&mut self.root,
TempDir::new().expect("Need to replace temp directory to keep it (keep() failed)."),
);
let path = owned.into_path();
let notes = format!(
"Keeping temp dir {:?} for debugging: \
Consider using `rm -rf {:?}` when you're done.",
path, path
);
if std::thread::panicking() {
println!("{}", notes);
return;
}
// This panic serves two purposes:
// - Causes the test to show console output (successful tests don't
// normally display the stderr/stdout output).
// - Deters checking in tests that use `keep()` to avoid creating
// debris on CI/CQ test machines. (Tip: do not check in tests that
// use `keep()`.
panic!("Using TempTestEnv::keep() intentionally panics when dropped. {}", notes);
// Note: the 'path' (temp directory) is intentionally not removed.
}
}
}
/// Determine the path to an executable based on the current PATH.
pub fn which(cmd: &str) -> Result<PathBuf> {
let output = process::Command::new("/usr/bin/env").arg("which").arg(cmd).output()?;
Ok(PathBuf::from(String::from_utf8_lossy(&output.stdout).trim()))
}
#[cfg(test)]
mod test {
use {super::*, serial_test::serial, std::process::Command};
#[test]
#[serial]
fn test_test_env() {
let test_env = TempTestEnv::new().expect("create test env");
assert!(test_env.bin.exists());
assert!(test_env.etc.exists());
assert!(test_env.home.exists());
assert!(test_env.root.path().exists());
assert_eq!(std::env::var_os("HOME"), Some(OsString::from(&test_env.home)));
assert_eq!(std::env::var_os("PATH"), Some(OsString::from(&test_env.bin)));
let fake_exe_path = test_env.bin.join("fake-exe");
assert!(!fake_exe_path.exists());
test_env.create_no_op_exe("fake-exe").expect("create exe");
assert!(fake_exe_path.exists());
let mut child = Command::new("fake-exe").spawn().expect("run fake-exe");
assert_eq!(child.wait().expect("wait").code(), Some(0));
}
#[test]
#[serial]
fn test_no_residue() {
let test_path: PathBuf;
let original_home = std::env::var_os("HOME");
let original_path = std::env::var_os("PATH");
{
let test_env = TempTestEnv::new().expect("create test env");
test_path = test_env.root.path().to_path_buf();
assert!(test_path.exists());
assert_ne!(original_home, std::env::var_os("HOME"));
assert_ne!(original_path, std::env::var_os("PATH"));
}
assert!(!test_path.display().to_string().is_empty());
assert!(!test_path.exists());
assert_eq!(original_home, std::env::var_os("HOME"));
assert_eq!(original_path, std::env::var_os("PATH"));
}
#[test]
#[serial]
fn test_original() {
let test_env = TempTestEnv::new().expect("create test env");
let path = test_env.original("bash").expect("path");
assert!(!path.is_empty());
}
#[test]
#[serial]
fn test_which() {
let path = which("bash").expect("which");
assert!(path.is_absolute());
assert!(path.has_root());
}
#[test]
#[serial]
#[should_panic(expected = "panics when dropped")]
fn test_keep_panic() {
TempTestEnv::keep().expect("create test env");
}
#[test]
#[serial]
#[should_panic(expected = "The test panic.")]
fn test_panic() {
// Create named var to extend life to end of block.
let _test_env = TempTestEnv::keep().expect("create test env");
panic!("The test panic.");
}
}