use std::{ fs::read_dir, fs::read_to_string, path::{Path, PathBuf}, process::Command, process::Stdio, }; use ron::de::from_str; use serde::{Deserialize, Serialize}; use text_diff::print_diff; #[derive(Clone, Deserialize, Serialize)] pub struct Test { args: String, exit_code: i32, stdout: String, stderr: String, } fn create_cmd(bin: &Path, cwd: &Path) -> Command { let mut pb = Command::new(bin); pb.stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(Stdio::piped()) .current_dir(cwd); pb } #[test] fn assertion_tests() { let publish_bin = format!( "{}/target/debug/cargo-publish-all", env!("CARGO_MANIFEST_DIR") ); let publish_bin = PathBuf::from(publish_bin); let crates_dir = format!("{}/tests/crates", env!("CARGO_MANIFEST_DIR")); let crates_dir = PathBuf::from(crates_dir); if !publish_bin.is_file() { panic!("Tests expect debug bin to be built"); } if !crates_dir.is_dir() { panic!("Could not find tests/crates"); } let mut passed_sum = 0; let mut failed_sum = 0; for dir in read_dir(&crates_dir).unwrap() { let dir = dir.expect("Cannot access crate test"); let dir = dir.path(); if !dir.is_dir() { panic!("Found file {}, but only dirs are allowed", dir.display()); } let assertions = read_dir(dir.join("assertions")) .expect("Could not find assertions") .collect::, _>>() .expect("Could not access all assertions"); let tests: Vec = assertions .iter() .map(|a| { read_to_string(a.path()) .map_err(Into::into) .and_then(|s| from_str(&s)) }) .collect::, _>>() .expect("Could not read all assertions"); let names: Vec = assertions .iter() .map(|a| a.file_name().to_str().unwrap().to_owned()) .collect(); let crate_name = dir.file_name().unwrap().to_str().unwrap(); println!( "-> Running tests for crate `{}`...", crate_name, ); let mut passed = 0; let mut failed = 0; for (test, name) in tests.into_iter().zip(names) { println!("--> Running test `{}`...", name); let mut args = test.args.clone(); if !args.contains("--allow-dirty") { args.insert_str(0, "--allow-dirty "); } if !args.contains("--dry-run") { args.insert_str(0, "--dry-run "); } println!( "---> Running command `cargo-publish-all {}`...", args.split_whitespace() .map(|arg| format!("{} ", arg)) .collect::() ); let child = create_cmd(&publish_bin, &dir) .args(args.split_whitespace()) .spawn() .expect("Could not spawn test child"); let output = child.wait_with_output().expect("Could not join child task"); let output_code = output.status.code().unwrap(); let stdout = String::from_utf8(output.stdout).unwrap(); let stderr = String::from_utf8(output.stderr).unwrap(); let mut test = test; test.stdout = test.stdout.trim().to_owned(); test.stderr = test.stderr.trim().to_owned(); if check_output(output_code, stdout.trim(), stderr.trim(), &test) { println!("---> Test `{}` completed successfully \u{2714}", name); passed += 1; } else { let f = |code, out, err| format!("exit code: {}\nstdout:\n======\n{}\nstderr:\n======\n{}", code, out, err); let orig = f(test.exit_code, &test.stdout, &test.stderr); let found = f(output_code, &stdout, &stderr); print_diff(&orig, &found, "\n"); println!("---> Test `{}` failed for crate `{}` \u{2718}", name, crate_name); failed += 1; } } if failed == 0 { println!( "--> {} of {} tests passed \u{2714}", passed, passed + failed ); } else { println!( "--> {} of {} tests failed \u{2718}", failed, passed + failed ); } passed_sum += passed; failed_sum += failed; } if failed_sum == 0 { println!("-> All {} tests passed \u{2714}", passed_sum); } else { println!( "-> {} of {} tests failed \u{2718}", failed_sum, failed_sum + passed_sum ); panic!("Some tests failed"); } } fn check_output(exit_code: i32, stdout: &str, stderr: &str, test: &Test) -> bool { if exit_code != test.exit_code { println!("---> Error: exit codes do not match"); return false; } // TODO: regex if stdout != &test.stdout { println!("---> Error: stdout does not match"); return false; } if stderr != &test.stderr { println!("---> Error: stderr does not match"); return false; } true }