mit_hook_test_helper/
lib.rs

1//! Tools for making tests less filled with boilerplate
2
3#![warn(clippy::nursery)]
4#![deny(
5    unused,
6    nonstandard_style,
7    future_incompatible,
8    missing_copy_implementations,
9    missing_debug_implementations,
10    missing_docs,
11    clippy::cargo,
12    clippy::complexity,
13    clippy::correctness,
14    clippy::perf,
15    clippy::style,
16    clippy::suspicious,
17    clippy::pedantic,
18    non_fmt_panics
19)]
20#![allow(clippy::multiple_crate_versions)]
21
22use std::{
23    env,
24    error::Error,
25    fmt,
26    fmt::{Display, Formatter},
27    path::{Path, PathBuf},
28    process::{Command, Output},
29    str,
30    time::Duration,
31};
32
33use git2::{Config, Repository};
34use tempfile::TempDir;
35
36/// Run a specific hook binary
37///
38/// # Panics
39///
40/// If the cargo command fails to run or for some reason running the hook fails
41#[must_use]
42pub fn run_hook(working_dir: &Path, package: &str, arguments: Vec<&str>) -> Output {
43    let toml_path = calculate_cargo_toml_path(package);
44    let mut cargo_arguments = vec![
45        "run",
46        "--locked",
47        "--quiet",
48        "--manifest-path",
49        &toml_path,
50        "--",
51    ];
52    cargo_arguments.extend(arguments);
53
54    Command::new("cargo")
55        .current_dir(working_dir)
56        .args(cargo_arguments)
57        .output()
58        .expect("failed to execute process")
59}
60
61#[derive(Debug)]
62struct PathError;
63
64impl Error for PathError {}
65
66impl Display for PathError {
67    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
68        write!(f, "Path not found")
69    }
70}
71/// set the co-authors via the git binary
72///
73/// # Panics
74///
75/// If the git binary fails to run for some reason, or it fails to set the
76/// configuration due to a readonly filesystem or similar.
77pub fn set_co_author(working_dir: &Path, author_name: &str, author_email: &str, index: i64) {
78    Command::new("git")
79        .current_dir(working_dir)
80        .arg("config")
81        .arg("--local")
82        .arg(format!("mit.author.coauthors.{index}.name"))
83        .arg(author_name)
84        .output()
85        .expect("failed to execute process");
86    Command::new("git")
87        .current_dir(working_dir)
88        .arg("config")
89        .arg("--local")
90        .arg(format!("mit.author.coauthors.{index}.email"))
91        .arg(author_email)
92        .output()
93        .expect("failed to execute process");
94}
95
96/// Set the authors expires time via the git binary
97///
98/// # Panics
99///
100/// If the git binary fails to execute, for example if it was not found or was
101/// broken in some way
102pub fn set_author_expires(expiration_time: Duration, working_dir: &Path) {
103    let now = format!("{}", expiration_time.as_secs());
104    Command::new("git")
105        .current_dir(working_dir)
106        .arg("config")
107        .arg("--local")
108        .arg("--type")
109        .arg("expiry-date")
110        .arg("mit.author.expires")
111        .arg(now)
112        .output()
113        .expect("failed to execute process");
114}
115
116/// # Panics
117///
118/// if it can't calculate the path to the cargo toml
119#[must_use]
120pub fn calculate_cargo_toml_path(package: &str) -> String {
121    let boxed_path_error = || Box::from(PathError);
122    let parent_directory = |x: PathBuf| x.parent().ok_or_else(boxed_path_error).map(PathBuf::from);
123    let bin_root = |x: PathBuf| x.join(package);
124    let cargo_toml = |x: PathBuf| x.join("Cargo.toml");
125    let path_buf_to_string = |x: PathBuf| x.to_str().ok_or_else(boxed_path_error).map(String::from);
126
127    env::current_exe()
128        .map_err(Box::<dyn Error>::from)
129        .and_then(parent_directory)
130        .and_then(parent_directory)
131        .and_then(parent_directory)
132        .and_then(parent_directory)
133        .map(bin_root)
134        .map(cargo_toml)
135        .and_then(path_buf_to_string)
136        .unwrap()
137}
138
139/// Make a config object on a repo in a temporary directory
140///
141/// # Panics
142///
143/// Panics on failure to create a temporary directory, to initialize a git repo
144/// (for example, if the filesystem was readonly) or to get the configuration if
145/// the configuration was malformed
146#[must_use]
147pub fn make_config() -> Config {
148    let add_repository_to_path = |x: PathBuf| x.join("repository");
149    TempDir::new()
150        .map(TempDir::keep)
151        .map(add_repository_to_path)
152        .map(Repository::init)
153        .expect("Failed to initialise the repository")
154        .expect("Failed create temporary directory")
155        .config()
156        .expect("Failed to get configuration")
157}
158
159/// # Panics
160///
161/// Panics on failed test
162pub fn assert_output(
163    output: &Output,
164    expected_stdout: &str,
165    expected_stderr: &str,
166    expect_success: bool,
167) {
168    let stdout = str::from_utf8(&output.stdout).expect("stdout couldn't be parsed");
169    let stderr = str::from_utf8(&output.stderr).expect("stderr couldn't be parsed");
170    assert_eq!(
171        stdout,
172        expected_stdout,
173        "Expected stdout to be {:?}, instead it contained {:?} stderr {:?} status {:?}",
174        expected_stdout,
175        stdout,
176        stderr,
177        output.status.code()
178    );
179    assert_eq!(
180        stderr,
181        expected_stderr,
182        "Expected stderr to {:?}, instead it contained {:?} stderr {:?} status {:?}",
183        expected_stderr,
184        stderr,
185        stdout,
186        output.status.code()
187    );
188    assert_eq!(
189        output.status.success(),
190        expect_success,
191        "Expected status to be {:?}, instead it was {:?}  stdout {:?} stderr {:?}",
192        expect_success,
193        &output.status.code(),
194        stdout,
195        stderr
196    );
197}
198
199/// Get working directory
200///
201/// This is a new temporary directory with a git repo in it
202///
203/// # Panics
204///
205/// Panics on failed creation of temporary directory, or on initialising git
206/// repo (for example if filesystem is read only)
207#[must_use]
208pub fn setup_working_dir() -> PathBuf {
209    let add_repository = |x: PathBuf| x.join("repository");
210    let temp = TempDir::new()
211        .map(TempDir::keep)
212        .map(add_repository)
213        .expect("Unable to make path");
214    Repository::init(&temp).expect("Couldn't create repo");
215
216    temp
217}