use lazy_static::lazy_static;
use rand::{thread_rng, Rng};
use regex::Regex;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use tempfile::Builder;
use tempfile::TempDir;
pub struct TestSentinel {
dir: Option<TempDir>,
env: HashMap<String, String>,
prog_name: String,
}
impl Drop for TestSentinel {
fn drop(&mut self) {
self.run_cmd("cargo", &["clean", "-p", &self.prog_name]);
if env::var("DO_NOT_ERASE_TESTS").is_ok() {
let _ = self.dir.take().unwrap().into_path();
}
}
}
pub struct ManifestParts {
tag: String,
distance: usize,
commit: String,
#[allow(dead_code)]
date: String,
dirty: Option<usize>,
}
lazy_static! {
static ref MANIFEST_RE: Regex = Regex::new(
r"^([^ ]+) \(([0-9a-f]{9}) (\d{4}-\d\d-\d\d)\)(?: dirty (\d+) modifications?)?$"
)
.unwrap();
static ref TAG_WITH_DISTANCE: Regex = Regex::new(r"^(.+)\+(\d+)$").unwrap();
}
fn test_base_dir() -> PathBuf {
let mut base = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
base.push("tests");
base.push("git-testament");
std::fs::create_dir_all(&base).expect("Unable to create test base directory");
base
}
pub fn prep_test(name: &str) -> TestSentinel {
let outdir = Builder::new()
.prefix(&format!("test-{name}-"))
.tempdir_in(test_base_dir())
.expect("Unable to create temporary directory for test");
let mut rng = thread_rng();
let mut name = (0..10)
.map(|_| rng.sample(rand::distributions::Alphanumeric))
.map(|c| c as char)
.collect::<String>();
name.make_ascii_lowercase();
let name = format!("gtt-{name}");
fs::create_dir(outdir.path().join("src")).expect("Unable to make src/ dir");
fs::copy(
concat!(env!("CARGO_MANIFEST_DIR"), "/test-template/src/main.rs"),
outdir.path().join("src/main.rs"),
)
.expect("Unable to copy main.rs in");
let toml = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test-template/Cargo.toml.in"
));
let toml = toml.replace("name = \"test2\"", &format!("name = \"{name}\""));
fs::write(
outdir.path().join("Cargo.toml"),
format!(
"{}\ngit-testament = {{ path=\"{}\" }}\n",
toml,
env::var("CARGO_MANIFEST_DIR")
.unwrap_or_else(|_| ".".to_owned())
.replace('\\', "\\\\")
),
)
.expect("Unable to write Cargo.toml for test");
println!(
"Wrote test Cargo.toml:\n{}",
fs::read_to_string(outdir.path().join("Cargo.toml"))
.expect("Cannot re-read Cargo.toml for test")
);
fs::create_dir(outdir.path().join(".cargo")).expect("Unable to make .cargo/");
fs::write(
outdir.path().join(".cargo/config"),
format!(
"[build]\ntarget-dir=\"{}/target\"",
env::var("CARGO_MANIFEST_DIR")
.unwrap_or_else(|_| "..".to_owned())
.replace('\\', "\\\\")
),
)
.expect("Unable to write .cargo/config");
TestSentinel {
dir: Some(outdir),
prog_name: name,
env: HashMap::new(),
}
}
impl TestSentinel {
pub fn setenv(&mut self, key: &str, value: &str) {
self.env.insert(key.to_owned(), value.to_owned());
}
pub fn run_cmd(&self, cmd: &str, args: &[&str]) -> bool {
let mut child = Command::new(cmd);
child.args(args).env(
"GIT_CEILING_DIRECTORIES",
self.dir.as_ref().unwrap().path().parent().unwrap(),
);
for (key, value) in self.env.iter() {
child.env(key, value);
}
let child = child
.current_dir(self.dir.as_ref().unwrap().path())
.stdin(Stdio::null())
.output()
.expect("Unable to run subcommand");
if !child.status.success() {
println!("Failed to run {cmd} {args:?}");
println!("Status was: {:?}", child.status.code());
println!("Stdout was:\n{:?}", String::from_utf8(child.stdout));
println!("Stderr was:\n{:?}", String::from_utf8(child.stderr));
}
child.status.success()
}
pub fn run_cmds(&self, cmds: &[(&str, &[&str])]) -> bool {
cmds.iter().all(|(cmd, args)| self.run_cmd(cmd, args))
}
pub fn basic_git_init(&self) -> bool {
self.run_cmds(&[
("git", &["init"]),
("git", &["config", "user.name", "Git Testament Test Suite"]),
(
"git",
&["config", "user.email", "git.testament@digital-scurf.org"],
),
("git", &["config", "commit.gpgsign", "false"]),
])
}
pub fn get_output(&self, cmd: &str, args: &[&str]) -> Option<String> {
let res = Command::new(cmd)
.env(
"GIT_CEILING_DIRECTORIES",
self.dir.as_ref().unwrap().path().parent().unwrap(),
)
.current_dir(self.dir.as_ref().unwrap().path())
.args(args)
.stdin(Stdio::null())
.output()
.expect("Unable to run subcommand");
if res.status.success() {
String::from_utf8(res.stdout).ok()
} else {
println!(
"Attempt to get output of {} {:?} failed: {:?}",
cmd,
args,
res.status.code()
);
println!("Output: {:?}", String::from_utf8(res.stdout));
println!("Error: {:?}", String::from_utf8(res.stderr));
None
}
}
pub fn get_manifest(&self) -> Option<String> {
self.get_output(
&format!(
"{}/target/debug/{}",
env::var("CARGO_MANIFEST_DIR").expect("Unable to run without CARGO_MANIFEST_DIR"),
self.prog_name
),
&[],
)
}
pub fn get_manifest_parts(&self) -> ManifestParts {
let output = self
.get_manifest()
.expect("Unable to retrieve full manifest support");
let first = output
.lines()
.next()
.expect("Unable to retrieve manifest line");
let caps = MANIFEST_RE
.captures(first)
.unwrap_or_else(|| panic!("Unable to parse manifest line: '{first}'"));
let (tag, distance) = if let Some(tcaps) =
TAG_WITH_DISTANCE.captures(caps.get(1).expect("No tag captures?").as_str())
{
(
tcaps.get(1).expect("No tag capture?").as_str().to_owned(),
tcaps
.get(2)
.expect("No distance capture?")
.as_str()
.parse::<usize>()
.expect("Unable to parse distance"),
)
} else {
(caps.get(1).unwrap().as_str().to_owned(), 0usize)
};
let dirty = caps.get(4).map(|dirtycap| {
dirtycap
.as_str()
.parse::<usize>()
.expect("Unable to parse dirty count")
});
ManifestParts {
tag,
distance,
commit: caps
.get(2)
.expect("Unable to extract commit")
.as_str()
.to_owned(),
date: caps
.get(3)
.expect("Unable to extract date")
.as_str()
.to_owned(),
dirty,
}
}
#[allow(dead_code)]
pub fn assert_manifest_exact(&self, manifest: &str) {
let output = self
.get_manifest()
.expect("Unable to retrieve full manifest output");
let first = output
.lines()
.next()
.expect("Unable to retrieve manifest line");
assert_eq!(first, manifest);
}
pub fn assert_manifest_parts(
&self,
tagname: &str,
distance: usize,
_date: &str,
dirty: Option<usize>,
) {
let manifest = self.get_manifest_parts();
let curcommit = self
.get_output("git", &["rev-parse", "HEAD"])
.expect("Unable to get HEAD commit");
assert_eq!(manifest.tag, tagname);
assert_eq!(manifest.distance, distance);
assert_eq!(&curcommit[..manifest.commit.len()], manifest.commit);
assert_eq!(dirty, manifest.dirty);
}
pub fn assert_manifest_contains(&self, substr: &str) {
let manifest = self.get_manifest().expect("Unable to retrieve manifest");
println!("Retrieved manifest: {manifest:?}");
println!("Does it contain: {substr:?}");
assert!(manifest.contains(substr));
}
pub fn dirty_code(&self) {
let main_rs = self.dir.as_ref().unwrap().path().join("src/main.rs");
let code = fs::read_to_string(&main_rs).expect("Unable to read code");
fs::write(main_rs, format!("{code}\n\n")).expect("Unable to write code");
}
}