diff --git a/.vscode/settings.json b/.vscode/settings.json index ae4c161b..7c0cbbad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,11 @@ { "editor.formatOnSave": true, "files.insertFinalNewline": true, - "files.trimTrailingWhitespace": true, + // We have some snapshots that have trailing whitespace, so we don't want to + // trim whitespace by default, so we set this to `false`. `rustfmt` does this for rust files (outside + // snapshots). (We could add something to pre-commit for other files if we + // wanted...) + "files.trimTrailingWhitespace": false, "rust-analyzer.cargo.features": "all", "rust-analyzer.check.command": "clippy" } diff --git a/Cargo.lock b/Cargo.lock index 38f813a5..77cd4713 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,18 @@ checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ "lazy_static", "memchr", - "regex-automata", + "regex-automata 0.1.10", + "serde", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata 0.4.7", "serde", ] @@ -65,6 +76,7 @@ dependencies = [ "proc-macro2", "serde", "serde_json", + "similar", "syn", "tempfile", "uuid", @@ -196,7 +208,7 @@ version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ - "bstr", + "bstr 0.2.17", "csv-core", "itoa 0.4.8", "ryu", @@ -292,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" dependencies = [ "aho-corasick", - "bstr", + "bstr 0.2.17", "fnv", "log", "regex", @@ -428,9 +440,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "once_cell" @@ -527,6 +539,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" + [[package]] name = "regex-syntax" version = "0.6.27" @@ -635,11 +653,11 @@ dependencies = [ [[package]] name = "similar" -version = "2.2.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" dependencies = [ - "bstr", + "bstr 1.10.0", "unicode-segmentation", ] diff --git a/Makefile b/Makefile index 26590d1f..64e647aa 100644 --- a/Makefile +++ b/Makefile @@ -15,10 +15,7 @@ cargotest: @cargo test -p insta --all-features @cargo test -p insta --no-default-features @cargo test -p insta --features redactions -- --test-threads 1 - @echo "CARGO-INSTA TESTS" - # Turn off CI flag so that cargo insta test behaves as we expect - # under normal operation - @CI=0 cargo test -p cargo-insta + @cargo test -p cargo-insta check-minver: @echo "MINVER CHECK" diff --git a/cargo-insta/Cargo.toml b/cargo-insta/Cargo.toml index 19e30b5d..1992d84b 100644 --- a/cargo-insta/Cargo.toml +++ b/cargo-insta/Cargo.toml @@ -32,3 +32,4 @@ clap = {version = "=4.1", features = ["derive", "env"]} [dev-dependencies] walkdir = "2.3.1" +similar= "2.2.1" diff --git a/cargo-insta/tests/main.rs b/cargo-insta/tests/main.rs index 8ba8a1fa..fbdc10d0 100644 --- a/cargo-insta/tests/main.rs +++ b/cargo-insta/tests/main.rs @@ -1,87 +1,468 @@ +use std::collections::HashMap; use std::env; use std::fs; -use std::path::Path; -use std::process::{Command, Output}; +use std::path::{Path, PathBuf}; +use std::process::Command; -use insta::{assert_snapshot, Settings}; -use walkdir::WalkDir; +use ignore::WalkBuilder; +use insta::assert_snapshot; +use similar::udiff::unified_diff; +use tempfile::TempDir; -struct OnDrop(Option); +struct TestProject { + files: HashMap, + /// Temporary directory where the project is created + temp_dir: TempDir, + /// Path of this repo, so we can have it as a dependency in the test project + project_path: Option, + /// File tree at start of test + file_tree: Option, +} -impl Drop for OnDrop { - fn drop(&mut self) { - let Self(f) = self; - f.take().unwrap()(); - } +fn workspace_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf() } -#[test] -fn main() { - const NO_RECURSION: &str = "CARGO_INSTA_INTEGRATION_TESTS_NO_RECURSION"; +fn target_dir() -> PathBuf { + let target_dir = env::var("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| workspace_path().join("target")) + .join("test-projects"); + fs::create_dir_all(&target_dir).unwrap(); + target_dir +} + +fn assert_success(output: &std::process::Output) { + assert!( + output.status.success(), + "Tests failed: {}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +impl TestProject { + fn new() -> Self { + Self { + files: HashMap::new(), + temp_dir: TempDir::new().unwrap(), + project_path: None, + file_tree: None, + } + } - if env::var_os(NO_RECURSION).is_some() { - return; + fn add_file>(mut self, path: P, content: String) -> Self { + self.files.insert(path.as_ref().to_path_buf(), content); + self } - // copy new tests over - - // late-bind files as they're copied to ensure cargo doesn't try to run - // these tests on the next invocation - let copied = std::cell::RefCell::new(Vec::new()); - let _on_drop = OnDrop(Some(|| { - let copied = copied.borrow(); - let copied = copied - .iter() - .filter_map(|copied| fs::remove_file(copied).err().map(|err| (copied, err))) - .collect::>(); - assert!(copied.is_empty(), "{:?}", copied); - })); - - const SRC: &str = "tests/test-input"; - const DST: &str = "tests"; - for entry in WalkDir::new(SRC) { - let entry = entry.unwrap(); - let source = entry.path(); - if source.is_dir() { - continue; + fn create(mut self) -> Self { + let project_path = self.temp_dir.path(); + let insta_path = workspace_path().join("insta"); + + // Create files and replace $PROJECT_PATH in all files + for (path, content) in &self.files { + let full_path = project_path.join(path); + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).unwrap(); + } + let replaced_content = content.replace("$PROJECT_PATH", insta_path.to_str().unwrap()); + fs::write(full_path, replaced_content).unwrap(); } - let relative_source = source.strip_prefix(SRC).unwrap(); - let destination = Path::new(DST).join(relative_source); - fs::copy(source, &destination).unwrap(); - copied.borrow_mut().push(destination); + + self.project_path = Some(project_path.to_path_buf()); + self } - // run tests and accept snapshots - let Output { - status, - stdout, - stderr, - } = Command::new(env!("CARGO_BIN_EXE_cargo-insta")) - .env(NO_RECURSION, "this value doesn't matter") - .arg("test") - .arg("--accept") - .arg("--no-ignore") + fn cmd(&mut self) -> Command { + self.file_tree = Some(self.current_file_tree()); + let project_path = self + .project_path + .as_ref() + .expect("Project has not been created yet. Call create() first."); + let mut command = Command::new(env!("CARGO_BIN_EXE_cargo-insta")); + command.current_dir(project_path); + // Use the same target directory as other tests, consistent across test + // run. This makes the compilation much faster (though do some tests + // tread on the toes of others? We could have a different cache for each + // project if so...) + command.env("CARGO_TARGET_DIR", target_dir()); + // Turn off CI flag so that cargo insta test behaves as we expect + // under normal operation + command.env("CI", "0"); + command + } + + fn diff(&self, file_path: &str) -> String { + let original_content = self.files.get(Path::new(file_path)).unwrap(); + let file_path_buf = self.project_path.as_ref().unwrap().join(file_path); + let updated_content = fs::read_to_string(&file_path_buf).unwrap(); + + unified_diff( + similar::Algorithm::Patience, + original_content, + &updated_content, + 3, + Some(( + &format!("Original: {}", file_path), + &format!("Updated: {}", file_path), + )), + ) + } + + fn current_file_tree(&self) -> String { + WalkBuilder::new(&self.temp_dir) + .filter_entry(|e| e.path().file_name() != Some(std::ffi::OsStr::new("target"))) + .build() + .filter_map(|e| e.ok()) + .map(|entry| { + let path = entry + .path() + .strip_prefix(&self.temp_dir) + .unwrap_or(entry.path()); + format!("{}{}", " ".repeat(entry.depth()), path.display()) + }) + .collect::>() + .join("\n") + } + + fn file_tree_diff(&self) -> String { + unified_diff( + similar::Algorithm::Patience, + &self.file_tree.clone().unwrap(), + self.current_file_tree().as_ref(), + 3, + Some(("Original file tree", "Updated file tree")), + ) + } +} + +#[test] +fn test_json_inline() { + let mut test_project = TestProject::new() + .add_file( + "Cargo.toml", + r#" +[package] +name = "test_json_inline" +version = "0.1.0" +edition = "2021" + +[dependencies] +insta = { path = '$PROJECT_PATH', features=["json", "redactions"] } +serde = { version = "1.0", features = ["derive"] } +"# + .to_string(), + ) + .add_file( + "src/main.rs", + r#" +use serde::Serialize; + +#[derive(Serialize)] +struct User { + id: u64, + email: String, +} + +#[test] +fn test_json_snapshot() { + let user = User { + id: 42, + email: "john.doe@example.com".into(), + }; + insta::assert_json_snapshot!(&user, { + ".id" => "[user_id]", + }, @""); +} +"# + .to_string(), + ) + .create(); + + let output = test_project + .cmd() + .args(["test", "--accept"]) .output() .unwrap(); - use std::io::Write as _; - std::io::stdout().write_all(&stdout).unwrap(); - std::io::stderr().write_all(&stderr).unwrap(); - assert!(status.success()); - let stdout = std::str::from_utf8(stdout.as_slice()).unwrap(); - assert!(stdout.contains("insta review finished")); - assert!(stdout.contains("accepted")); - - // use insta itself to assert snapshots - for entry in WalkDir::new(SRC) { - let entry = entry.unwrap(); - let filename = entry.path().strip_prefix(SRC).unwrap().to_str().unwrap(); - if let Some(snapshot) = filename.strip_suffix(".rs") { - let gen_file = Path::new(DST).join(filename); - let mut settings = Settings::clone_current(); - settings.set_input_file(&gen_file); - settings.bind(|| { - assert_snapshot!(snapshot, &fs::read_to_string(gen_file).unwrap()); - }); - } - } + + assert_success(&output); + + assert_snapshot!(test_project.diff("src/main.rs"), @r##" + --- Original: src/main.rs + +++ Updated: src/main.rs + @@ -15,5 +15,10 @@ + }; + insta::assert_json_snapshot!(&user, { + ".id" => "[user_id]", + - }, @""); + + }, @r#" + + { + + "id": "[user_id]", + + "email": "john.doe@example.com" + + } + + "#); + } + "##); +} + +#[test] +fn test_yaml_inline() { + let mut test_project = TestProject::new() + .add_file( + "Cargo.toml", + r#" +[package] +name = "test_yaml_inline" +version = "0.1.0" +edition = "2021" + +[dependencies] +insta = { path = '$PROJECT_PATH', features=["yaml", "redactions"] } +serde = { version = "1.0", features = ["derive"] } +"# + .to_string(), + ) + .add_file( + "src/main.rs", + r#" +use serde::Serialize; + +#[derive(Serialize)] +struct User { + id: u64, + email: String, +} + +#[test] +fn test_yaml_snapshot() { + let user = User { + id: 42, + email: "john.doe@example.com".into(), + }; + insta::assert_yaml_snapshot!(&user, { + ".id" => "[user_id]", + }, @""); +} +"# + .to_string(), + ) + .create(); + + let output = test_project + .cmd() + .args(["test", "--accept"]) + .output() + .unwrap(); + + assert_success(&output); + + assert_snapshot!(test_project.diff("src/main.rs"), @r##" + --- Original: src/main.rs + +++ Updated: src/main.rs + @@ -15,5 +15,9 @@ + }; + insta::assert_yaml_snapshot!(&user, { + ".id" => "[user_id]", + - }, @""); + + }, @r#" + + --- + + id: "[user_id]" + + email: john.doe@example.com + + "#); + } + "##); +} + +#[test] +fn test_utf8_inline() { + let mut test_project = TestProject::new() + .add_file( + "Cargo.toml", + r#" +[package] +name = "test_utf8_inline" +version = "0.1.0" +edition = "2021" + +[dependencies] +insta = { path = '$PROJECT_PATH' } +"# + .to_string(), + ) + .add_file( + "src/main.rs", + r#" +#[test] +fn test_non_basic_plane() { + /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @""); +} + +#[test] +fn test_remove_existing_value() { + insta::assert_snapshot!("this is the new value", @"this is the old value"); +} + +#[test] +fn test_remove_existing_value_multiline() { + insta::assert_snapshot!( + "this is the new value", + @"this is\ + this is the old value\ + it really is" + ); +} + +#[test] +fn test_trailing_comma_in_inline_snapshot() { + insta::assert_snapshot!( + "new value", + @"old value", // comma here + ); +} +"# + .to_string(), + ) + .create(); + + let output = test_project + .cmd() + .args(["test", "--accept"]) + .output() + .unwrap(); + + assert_success(&output); + + assert_snapshot!(test_project.diff("src/main.rs"), @r##" + --- Original: src/main.rs + +++ Updated: src/main.rs + @@ -1,21 +1,19 @@ + + #[test] + fn test_non_basic_plane() { + - /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @""); + + /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @"a 😀oeu"); + } + + #[test] + fn test_remove_existing_value() { + - insta::assert_snapshot!("this is the new value", @"this is the old value"); + + insta::assert_snapshot!("this is the new value", @"this is the new value"); + } + + #[test] + fn test_remove_existing_value_multiline() { + insta::assert_snapshot!( + "this is the new value", + - @"this is\ + - this is the old value\ + - it really is" + + @"this is the new value" + ); + } + + @@ -23,6 +21,6 @@ + fn test_trailing_comma_in_inline_snapshot() { + insta::assert_snapshot!( + "new value", + - @"old value", // comma here + + @"new value", // comma here + ); + } + "##); +} + +// TODO: This panics and will be fixed by #531 (and the snapshot requires +// updating; the result is not what we want) +#[ignore] +#[test] +fn test_nested_crate() { + let mut test_project = TestProject::new() + .add_file( + "Cargo.toml", + r#" +[workspace] +members = [ + "crates/member-crate", +] + +[workspace.dependencies] +insta = {path = '$PROJECT_PATH'} + + +[package] +name = "nested" +version = "0.1.0" +edition = "2021" + +[dependencies] +insta = { workspace = true } + +"# + .to_string(), + ) + .add_file( + "crates/member-crate/Cargo.toml", + r#" +[package] +name = "member-crate" +version = "0.0.0" +edition = "2021" + +[dependencies] +insta = { workspace = true } +"# + .to_string(), + ) + .add_file( + "crates/member-crate/src/lib.rs", + r#" +#[test] +fn test_member() { + insta::assert_debug_snapshot!(vec![1, 2, 3]); +} +"# + .to_string(), + ) + .add_file( + "src/main.rs", + r#" +fn main() { + println!("Hello, world!"); +} + +#[test] +fn test_root() { + insta::assert_debug_snapshot!(vec![1, 2, 3]); +} +"# + .to_string(), + ) + .create(); + + let output = test_project + .cmd() + .args(["test", "--accept", "--workspace"]) + .output() + .unwrap(); + + assert_success(&output); + + assert_snapshot!(test_project.file_tree_diff(), @r#" + --- Original file tree + +++ Updated file tree + @@ -5,5 +5,8 @@ + crates/member-crate/Cargo.toml + crates/member-crate/src + crates/member-crate/src/lib.rs + + Cargo.lock + src + + src/snapshots + + src/snapshots/nested__root.snap + src/main.rs + \ No newline at end of file + "# ); } diff --git a/cargo-insta/tests/snapshots/main__test_basic_utf8_inline.snap b/cargo-insta/tests/snapshots/main__test_basic_utf8_inline.snap deleted file mode 100644 index 5f842a82..00000000 --- a/cargo-insta/tests/snapshots/main__test_basic_utf8_inline.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: cargo-insta/tests/main.rs -expression: "&fs::read_to_string(gen_file).unwrap()" ---- -#[test] -#[rustfmt::skip] -fn test_non_basic_plane() { - /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @"a 😀oeu"); -} - -#[test] -fn test_remove_existing_value() { - insta::assert_snapshot!("this is the new value", @"this is the new value"); -} - -#[test] -fn test_remove_existing_value_multiline() { - insta::assert_snapshot!( - "this is the new value", - @"this is the new value" - ); -} - -#[test] -fn test_trailing_comma_in_inline_snapshot() { - insta::assert_snapshot!( - "new value", - @"new value", // comma here - ); -} diff --git a/cargo-insta/tests/snapshots/main__test_json_inline.snap b/cargo-insta/tests/snapshots/main__test_json_inline.snap deleted file mode 100644 index ca186b36..00000000 --- a/cargo-insta/tests/snapshots/main__test_json_inline.snap +++ /dev/null @@ -1,59 +0,0 @@ ---- -source: cargo-insta/tests/main.rs -expression: "&fs::read_to_string(gen_file).unwrap()" ---- -use serde::Serialize; - -#[derive(Serialize)] -struct User { - id: u64, - email: String, -} - -#[test] -fn test_json_snapshot() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_json_snapshot!(&user, { - ".id" => "[user_id]", - }, @r#" - { - "id": "[user_id]", - "email": "john.doe@example.com" - } - "#); -} - -#[test] -fn test_json_snapshot_trailing_comma() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_compact_json_snapshot!( - &user, - @r#"{"id": 42, "email": "john.doe@example.com"}"#, - ); -} - -#[test] -fn test_json_snapshot_trailing_comma_redaction() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_json_snapshot!( - &user, - { - ".id" => "[user_id]", - }, - @r#" - { - "id": "[user_id]", - "email": "john.doe@example.com" - } - "#, - ); -} diff --git a/cargo-insta/tests/snapshots/main__test_yaml_inline.snap b/cargo-insta/tests/snapshots/main__test_yaml_inline.snap deleted file mode 100644 index 3d739b77..00000000 --- a/cargo-insta/tests/snapshots/main__test_yaml_inline.snap +++ /dev/null @@ -1,45 +0,0 @@ ---- -source: cargo-insta/tests/main.rs -expression: "&fs::read_to_string(gen_file).unwrap()" ---- -use serde::Serialize; - -#[derive(Serialize)] -struct User { - id: u64, - email: String, -} - -#[test] -fn test_yaml_snapshot() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_yaml_snapshot!(&user, { - ".id" => "[user_id]", - }, @r#" - --- - id: "[user_id]" - email: john.doe@example.com - "#); -} - -#[test] -fn test_yaml_snapshot_trailing_comma() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_yaml_snapshot!( - &user, - { - ".id" => "[user_id]", - }, - @r#" - --- - id: "[user_id]" - email: john.doe@example.com - "#, - ); -} diff --git a/cargo-insta/tests/test-input/test_basic_utf8_inline.rs b/cargo-insta/tests/test-input/test_basic_utf8_inline.rs deleted file mode 100644 index 6ad28d3b..00000000 --- a/cargo-insta/tests/test-input/test_basic_utf8_inline.rs +++ /dev/null @@ -1,28 +0,0 @@ -#[test] -#[rustfmt::skip] -fn test_non_basic_plane() { - /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @""); -} - -#[test] -fn test_remove_existing_value() { - insta::assert_snapshot!("this is the new value", @"this is the old value"); -} - -#[test] -fn test_remove_existing_value_multiline() { - insta::assert_snapshot!( - "this is the new value", - @"this is\ - this is the old value\ - it really is" - ); -} - -#[test] -fn test_trailing_comma_in_inline_snapshot() { - insta::assert_snapshot!( - "new value", - @"old value", // comma here - ); -} diff --git a/cargo-insta/tests/test-input/test_json_inline.rs b/cargo-insta/tests/test-input/test_json_inline.rs deleted file mode 100644 index ba118dd0..00000000 --- a/cargo-insta/tests/test-input/test_json_inline.rs +++ /dev/null @@ -1,45 +0,0 @@ -use serde::Serialize; - -#[derive(Serialize)] -struct User { - id: u64, - email: String, -} - -#[test] -fn test_json_snapshot() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_json_snapshot!(&user, { - ".id" => "[user_id]", - }, @""); -} - -#[test] -fn test_json_snapshot_trailing_comma() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_compact_json_snapshot!( - &user, - @"", - ); -} - -#[test] -fn test_json_snapshot_trailing_comma_redaction() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_json_snapshot!( - &user, - { - ".id" => "[user_id]", - }, - @"", - ); -} diff --git a/cargo-insta/tests/test-input/test_yaml_inline.rs b/cargo-insta/tests/test-input/test_yaml_inline.rs deleted file mode 100644 index 0530c323..00000000 --- a/cargo-insta/tests/test-input/test_yaml_inline.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::Serialize; - -#[derive(Serialize)] -struct User { - id: u64, - email: String, -} - -#[test] -fn test_yaml_snapshot() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_yaml_snapshot!(&user, { - ".id" => "[user_id]", - }, @""); -} - -#[test] -fn test_yaml_snapshot_trailing_comma() { - let user = User { - id: 42, - email: "john.doe@example.com".into(), - }; - insta::assert_yaml_snapshot!( - &user, - { - ".id" => "[user_id]", - }, - @"", - ); -}