use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::{env, str}; use tempdir::TempDir; macro_rules! touch_command { ($project:ident, $file_name:literal) => { if cfg!(windows) { format!( "cmd /c copy nul {}\\{}", $project.path().display(), $file_name ) } else { format!("touch {}", $file_name) } }; } pub fn project(name: &str) -> ProjectBuilder { ProjectBuilder::new(name) } pub struct ProjectBuilder { pub name: String, } impl ProjectBuilder { fn new(name: &str) -> Self { ProjectBuilder { name: name.into() } } pub fn build(self) -> Project { let work_dir = TempDir::new(&self.name).unwrap(); let remote_dir = TempDir::new(&format!("{}_remote", &self.name)).unwrap(); let project = Project { directory: work_dir, name: self.name, remote: remote_dir, }; let touch_command = touch_command!(project, "test_file.txt"); project.batch_setup_commands(&[ "git init", "git checkout -b main", "git config push.default matching", "git remote add origin remote", &touch_command, "git add .", "git commit -am Init", ]); project } } pub struct Project { directory: TempDir, pub name: String, remote: TempDir, } impl Project { pub fn setup_command(&self, command: &str) -> TestCommandResult { let command_pieces = command.split(' ').collect::>(); let result = TestCommand::new( &self.path(), command_pieces[1..].to_vec(), command_pieces[0], ) .run(); if !result.is_success() { panic!("{}", result.failure_message("setup command to succeed")) } result } pub fn remote_setup_command(&self, command: &str) -> TestCommandResult { let command_pieces = command.split(' ').collect::>(); let result = TestCommand::new( &self.remote_path(), command_pieces[1..].to_vec(), command_pieces[0], ) .run(); if !result.is_success() { panic!( "{}", result.failure_message("remote setup command to succeed") ) } result } pub fn batch_setup_commands(&self, commands: &[&str]) { for command in commands.iter() { self.setup_command(command); } } pub fn git_clean_command(&self, command: &str) -> TestCommand { let command_pieces = command.split(' ').collect::>(); TestCommand::new(&self.path(), command_pieces, path_to_git_clean()) } pub fn path(&self) -> PathBuf { self.directory.path().into() } fn remote_path(&self) -> PathBuf { self.remote.path().into() } pub fn setup_remote(self) -> Project { self.remote_setup_command("git init"); self.remote_setup_command("git checkout -b other"); self.setup_command(&format!( "git remote set-url origin {}", self.remote_path().display() )); self.setup_command("git push origin HEAD"); self } } pub struct TestCommand { pub path: PathBuf, args: Vec, envs: Vec<(String, String)>, top_level_command: String, } impl TestCommand { fn new>(path: &Path, args: Vec<&str>, top_level_command: S) -> Self { let owned_args = args .iter() .map(|arg| arg.to_owned().to_owned()) .collect::>(); TestCommand { path: path.into(), args: owned_args, envs: vec![], top_level_command: top_level_command.into(), } } pub fn env(mut self, key: &str, value: &str) -> TestCommand { self.envs.push((key.into(), value.into())); self } pub fn run(&self) -> TestCommandResult { let mut command = Command::new(&self.top_level_command); for &(ref k, ref v) in &self.envs { command.env(&k, &v); } let output = command .args(&self.args) .current_dir(&self.path) .output() .unwrap(); TestCommandResult { output: output } } } pub struct TestCommandResult { output: Output, } impl TestCommandResult { pub fn is_success(&self) -> bool { self.output.status.success() } pub fn stdout(&self) -> &str { str::from_utf8(&self.output.stdout).unwrap() } pub fn stderr(&self) -> &str { str::from_utf8(&self.output.stderr).unwrap() } pub fn failure_message(&self, expectation: &str) -> String { format!( "Expected {}, instead found\nstdout: {}\nstderr: {}\n", expectation, self.stdout(), self.stderr() ) } } fn path_to_git_clean() -> String { let path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("target") .join("debug") .join(if cfg!(windows) { "git-clean.exe" } else { "git-clean" }) .to_str() .unwrap() .to_owned(); println!("Path is: {:?}", path); path }