k9-0.12.0/.cargo_vcs_info.json0000644000000001400000000000100114320ustar { "git": { "sha1": "7e00bf658acbf4f869bb554f64a1d2bb5c95fefe" }, "path_in_vcs": "k9" }k9-0.12.0/Cargo.toml0000644000000030040000000000100074320ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "k9" version = "0.12.0" authors = ["Aaron Abramov "] exclude = ["tests/**/*"] description = "rust testing library" readme = "README.md" license = "MIT" repository = "https://github.com/aaronabramov/k9" [dependencies.anyhow] version = "1.0.32" [dependencies.colored] version = "2" [dependencies.diff] version = "0.1" [dependencies.lazy_static] version = "1.4" [dependencies.libc] version = "0.2" [dependencies.proc-macro2] version = "1.0" features = ["span-locations"] default-features = false [dependencies.regex] version = "1.3" optional = true [dependencies.syn] version = "2.0" features = [ "full", "extra-traits", "visit", ] [dependencies.terminal_size] version = "0.2" [dev-dependencies.derive_builder] version = "0.9.0" [dev-dependencies.k9_stable] version = "0.11.7" package = "k9" [dev-dependencies.rand] version = "0.7.3" [dev-dependencies.sha2] version = "0.9.1" [dev-dependencies.strip-ansi-escapes] version = "0.1.0" [features] custom_comparison_formatters = [] default = ["regex"] k9-0.12.0/Cargo.toml.orig000064400000000000000000000016121046102023000131160ustar 00000000000000[package] name = "k9" version = "0.12.0" authors = ["Aaron Abramov "] edition = "2018" description = "rust testing library" readme = "../README.md" license = "MIT" repository = "https://github.com/aaronabramov/k9" exclude = ["tests/**/*"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = ["regex"] custom_comparison_formatters = [] [dependencies] colored = "2" diff = "0.1" lazy_static = "1.4" libc = "0.2" proc-macro2 = { version = "1.0", default-features = false, features = [ "span-locations", ] } regex = { version = "1.3", optional = true } syn = { version = "2.0", features = ["full", "extra-traits", "visit"] } terminal_size = "0.2" anyhow = "1.0.32" [dev-dependencies] rand = "0.7.3" sha2 = "0.9.1" strip-ansi-escapes = "0.1.0" derive_builder = "0.9.0" k9_stable = { version = "0.11.7", package = "k9" } k9-0.12.0/LICENSE000064400000000000000000000020561046102023000112370ustar 00000000000000MIT License Copyright (c) 2020 Aaron Abramov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. k9-0.12.0/README.md000064400000000000000000000101171046102023000115060ustar 00000000000000# K9 - Rust Testing Library [![Crates.io][crates-badge]][crates-url] [![Docs.rs][docs-badge]][docs-url] ![Rust CI](https://github.com/aaronabramov/k9/workflows/Rust%20CI/badge.svg) [crates-badge]: https://img.shields.io/crates/v/k9.svg [crates-url]: https://crates.io/crates/k9 [docs-badge]: https://docs.rs/k9/badge.svg [docs-url]: https://docs.rs/k9 ![k9_header](https://user-images.githubusercontent.com/940133/98482607-2140c200-21c8-11eb-84f0-af488323a49a.png) ## Snapshot testing + better assertions ### Available test macros - `snapshot` - `assert_equal` - `assert_greater_than` - `assert_greater_than_or_equal` - `assert_lesser_than` - `assert_lesser_than_or_equal` - `assert_matches_regex` - `assert_err_matches_regex` - `assert_matches_snapshot` - `assert_matches_inline_snapshot` - `assert_ok` - `assert_err` See [https://docs.rs/k9](https://docs.rs/k9) for API documentation ## `snapshot!()` macro Snapshot macro provides the functionality to capture the `Debug` representation of any value and make sure it does not change over time. If it does change, the test will fail and print the difference between "old" and "new" values. If the change is expected and valid, running `cargo test` with `K9_UPDATE_SNAPSHOTS=1` env variable set will automatically take the new value and insert it into the test source code file as a second argument, after which all subsequent test runs should start passing again. ![inline_snapshot_demo](https://user-images.githubusercontent.com/940133/102737400-ed030a00-430c-11eb-90ac-66d4d24c9acd.gif) ## `assert_equal!()` macro Rust already provides a good built-in test runner and a set of assertion macros like `assert!` and `assert_eq!`. They work great for for quick unit tests, but once the codebase and test suites grows to a certain point it gets harder and harder to test things and keep tests readable. For example, when testing that two structs are equal using `assert_eq!` macro the output does not provide a lot of help in understanding why exactly this test failed. ```rust #[derive(PartialEq, Debug)] struct Person { name: &'static str, age: usize, } #[test] fn test_eq() { let person1 = Person {name: "Bob", age: 12 }; let person2 = Person {name: "Alice", age: 20 }; assert_eq!(person1, person2, "These two must be the same person!"); } ``` All we get is usually a wall of text collapsed into a single line and you have to find the difference between two structs yourself. Which becomes very time consuming when structs are 10+ fields. ``` ---- eq::test_eq stdout ---- thread 'eq::test_eq' panicked at 'assertion failed: `(left == right)` left: `Person { name: "Bob", age: 12 }`, right: `Person { name: "Alice", age: 20 }`: These two must be the same person!', src/eq.rs:13:5 ``` using `k9::assert_equal` macro improves this output and prints the difference between two structs: ```rust use k9::assert_equal; assert_equal!(person1, person2, "These two must be the same person!"); ``` ![assert_equal_example](https://user-images.githubusercontent.com/940133/84608052-35310380-ae76-11ea-97fe-751ee76a7735.png) # Non-equality based assertions Testing equality is very simple and can definitely work for most of the cases, but one of the disadvantages of only using `assert!` and `assert_eq!` is the error messages when something fails. For example, if you're testing that your code produces valid URL ```rust let url = generate_some_url(); assert_eq!(URL_REGEX.is_match(url), true); ``` What you get is ``` thread 'eq::test_eq3' panicked at 'assertion failed: `(left == right)` left: `false`, right: `true`', src/eq.rs:19:5 ``` Which doesn't help much. Especially, if you're new to the code base, seeing things like `expected 'true' but got 'false'` will make you go and look at the code before you even know what the problem can be, which can be very time consuming. What we probably want to see is: ![assert_matches_regex_example](https://user-images.githubusercontent.com/940133/84608051-35310380-ae76-11ea-87c8-c7c8b9ee3903.png) Which gives us enough context on what the problem is and how to fix it without for us having to go and run/debug the test first. k9-0.12.0/src/__k9_snapshots__/lib/main.snap000064400000000000000000000000451046102023000166130ustar 00000000000000A { name: "Susan", age: 44, }k9-0.12.0/src/__k9_snapshots__/lib/tests_it_works.snap000064400000000000000000000000051046102023000207460ustar 00000000000000yoeyok9-0.12.0/src/assertions/equal.rs000064400000000000000000000050051046102023000146650ustar 00000000000000use crate::string_diff::colored_diff; use colored::*; /// Trait used to turn types into a string we can then diff to show pretty /// assertions. This allows customizations for certain types to make the /// generated strings more human readable. pub trait FormattableForComparison { fn format(&self) -> String; } #[cfg(not(feature = "custom_comparison_formatters"))] mod specialization { use super::FormattableForComparison; use std::fmt::Debug; impl FormattableForComparison for T where T: Debug, { fn format(&self) -> String { format!("{:#?}", self) } } } #[cfg(feature = "custom_comparison_formatters")] mod specialization { use super::FormattableForComparison; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::Debug; impl FormattableForComparison for T where T: Debug, { default fn format(&self) -> String { format!("{:#?}", self) } } /// Specialize for HashMaps if the key is `Ord` - doing a diff with /// sorted keys will highlight only the values that have changed /// rather than showing ordering differences. impl FormattableForComparison for &HashMap where K: Debug + Ord, V: Debug, { fn format(&self) -> String { let sorted = self.iter().collect::>(); format!("{:#?}", sorted) } } /// Specialize for HashSets if the key is `Ord` - doing a diff with /// sorted keys will highlight only the values that have changed /// rather than showing ordering differences. impl FormattableForComparison for &HashSet where T: Debug + Ord, { fn format(&self) -> String { let sorted = self.iter().collect::>(); format!("{:#?}", sorted) } } } pub fn assert_equal< T1: FormattableForComparison + PartialEq, T2: FormattableForComparison + PartialEq, >( left: T1, right: T2, fail: bool, ) -> Option { if fail { let diff_string = colored_diff(&left.format(), &right.format()) .unwrap_or_else(|| "no visual difference between values".to_string()); let message = format!( " Expected `{left_desc}` to equal `{right_desc}`: {diff_string}", left_desc = "Left".red(), right_desc = "Right".green(), diff_string = &diff_string ); Some(message) } else { None } } k9-0.12.0/src/assertions/err.rs000064400000000000000000000006421046102023000143500ustar 00000000000000use colored::*; use std::fmt::Debug; pub fn assert_err(value: Result) -> Option { if value.is_err() { None } else { Some(format!( "Expected {value_desc} to be {type_desc} Got: {value} ", value_desc = "Value".red(), type_desc = "Err(E)".green(), value = format!("{:?}", value).red(), )) } } k9-0.12.0/src/assertions/err_matches_regex.rs000064400000000000000000000026221046102023000172460ustar 00000000000000use colored::*; use regex::Regex; pub fn assert_err_matches_regex( result: Result, regex: &str, ) -> Option { let r = Regex::new(regex).unwrap(); let result_desc = "Result".red(); let err_desc = "Err(E)".red(); let format_desc = "format!(\"{:?}\", error)".yellow(); let regex_desc = "regex".green(); if let Err(err) = result { let s = format!("{:?}", err); if !r.is_match(&s) { let message = format!( "Expected {result_desc} to be {err_desc} that matches {regex_desc} when formatted with `{format_desc}`, Regex: {regex} Formatted error: {error} ", result_desc = result_desc, err_desc = err_desc, format_desc = format_desc, regex_desc = regex_desc, regex = regex.green(), error = s.red(), ); Some(message) } else { None } } else { Some(format!( "Expected {result_desc} to be {err_desc} that matches {regex_desc} when formatted with `{format_desc}`, but it was {ok_desc} Regex: {regex} ", err_desc = err_desc, regex = regex.green(), result_desc = result_desc, ok_desc = "Ok(T)".green(), regex_desc = regex_desc, format_desc = format_desc, )) } } k9-0.12.0/src/assertions/greater_than.rs000064400000000000000000000015021046102023000162170ustar 00000000000000use colored::*; use std::cmp::Ordering; use std::fmt::Debug; pub fn assert_greater_than(left: T, right: T) -> Option { let cmp = left.partial_cmp(&right); if let Some(Ordering::Greater) = cmp { None } else { let reason = match cmp { None => ",\nbut these values can't be compared", Some(Ordering::Equal) => ",\nbut they were equal", _ => "", }; Some(format!( "Expected {left_desc} value to be greater than {right_desc} value{reason} Left value: {left} Right value: {right} ", left_desc = "Left".red(), right_desc = "Right".green(), reason = reason, left = format!("{:#?}", left).red(), right = format!("{:#?}", right).green(), )) } } k9-0.12.0/src/assertions/greater_than_or_equal.rs000064400000000000000000000015721046102023000201150ustar 00000000000000use colored::*; use std::cmp::Ordering; use std::fmt::Debug; pub fn assert_greater_than_or_equal(left: T, right: T) -> Option { let cmp = left.partial_cmp(&right); match cmp { Some(Ordering::Greater) | Some(Ordering::Equal) => None, _ => { let reason = if cmp.is_none() { ",\nbut these values can't be compared" } else { "" }; Some(format!( "Expected {left_desc} value to be greater than or equal to {right_desc} value{reason} Left value: {left} Right value: {right} ", left_desc = "Left".red(), right_desc = "Right".green(), reason = reason, left = format!("{:#?}", left).red(), right = format!("{:#?}", right).green(), )) } } } k9-0.12.0/src/assertions/lesser_than.rs000064400000000000000000000014751046102023000160740ustar 00000000000000use colored::*; use std::cmp::Ordering; use std::fmt::Debug; pub fn assert_lesser_than(left: T, right: T) -> Option { let cmp = left.partial_cmp(&right); if let Some(Ordering::Less) = cmp { None } else { let reason = match cmp { None => ",\nbut these values can't be compared", Some(Ordering::Equal) => ",\nbut they were equal", _ => "", }; Some(format!( "Expected {left_desc} value to be lesser than {right_desc} value{reason} Left value: {left} Right value: {right} ", left_desc = "Left".red(), right_desc = "Right".green(), reason = reason, left = format!("{:#?}", left).red(), right = format!("{:#?}", right).green(), )) } } k9-0.12.0/src/assertions/lesser_than_or_equal.rs000064400000000000000000000015641046102023000177620ustar 00000000000000use colored::*; use std::cmp::Ordering; use std::fmt::Debug; pub fn assert_lesser_than_or_equal(left: T, right: T) -> Option { let cmp = left.partial_cmp(&right); match cmp { Some(Ordering::Less) | Some(Ordering::Equal) => None, _ => { let reason = if cmp.is_none() { ",\nbut these values can't be compared" } else { "" }; Some(format!( "Expected {left_desc} value to be lesser than or equal to {right_desc} value{reason} Left value: {left} Right value: {right} ", left_desc = "Left".red(), right_desc = "Right".green(), reason = reason, left = format!("{:#?}", left).red(), right = format!("{:#?}", right).green(), )) } } } k9-0.12.0/src/assertions/matches_regex.rs000064400000000000000000000010101046102023000163640ustar 00000000000000use colored::*; use regex::Regex; pub fn assert_matches_regex(s: &str, regex: &str) -> Option { let r = Regex::new(regex).unwrap(); if !r.is_match(s) { let message = format!( "Expected {string_desc} to match {regex_desc} Regex: {regex} Received string: {string} ", string_desc = "string".red(), regex_desc = "regex".green(), string = s.red(), regex = regex.green(), ); Some(message) } else { None } } k9-0.12.0/src/assertions/matches_snapshot.rs000064400000000000000000000062271046102023000171300ustar 00000000000000use crate::string_diff::colored_diff; use anyhow::{Context, Result}; use colored::*; use std::path::{Path, PathBuf}; const SNAPSHOT_DIR: &str = "__k9_snapshots__"; pub fn get_snapshot_dir(source_file: &str) -> PathBuf { let mut c = Path::new(source_file).components(); let source_file_name = c.next_back().unwrap().as_os_str().to_string_lossy(); let mut p: PathBuf = c.collect(); p.push(SNAPSHOT_DIR); p.push(source_file_name.replace(".rs", "")); p } pub fn get_test_name() -> String { let t = std::thread::current(); t.name() .expect("Can't extract the test name") .to_string() .replace("::", "_") } pub fn get_test_snap_path(snapshot_dir: &Path, test_name: &str) -> PathBuf { let mut p = snapshot_dir.to_owned(); p.push(format!("{}.snap", test_name)); p } pub fn ensure_snap_dir_exists(snapshot_path: &Path) -> Result<()> { let snapshot_dir_path = snapshot_path.parent().with_context(|| { format!( "Can't determine snapshot directory. Snapshot path: `{}`", snapshot_path.display() ) })?; std::fs::create_dir_all(snapshot_dir_path).with_context(|| { format!( "Failed to create snapshot directory: `{}`", snapshot_dir_path.display() ) })?; Ok(()) } pub fn snap_internal( thing: T, _line: u32, _column: u32, file: &str, ) -> Option { let thing_str = thing.to_string(); let snapshot_dir = get_snapshot_dir(file); let test_name = get_test_name(); let relative_snap_path = get_test_snap_path(&snapshot_dir, &test_name) .display() .to_string(); let crate_root = crate::paths::find_crate_root(file).unwrap(); let mut absolute_snap_path = crate_root; absolute_snap_path.push(&relative_snap_path); let string_desc = "string".red(); let snapshot_desc = "snapshot".green(); if crate::config::CONFIG.update_mode { ensure_snap_dir_exists(&absolute_snap_path).unwrap(); std::fs::write(&absolute_snap_path, thing_str).unwrap(); None } else if absolute_snap_path.exists() { let snapshot_content = std::fs::read_to_string(absolute_snap_path.display().to_string()) .expect("can't read snapshot file"); let diff = colored_diff(&snapshot_content, &thing_str); diff.map(|diff| { format!( "Expected {string_desc} to match {snapshot_desc} stored in {file} Difference: {diff} {update_instructions} ", string_desc = string_desc, snapshot_desc = snapshot_desc, file = relative_snap_path.green(), diff = diff, update_instructions = crate::config::update_instructions(), ) }) } else { Some(format!( "Expected {string_desc} to match {snapshot_desc} stored in {file} but that snapshot file does not exist. {update_instructions} ", string_desc = string_desc, snapshot_desc = snapshot_desc, file = relative_snap_path.green(), update_instructions = crate::config::update_instructions(), )) } } k9-0.12.0/src/assertions/ok.rs000064400000000000000000000006371046102023000141750ustar 00000000000000use colored::*; use std::fmt::Debug; pub fn assert_ok(value: Result) -> Option { if value.is_ok() { None } else { Some(format!( "Expected {value_desc} to be {type_desc} Got: {value} ", value_desc = "Value".red(), type_desc = "Ok(T)".green(), value = format!("{:?}", value).red(), )) } } k9-0.12.0/src/assertions/snapshot.rs000064400000000000000000000234041046102023000154200ustar 00000000000000use crate::snapshot::ast; use crate::snapshot::source_code; use crate::snapshot::source_code::Range; use crate::types; use anyhow::{Context, Result}; use colored::*; use lazy_static::lazy_static; use std::collections::HashMap; use std::fmt::Debug; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; lazy_static! { static ref SOURCE_FILES: Mutex>> = Mutex::new(Some(HashMap::new())); static ref ATEXIT_HOOK_REGISTERED: AtomicBool = AtomicBool::new(false); } extern "C" fn libc_atexit_hook() { let files = SOURCE_FILES.lock().expect("poisoned lock").take().unwrap(); for (_path, file) in files { update_inline_snapshots(file).expect("Failed to update snapshots"); } } fn maybe_register_atexit_hook() { if !ATEXIT_HOOK_REGISTERED.swap(true, Ordering::SeqCst) { unsafe { libc::atexit(libc_atexit_hook); } } } pub fn snapshot( value: V, snapshot: Option<&str>, line: u32, _column: u32, file: &str, ) -> Option { snapshot_internal(value, snapshot, line, file) .context("snapshot!() macro failed") .unwrap() } pub fn snapshot_internal( value: V, snapshot: Option<&str>, line: u32, file: &str, ) -> Result> { let value_str = value_to_string(value); match (snapshot, crate::config::CONFIG.update_mode) { (Some(snapshot), false) => Ok(snapshot_matching_message(&value_str, snapshot)), (None, false) => Ok(Some(empty_snapshot_message(&value_str))), (_, true) => { let line = line as usize; let crate_root = crate::paths::find_crate_root(file).context("Failed to find crate root")?; let mut this_file_path = crate_root; this_file_path.push(file); if let Some(snapshot) = snapshot { let need_updating = snapshot_matching_message(&value_str, snapshot).is_some(); if need_updating { let mode = UpdateInlineSnapshotMode::Replace; schedule_snapshot_update(this_file_path, line, &value_str, mode).unwrap(); } } else { let mode = UpdateInlineSnapshotMode::Create; schedule_snapshot_update(this_file_path, line, &value_str, mode).unwrap(); }; Ok(None) } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum UpdateInlineSnapshotMode { Create, // when there's no inline snapshot Replace, // when there's an existing inline snapshot } #[derive(Debug)] pub struct InlineSnapshotUpdate { range: Range, new_value: String, mode: UpdateInlineSnapshotMode, } #[derive(Debug)] pub struct SourceFile { pub content: String, pub updates: Vec, pub path: types::FilePath, } impl SourceFile { fn new(path: types::FilePath) -> Result { let content = Self::read(&path)?; Ok(Self { path, content, updates: vec![], }) } // read source file content and panic if the file on disk changed pub fn read_and_compare(&self) -> Result<()> { let read_content = Self::read(&self.path)?; if read_content != self.content { anyhow::bail!("File content was modified during test run"); } Ok(()) } pub fn read(absolute_path: &str) -> Result { std::fs::read_to_string(absolute_path) .with_context(|| format!("Can't read source file. File path: {}", absolute_path)) } pub fn write(&self) { std::fs::write(&self.path, &self.content).unwrap(); } pub fn format(&mut self) { use std::process::Command; // Don't blow up if failed to format. TODO: find a way to // print a message about broken rustfmt let _output = Command::new("rustfmt").arg(&self.path).output(); } } pub fn with_source_file(absolute_path: &str, f: F) -> Result where F: FnOnce(&mut SourceFile) -> Result, { let mut map = SOURCE_FILES.lock().expect("poisoned lock"); let source_file = map .as_mut() .context("Missing source file")? .entry(absolute_path.to_string()) .or_insert_with(|| SourceFile::new(absolute_path.to_string()).unwrap()); f(source_file) } fn snapshot_matching_message(s: &str, snapshot: &str) -> Option { let diff = crate::string_diff::colored_diff(snapshot, s); diff.map(|diff| { format!( "Expected {string_desc} to match {snapshot_desc} Difference: {diff} {update_instructions} ", string_desc = "string".red(), snapshot_desc = "inline snapshot".green(), diff = diff, update_instructions = crate::config::update_instructions(), ) }) } fn empty_snapshot_message(s: &str) -> String { format!( "Expected {string_desc} to match {snapshot_desc} but that assertion did not have any inline snapshots. Received value: {received_value} {update_instructions} ", string_desc = "string".red(), snapshot_desc = "inline snapshot".green(), update_instructions = crate::config::update_instructions(), received_value = s.green(), ) } fn schedule_snapshot_update( file_path: PathBuf, original_line_num: usize, to_add: &str, mode: UpdateInlineSnapshotMode, ) -> Result<()> { maybe_register_atexit_hook(); with_source_file(&file_path.display().to_string(), |file| { let range = ast::find_snapshot_literal_range( &file.content, "snapshot", original_line_num, mode == UpdateInlineSnapshotMode::Replace, ) .with_context(|| { format!( "Failed to find the origin of snapshot macro call in `{}`", &file_path.display() ) })?; file.updates.push(InlineSnapshotUpdate { range, new_value: to_add.to_string(), mode, }); Ok(()) }) } fn update_inline_snapshots(mut file: SourceFile) -> Result<()> { file.read_and_compare()?; let content = file.content.clone(); let mut updates = file.updates.iter().collect::>(); updates.sort_by(|a, b| a.range.start.line.cmp(&b.range.start.line)); let parts = source_code::split_by_ranges(content, updates.iter().map(|u| &u.range).collect()); assert_eq!(parts.len(), updates.len() + 1); let mut parts_iter = parts.into_iter(); let mut updates_iter = updates.into_iter(); let mut result = String::new(); loop { match (parts_iter.next(), updates_iter.next()) { (Some(part), Some(update)) => { result.push_str(&part); let comma_separator = match update.mode { UpdateInlineSnapshotMode::Create => ", ", UpdateInlineSnapshotMode::Replace => "", }; let literal = make_literal(&update.new_value); let update_string = format!( "{comma_separator}{to_add}", comma_separator = comma_separator, to_add = literal ); result.push_str(&update_string); } (Some(part), None) => { result.push_str(&part); } (None, None) => { break; } _ => panic!("unreachable"), } } file.content = result; file.write(); file.format(); Ok(()) } /// Format the value passed into snapshot macro to a snapshot, which /// can be either compared to existing snapshot or used as a value to /// update snapshots to. fn value_to_string(value: V) -> String { let mut s = format!("{:#?}", value); // Undebug string newlines. // Formatting string as `Debug` escapes all newline characters // with `\\n` (escaped newlines), so they actually get printed as `\n` // which is super hard to read and it defeats the purpose of multiline // snapshots. This will replace them back to be displayed as newlines s = s.replace(r#"\n"#, "\n"); // Debug representation of a string also has quotes escaped, which can get // pretty noisy. We'll unescape them too. s = s.replace(r#"\""#, r#"""#); s = s.replace(r#"\'"#, r#"'"#); let mut chars = s.chars(); // `Debug` of a string also wraps the printed value in leading and trailing " // We'll trim these quotes in this case. This is a bit risky, since // not only `String` dbg can produce leading and trailing ". But currently // there's no other easy way to apply certain formatting to `String`s only if let (Some('"'), Some('"')) = (chars.next(), chars.next_back()) { s = chars.collect(); } if s.contains('\n') { // If it's a multiline string, we always add a leading and trailing `\n` // to avoid awkward macros like // snapshot!("hello // world"); s = format!("\n{}\n", s); } s } fn make_literal(s: &str) -> String { // If snapshot doesn't contain any of these characters characters // wrap the string in "" and use it as a literal // Otherwise we'd need to use r#""# literals to avoid crazy escaping rules if !s.contains('"') && !s.contains('\'') && !s.contains('\\') { return format!(r#""{}""#, s); } // Otherwise find the longest string of "##... and generate an appropriate escape sequence. let mut max = 0; let mut chars = s.chars(); while let Some(c) = chars.next() { if c == '\"' { max = max.max(chars.by_ref().take_while(|c| *c == '#').count()) } } let esc = "#".repeat(max + 1); format!(r#"r{}"{}"{}"#, esc, s, esc) } k9-0.12.0/src/assertions.rs000064400000000000000000000512131046102023000135600ustar 00000000000000use crate::utils; use colored::*; #[cfg(feature = "regex")] pub mod err_matches_regex; #[cfg(feature = "regex")] pub mod matches_regex; pub mod equal; pub mod err; pub mod greater_than; pub mod greater_than_or_equal; pub mod lesser_than; pub mod lesser_than_or_equal; pub mod matches_snapshot; pub mod ok; pub mod snapshot; #[derive(Debug)] pub struct Assertion { /// Description of what's being asserted to provide a bit more context in the error message pub description: Option, /// the name of the assertion macro that was invoked. e.g. `assert_equals` pub name: String, /// string containing all arguments passed to the assertion macro. e.g. "1 + 1, my_var" pub args_str: String, /// Assertion failure message, e.g. `expected blah blah but got blah` pub failure_message: String, } impl Assertion { pub fn get_failure_message(&self) -> String { let message = format!( " {separator} {assertion_expression} {description} {failure_message} {separator} ", assertion_expression = self.assertion_expression(), description = utils::add_linebreaks( self.description .as_ref() .unwrap_or(&"Assertion Failure!".to_string()) ), failure_message = self.failure_message, separator = utils::terminal_separator_line().dimmed(), ); message } pub fn assertion_expression(&self) -> String { format!( "{assertion_name}({args});", assertion_name = format!("{}!", self.name).yellow(), args = self.args_str ) } } #[macro_export] macro_rules! make_assertion { ($name:expr, $args_str:expr, $failure_message:expr, $description:expr,) => {{ let assertion = $crate::assertions::make_assertion_impl( $name, $args_str, $failure_message, $description, ); if let Some(assertion) = &assertion { #[allow(clippy::all)] if $crate::config::should_panic() { panic!("{}", assertion.get_failure_message()); } } assertion }}; } pub fn make_assertion_impl( name: &str, args_str: String, failure_message: Option, description: Option<&str>, ) -> Option { if let Some(failure_message) = failure_message { let assertion = Assertion { description: description.map(|d| d.into()), failure_message, name: name.to_string(), args_str, }; Some(assertion) } else { None } } pub fn initialize_colors() { if crate::config::CONFIG.force_enable_colors { colored::control::set_override(true); } } /// Asserts that two passed arguments are equal. /// Panics if they're not, using a pretty printed difference of /// [Debug](std::fmt::Debug) representations of the passed arguments. /// /// This is a drop-in replacement for [assert_eq][assert_eq] macro /// /// ``` /// use k9::assert_equal; /// /// // simple values /// assert_equal!(1, 1); /// /// #[derive(Debug, PartialEq)] /// struct A { /// name: &'static str /// } /// /// let a1 = A { name: "Kelly" }; /// let a2 = A { name: "Kelly" }; /// /// assert_equal!(a1, a2); /// ``` /// /// ```should_panic /// # use k9::assert_equal; /// # #[derive(Debug, PartialEq)] /// # struct A { /// # name: &'static str /// # } /// let a1 = A { name: "Kelly" }; /// let a2 = A { name: "Rob" }; /// /// // this will print the visual difference between two structs /// assert_equal!(a1, a2); /// ``` #[macro_export] macro_rules! assert_equal { ($left:expr, $right:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}", stringify!($left).red(), stringify!($right).green(), ); match (&$left, &$right) { (left, right) => { let fail = *left != *right; $crate::make_assertion!( "assert_equal", args_str, $crate::assertions::equal::assert_equal(left, right, fail), None, ) } } }}; ($left:expr, $right:expr, $($description:expr),*) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let description = format!($( $description ),*); let args_str = format!( "{}, {}, {}", stringify!($left).red(), stringify!($right).green(), stringify!($( $description ),* ).dimmed(), ); match (&$left, &$right) { (left, right) => { let fail = *left != *right; $crate::make_assertion!( "assert_equal", args_str, $crate::assertions::equal::assert_equal(left, right, fail), Some(&description), ) } } }}; } /// Asserts if left is greater than right. /// panics if they are not /// /// ```rust /// use k9::assert_greater_than; /// /// assert_greater_than!(2, 1); /// ``` #[macro_export] macro_rules! assert_greater_than { ($left:expr, $right:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}", stringify!($left).red(), stringify!($right).green(), ); $crate::make_assertion!( "assert_greater_than", args_str, $crate::assertions::greater_than::assert_greater_than($left, $right), None, ) }}; ($left:expr, $right:expr, $description:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}, {}", stringify!($left).red(), stringify!($right).green(), stringify!($description).dimmed(), ); $crate::make_assertion!( "assert_greater_than", args_str, $crate::assertions::greater_than::assert_greater_than($left, $right), Some(&$description), ) }}; } /// Asserts if left greater than or equal to right. /// panics if they are not /// /// ```rust /// use k9::assert_greater_than_or_equal; /// /// assert_greater_than_or_equal!(2, 1); /// assert_greater_than_or_equal!(1, 1); /// ``` #[macro_export] macro_rules! assert_greater_than_or_equal { ($left:expr, $right:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}", stringify!($left).red(), stringify!($right).green(), ); $crate::make_assertion!( "assert_greater_than_or_equal", args_str, $crate::assertions::greater_than_or_equal::assert_greater_than_or_equal($left, $right), None, ) }}; ($left:expr, $right:expr, $description:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}, {}", stringify!($left).red(), stringify!($right).green(), stringify!($description).dimmed(), ); $crate::make_assertion!( "assert_greater_than_or_equal", args_str, $crate::assertions::greater_than_or_equal::assert_greater_than_or_equal($left, $right), Some(&$description), ) }}; } /// Asserts if left is lesser than right. /// panics if they are not /// /// ```rust /// use k9::assert_lesser_than; /// /// assert_lesser_than!(1, 2); /// ``` #[macro_export] macro_rules! assert_lesser_than { ($left:expr, $right:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}", stringify!($left).red(), stringify!($right).green(), ); $crate::make_assertion!( "assert_lesser_than", args_str, $crate::assertions::lesser_than::assert_lesser_than($left, $right), None, ) }}; ($left:expr, $right:expr, $description:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}, {}", stringify!($left).red(), stringify!($right).green(), stringify!($description).dimmed(), ); $crate::make_assertion!( "assert_lesser_than", args_str, $crate::assertions::lesser_than::assert_lesser_than($left, $right), Some(&$description), ) }}; } /// Asserts if left lesser than or equal to right. /// panics if they are not /// /// ```rust /// use k9::assert_lesser_than_or_equal; /// /// assert_lesser_than_or_equal!(1, 2); /// assert_lesser_than_or_equal!(1, 1); /// ``` #[macro_export] macro_rules! assert_lesser_than_or_equal { ($left:expr, $right:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}", stringify!($left).red(), stringify!($right).green(), ); $crate::make_assertion!( "assert_lesser_than_or_equal", args_str, $crate::assertions::lesser_than_or_equal::assert_lesser_than_or_equal($left, $right), None, ) }}; ($left:expr, $right:expr, $description:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}, {}", stringify!($left).red(), stringify!($right).green(), stringify!($description).dimmed(), ); $crate::make_assertion!( "assert_lesser_than_or_equal", args_str, $crate::assertions::lesser_than_or_equal::assert_lesser_than_or_equal($left, $right), Some(&$description), ) }}; } /// Asserts that passed `&str` matches a regular expression. /// Regular expressions are compiled using `regex` crate. /// /// ```rust /// use k9::assert_matches_regex; /// /// assert_matches_regex!("1234-45", "\\d{4}-\\d{2}"); /// assert_matches_regex!("abc", "abc"); /// ```` #[cfg(feature = "regex")] #[macro_export] macro_rules! assert_matches_regex { ($s:expr, $regex:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!("{}, {}", stringify!($s).red(), stringify!($regex).green()); $crate::make_assertion!( "assert_matches_regex", args_str, $crate::assertions::matches_regex::assert_matches_regex($s, $regex), None, ) }}; ($s:expr, $regex:expr, $description:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}, {}", stringify!($s).red(), stringify!($regex).green(), stringify!($description).dimmed() ); $crate::make_assertion!( "assert_matches_regex", args_str, $crate::assertions::matches_regex::assert_matches_regex($s, $regex), Some($description), ) }}; } /// Asserts that the passed `Result` argument is an `Err` and /// and the debug string of that error matches provided regex. /// Regular expressions are compiled using `regex` crate. /// /// ```rust /// use k9::assert_err_matches_regex; /// // Borrowed from Rust by Example: https://doc.rust-lang.org/stable/rust-by-example/std/result.html /// fn divide(x: f64, y: f64) -> Result { /// if y == 0.0 { /// // This operation would `fail`, instead let's return the reason of /// // the failure wrapped in `Err` /// Err("Cannot divide by 0.") /// } else { /// // This operation is valid, return the result wrapped in `Ok` /// Ok(x / y) /// } /// } /// let division_error = divide(4.0, 0.0); /// assert_err_matches_regex!(division_error, "Cannot"); /// ``` #[cfg(feature = "regex")] #[macro_export] macro_rules! assert_err_matches_regex { ($err:expr, $regex:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!("{}, {}", stringify!($err).red(), stringify!($regex).green(),); $crate::make_assertion!( "assert_err_matches_regex", args_str, $crate::assertions::err_matches_regex::assert_err_matches_regex($err, $regex), None, ) }}; ($err:expr, $regex:expr, $context:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}, {}", stringify!($err).red(), stringify!($regex).green(), stringify!($context).dimmed(), ); $crate::make_assertion!( "assert_err_matches_regex", args_str, $crate::assertions::err_matches_regex::assert_err_matches_regex($err, $regex), Some($context), ) }}; } /// Same as [snapshot!()](./macro.snapshot.html) macro, but it takes a string as the /// only argument and stores the snapshot in a separate file instead of inlining /// it in the source code of the test. /// /// ```should_panic /// #[derive(Debug)] /// struct A<'a> { /// name: &'a str, /// age: u32 /// } /// /// let a = A { name: "Lance", age: 9 }; /// /// // When first run with `K9_UPDATE_SNAPSHOTS=1` it will /// // create `__k9_snapshots__/my_test_file/my_test.snap` file /// // with contents being the serialized value of `a`. /// // Next time the test is run, if the newly serialized value of a /// // is different from the value of that snapshot file, the assertion /// // will fail. /// k9::assert_matches_snapshot!(format!("{:#?}", a)); /// ``` #[macro_export] macro_rules! assert_matches_snapshot { ($to_snap:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let line = line!(); let column = column!(); let file = file!(); let args_str = format!("{}", stringify!($to_snap).red(),); $crate::make_assertion!( "assert_matches_snapshot", args_str, $crate::assertions::matches_snapshot::snap_internal($to_snap, line, column, file), None, ) }}; ($to_snap:expr, $description:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let line = line!(); let column = column!(); let file = file!(); let args_str = format!( "{}, {}", stringify!($to_snap).red(), stringify!($description).dimmed(), ); $crate::make_assertion!( "assert_matches_snapshot", args_str, $crate::assertions::matches_snapshot::snap_internal($to_snap, line, column, file), Some($description), ) }}; } /// Asserts if value is Ok(T). /// panics if it is not /// /// ```rust /// use k9::assert_ok; /// /// let result: Result<_, ()> = Ok(2); /// assert_ok!(result); /// ``` #[macro_export] macro_rules! assert_ok { ($left:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!("{}", stringify!($left).red()); $crate::make_assertion!( "assert_ok", args_str, $crate::assertions::ok::assert_ok($left), None, ) }}; ($left:expr, $description:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}", stringify!($left).red(), stringify!($description).dimmed(), ); $crate::make_assertion!( "assert_ok", args_str, $crate::assertions::ok::assert_ok($left), Some(&$description), ) }}; } /// Asserts if value is Err(E). /// panics if it is not /// /// ```rust /// use k9::assert_err; /// /// let result: Result<(), _> = Err("invalid path"); /// assert_err!(result); /// ``` #[macro_export] macro_rules! assert_err { ($left:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!("{}", stringify!($left).red()); $crate::make_assertion!( "assert_err", args_str, $crate::assertions::err::assert_err($left), None, ) }}; ($left:expr, $description:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let args_str = format!( "{}, {}", stringify!($left).red(), stringify!($description).dimmed(), ); $crate::make_assertion!( "assert_err", args_str, $crate::assertions::err::assert_err($left), Some(&$description), ) }}; } /// Serializes the first argument into a string and compares it with /// the second argument, which is a snapshot string that was automatically generated /// during previous test runs. Panics if the values are not equal. /// /// If second argument is missing, assertion will always fail by prompting to /// re-run the test in `update snapshots` mode. /// /// If run in `update snapshots` mode, serialization of the first argument will /// be made into a string literal and inserted into source code as the second /// argument of this macro. (It will actually modify the file in the filesystem) /// /// Typical workflow for this assertion is: /// /// ```should_panic /// // Step 1: /// // - Take a result of some computation and pass it as a single argument to the macro /// // - Run the test /// // - Test will fail prompting to re-run it in update mode /// use std::collections::BTreeMap; /// /// k9::snapshot!((1..=3).rev().enumerate().collect::>()); /// ``` /// /// ```text /// # Step 2: /// # Run tests with K9_UPDATE_SNAPSHOTS=1 env variable set /// $ K9_UPDATE_SNAPSHOTS=1 cargo test /// ``` /// /// ``` /// // Step 3: /// // After test run finishes and process exits successfully, the source code of the /// // test file will be updated with the serialized value of the first argument. /// // All subsequent runs of this test will pass /// use std::collections::BTreeMap; /// /// k9::snapshot!( /// (1..=3).rev().enumerate().collect::>(), /// " /// { /// 0: 3, /// 1: 2, /// 2: 1, /// } /// " /// ); /// ``` /// /// ```should_panic /// // If the logic behind first argument ever changes and affects the serialization /// // the test will fail and print the difference between the "old" and the "new" values /// use std::collections::BTreeMap; /// /// k9::snapshot!( /// /// remove `.rev()` /// (1..=3).enumerate().collect::>(), /// " /// { /// 0: 3, /// 1: 2, /// 2: 1, /// } /// " /// ); /// ``` /// /// The test above will now fail with the following message: /// ```text /// Difference: /// { /// - 0: 3, /// + 0: 1, /// 1: 2, /// - 2: 1, /// + 2: 3, /// } /// ``` #[macro_export] macro_rules! snapshot { ($to_snap:expr) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let line = line!(); let column = column!(); let file = file!(); let args_str = format!("{}", stringify!($to_snap).red(),); $crate::make_assertion!( "snapshot", args_str, $crate::assertions::snapshot::snapshot($to_snap, None, line, column, file), None, ) }}; ($to_snap:expr, $inline_snap:literal) => {{ use $crate::__macros__::colored::*; $crate::assertions::initialize_colors(); let line = line!(); let column = column!(); let file = file!(); let args_str = format!( "{}, {}", stringify!($to_snap).red(), stringify!($inline_snap).green(), ); $crate::make_assertion!( "snapshot", args_str, $crate::assertions::snapshot::snapshot( $to_snap, Some($inline_snap), line, column, file, ), None, ) }}; } k9-0.12.0/src/config.rs000064400000000000000000000066621046102023000126430ustar 00000000000000use colored::*; use lazy_static::lazy_static; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; pub enum BuildSystem { /// https://buck.build/ Buck, /// https://developers.facebook.com/blog/post/2021/07/01/future-of-buck/ Buck2, /// https://github.com/rust-lang/cargo Cargo, } impl BuildSystem { pub fn is_buck(&self) -> bool { match self { Self::Buck | Self::Buck2 => true, Self::Cargo => false, } } } pub struct Config { /// Configurable so we can test all assertions in our own test suite without panicking. pub assertions_will_panic: AtomicBool, /// 0 === disabled pub terminal_width_override: AtomicUsize, // Snapshot update mode pub update_mode: bool, /// What build system this project is being built with pub build_system: BuildSystem, /// Whether we should always enable colored output pub force_enable_colors: bool, } lazy_static! { pub static ref CONFIG: Config = Config { assertions_will_panic: AtomicBool::new(true), terminal_width_override: AtomicUsize::new(0), update_mode: is_update_mode(), build_system: build_system(), force_enable_colors: should_force_enable_colors(), }; } pub fn set_panic(v: bool) { CONFIG.assertions_will_panic.store(v, Ordering::Relaxed) } pub fn should_panic() -> bool { CONFIG.assertions_will_panic.load(Ordering::Relaxed) } pub fn set_terminal_with_override(width: usize) { CONFIG .terminal_width_override .store(width, Ordering::Relaxed); } pub fn terminal_width_override() -> usize { CONFIG.terminal_width_override.load(Ordering::Relaxed) } fn build_system() -> BuildSystem { if std::env::var("BUCK_BUILD_ID").is_ok() { BuildSystem::Buck } else if std::env::var("BUCK2_DAEMON_UUID").is_ok() { BuildSystem::Buck2 } else { BuildSystem::Cargo } } fn is_update_mode() -> bool { // If runtime ENV variable is set, it takes precedence std::env::var("K9_UPDATE_SNAPSHOTS").map_or(false, |_| true) } fn should_force_enable_colors() -> bool { // If we are running with buck, stdout will not be a tty and we'll lose // colored output. Detect that case so we can force enable colored // output. // If this is not set, fall back to the usual `colored` behavior. if build_system().is_buck() { return true; } if let Ok(force_colors) = std::env::var("K9_FORCE_COLORS") { match force_colors.as_str() { "1" => return true, "0" => return false, _ => (), } } false } pub fn update_instructions() -> colored::ColoredString { match CONFIG.build_system { BuildSystem::Buck => { "buck test //path/to/your/buck/target/... -- --env K9_UPDATE_SNAPSHOTS=1".yellow() } BuildSystem::Buck2 => { "buck2 test //path/to/your/buck/target/... -- --env K9_UPDATE_SNAPSHOTS=1".yellow() } BuildSystem::Cargo => { "run with `K9_UPDATE_SNAPSHOTS=1` to update/create snapshots".yellow() } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_building() { // make sure we can construct the CONFIG. this is a regression test // after i accidentally put a circular reference in the config functions // and caused everything to stall. let _b = format!("{}", CONFIG.force_enable_colors); } } k9-0.12.0/src/lib.rs000064400000000000000000000007301046102023000121320ustar 00000000000000/*! see for high level description of the library */ #![cfg_attr(feature = "custom_comparison_formatters", feature(specialization))] pub mod assertions; pub mod config; pub mod snapshot; pub mod string_diff; mod multiline_string; mod paths; mod snap; mod types; mod utils; pub use multiline_string::MultilineString; pub use snap::Snap; // re-export things so macros have access to them pub mod __macros__ { pub use colored; } k9-0.12.0/src/multiline_string.rs000064400000000000000000000022561046102023000147610ustar 00000000000000/// [assert_equal!](crate::assert_equal) takes a [std::fmt::Debug](std::fmt::Debug) trait object as an argument /// which doesn't work well with multiline strings, since newline characters will be displayed as `\n` /// For example this string: /// ``` /// let s = "A /// B /// C"; /// ``` /// /// will be printed as a single line `"A\nB\nC"`, which is not useful /// for multiline comparison diff /// /// Using this struct makes the original string serialize into a proper multiline string /// which will produce a nice line by line difference comparison when used together with /// [assert_equal!](crate::assert_equal). /// /// ```should_panic /// use k9::{MultilineString, assert_equal}; /// /// let s1 = "A\nB\nC".to_string(); /// let s2 = "A\nD\nC"; /// assert_equal!(MultilineString(s1), MultilineString::new(s2)); /// ``` #[derive(Eq, PartialEq)] pub struct MultilineString(pub String); impl MultilineString { pub fn new>(s: S) -> Self { Self(s.into()) } } /// Make diff to display string as multi-line string impl std::fmt::Debug for MultilineString { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str(&self.0) } } k9-0.12.0/src/paths.rs000064400000000000000000000132041046102023000125030ustar 00000000000000use anyhow::Result; use std::path::{Component, Path, PathBuf}; // Project root is the root of the entire project. The project might contain multiple crate and it should not // be used together with whatever `file!()` macro will return. pub fn get_project_root_path() -> PathBuf { // It seems like when it's built with Buck, PWD will always point to the // repo root, regardless of where it's run from. We'll use it as a base dir if crate::config::CONFIG.build_system.is_buck() { let pwd = std::env::var("PWD").expect( " `BUCK_BUILD_ID` or `BUCK2_DAEMON_UUID` environment variable was present, which means this project is being built with buck and relies on `PWD` env variable to contain the project root, but `PWD` wasn't there", ); return PathBuf::from(pwd); } // otherwise ask cargo for project root let project_root = std::env::var("CARGO_MANIFEST_DIR").expect("Can't get project root directory"); PathBuf::from(project_root) } // Crate root will be the root of the project + directory of one of the workspace crates (if exists) // To find this we'll need to use any `file!()` macro value to test if the file exist using // an absolute path. pub fn find_crate_root(result_of_file_macro: &str) -> Result { let project_root = get_project_root_path(); let mut without_overlap = project_root.clone(); without_overlap.push(result_of_file_macro); if without_overlap.exists() { return Ok(project_root); } let root_with_overlap_removed = remove_overlap(&project_root, result_of_file_macro)?; let mut with_overlap_removed = root_with_overlap_removed.clone(); with_overlap_removed.push(result_of_file_macro); if !with_overlap_removed.exists() { anyhow::bail!(format!( " Failed to locate the path of the source file. Project root was determined to be `{cargo_manifest_dir}` and the relative source file path given `{result_of_file_macro} Tried paths: `{without_overlap}` `{with_overlap_removed}` ", cargo_manifest_dir = project_root.display(), result_of_file_macro = result_of_file_macro, without_overlap = without_overlap.display(), with_overlap_removed = with_overlap_removed.display(), )) } Ok(root_with_overlap_removed) } // This is a hack to work around the issue with project root path resolution when // using workspaces. // // When using a `file!()` macro in a crate that is a part of a workspace it will return // a relative path from the workspace root. // At the same time CARGO_MANIFEST env variable with hold the value of the crate's Cargo.toml // path and not the workspace Cargo.toml. // // e.g. // /home // my_project/ // Cargo.toml <---- workspace definition // nested_crate/ // Cargo.toml <---------- other project's manifest // lib.rs <------- file!() macro used here // // // `file()` macro will return "my_project/nested_crate/lib.rs" // and CARGO_MANIFEST will contain "/home/my_project" // // In the end we want to find the absolute path to the file, which is // `/home/my_project/nested_crate/lib.rs` // // // There's probably a better solution for this problem, but after 20 min of research // the sketchy workaround i found is to just join two paths while also removing the // overlapping part. // // Technically this can be a bit dangerous, since the joining part may resolve in // another existing file that is not the file we're looking for (esp if trying to // resolve some generic file names like `lib.rs`) but the risk should be fairly minimal. fn remove_overlap(left: &Path, right: &str) -> Result { let right = PathBuf::from(right); let left_comps = left.components().collect::>(); let mut r_prefix = vec![]; for (i, r_comp) in right.components().enumerate() { match r_comp { Component::Normal(_n) => { r_prefix.push(r_comp); let l_suffix = &left_comps[(left_comps.len() - i - 1)..]; if l_suffix == &r_prefix[..] { let result = left_comps[..(left_comps.len() - i - 1)] .iter() .collect::(); return Ok(result); } } _ => anyhow::bail!(format!( "Invalid path component. Expected to only have normals: `{:?}`", r_comp )), } } let result = left.to_owned(); Ok(result) } #[cfg(test)] mod tests { use super::*; fn remove_overlap_helper(l: &str, r: &str) -> Result { Ok(remove_overlap(&PathBuf::from(l), r)?.display().to_string()) } #[test] fn remove_path_overlap_test() -> Result<()> { k9_stable::snapshot!( remove_overlap_helper("hello/world", "world/hello")?, r##"hello"## ); k9_stable::snapshot!( remove_overlap_helper("a/b/c/d/e/f/g", "c/d/e/f/g/h/i")?, r##"a/b"## ); k9_stable::snapshot!(remove_overlap_helper("a/b/c", "a/b/c")?, r##""##); // no overlap, similar directories k9_stable::snapshot!(remove_overlap_helper("a/b/c/d", "a/b/c")?, r##"a/b/c/d"##); k9_stable::snapshot!( remove_overlap_helper("/home/workspace/my_crate", "my_crate/my_file")?, r##"/home/workspace"## ); k9_stable::snapshot!( remove_overlap_helper("/Users/me/p/gull/gull", "gull/e2e/flow_codegen_test.rs")?, r##"/Users/me/p/gull"## ); Ok(()) } } k9-0.12.0/src/snap.rs000064400000000000000000000040561046102023000123320ustar 00000000000000use std::convert::From; use std::fmt::{Display, Formatter, Result}; use std::sync::{Arc, Mutex}; /// String with concurrent access. Allows mutation without &mut reference to itself. /// It makes passing it to different parts of tests easier when performance is not important. /// Useful for accumulating output from a system under test and later using it with /// [assert_matches_inline_snapshot](crate::assert_matches_inline_snapshot). /// /// ```rust /// let snap = k9::Snap::new(); /// let closure_captured_snap_by_ref = || { /// snap.push("a"); /// }; /// /// closure_captured_snap_by_ref(); /// closure_captured_snap_by_ref(); /// closure_captured_snap_by_ref(); // /// k9::snapshot!(snap.to_string(), "aaa"); /// ``` #[derive(Clone)] pub struct Snap(Arc>); impl Snap { #[allow(clippy::new_without_default)] pub fn new() -> Self { Self(Arc::new(Mutex::new(String::new()))) } pub fn from>(s: S) -> Self { Self(Arc::new(Mutex::new(s.into()))) } /// Push a substring pub fn push>(&self, s: S) { let mut content = self.0.lock().unwrap(); let s = s.into(); content.push_str(&s); } /// Push a substring and add a newline `\n` at the end pub fn pushln>(&self, s: S) { self.push(s); self.push("\n"); } } impl From<&Snap> for String { fn from(snap: &Snap) -> Self { snap.0.lock().unwrap().clone() } } impl Display for Snap { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "{}", self.0.lock().unwrap()) } } #[cfg(test)] mod tests { use super::*; #[test] fn snap() { let snap = Snap::from("\n"); let closure_captured_snap_by_ref = || { snap.pushln("new line"); }; closure_captured_snap_by_ref(); closure_captured_snap_by_ref(); closure_captured_snap_by_ref(); crate::assert_equal!( snap.to_string(), " new line new line new line " .to_string() ); } } k9-0.12.0/src/snapshot/ast.rs000064400000000000000000000133561046102023000140220ustar 00000000000000use super::source_code::{extract_range, LineColumn, Range}; use anyhow::{Context, Result}; use proc_macro2::{Span, TokenStream, TokenTree}; use syn::spanned::Spanned; use syn::visit::Visit; use syn::{Macro, PathSegment}; #[derive(Debug)] struct MacroVisitor { found: Option<(TokenStream, Macro)>, line: usize, macro_name: String, } impl<'ast> Visit<'ast> for MacroVisitor { fn visit_macro(&mut self, m: &'ast Macro) { let last_path_segment = m.path.segments.last(); if let Some(PathSegment { ident, .. }) = last_path_segment { if ident == &self.macro_name && ident.span().start().line == self.line { self.found.replace((m.tokens.to_owned(), m.to_owned())); } } } } /// Find the Range containing the space where snapshot literal needs to be written. /// /// If a literal already exists, it will return the range of the existing string literal. /// /// some_macro!(blah); /// ^ /// | /// 0 length range of the position where /// new snapshot needs to be written /// /// Otherwise, if the snapshot literal is there, return its range /// /// some_macro(blah, "hello") /// ^ ^ /// | | /// range pub fn find_snapshot_literal_range>( file_content: &str, macro_name: S, line_num: usize, literal_exists: bool, ) -> Result { let syntax = syn::parse_file(file_content).expect("Unable to parse file"); let macro_name = macro_name.into(); let mut macro_visitor = MacroVisitor { found: None, line: line_num, macro_name: macro_name.clone(), }; macro_visitor.visit_file(&syntax); let (tt, macro_node) = macro_visitor.found.with_context(|| { format!( "Failed to find a macro call AST node with macro name `{}!()`.\nThis macro was called on line `{}`\n\n", ¯o_name, line_num ) })?; if literal_exists { let literal = tt.into_iter().last(); if let Some(TokenTree::Literal(literal)) = literal { Ok(Range { start: LineColumn { line: literal.span().start().line, // columns might be 0 based? i'm not sure column: literal.span().start().column + 1, }, end: LineColumn { line: literal.span().end().line, column: literal.span().end().column + 1, }, }) } else { let macro_range = syn_span_to_range(macro_node.span()); let macro_code = extract_range(file_content, ¯o_range); anyhow::bail!( r#" Failed to extract a snapshot literal from a snapshot macro call. Snapshot literal must be the last argument to a macro call and must be a string literal. e.g. assert_matches_inline_snapshot!(12345, "12345"); ^ ^ | | snapshot literal Given macro call: ``` {} ``` "#, macro_code, ) } } else { let last = tt.into_iter().last().expect("must have last tokentree"); let span = last.span(); Ok(Range { start: LineColumn { line: span.end().line, column: span.end().column + 1, }, end: LineColumn { line: span.end().line, column: span.end().column + 1, }, }) } } /// Convert proc_macro2 Span struct to local Range struct, which indexes /// for Lines and Columns starting from 1 and not 0 fn syn_span_to_range(span: Span) -> Range { Range { start: LineColumn { line: span.start().line, column: span.start().column + 1, }, end: LineColumn { line: span.end().line, column: span.end().column + 1, }, } } #[cfg(test)] mod tests { use super::*; const SOURCE: &str = r##" // 1 fn main() { // 2 let hello = "world"; // 3 random_macro!(hello); // 4 hello_macro!(stuff, "literal"); // 5 wrong_macro!(stuff, not_a_literal); // 6 } "##; #[test] fn no_literal() -> Result<()> { let range = find_snapshot_literal_range(SOURCE, "random_macro", 4, false)?; k9_stable::assert_equal!(&range.start, &range.end); k9_stable::snapshot!( format!("{:?}", range), r##"Range { start: LineColumn { line: 4, column: 24 }, end: LineColumn { line: 4, column: 24 } }"## ); Ok(()) } #[test] fn literal() -> Result<()> { let range = find_snapshot_literal_range(SOURCE, "hello_macro", 5, true)?; k9_stable::snapshot!( format!("{:?}", range), r##"Range { start: LineColumn { line: 5, column: 25 }, end: LineColumn { line: 5, column: 34 } }"## ); Ok(()) } #[test] fn not_a_literal_error() { let err = find_snapshot_literal_range(SOURCE, "wrong_macro", 6, true).unwrap_err(); k9_stable::snapshot!( format!("{:?}", err), r#" Failed to extract a snapshot literal from a snapshot macro call. Snapshot literal must be the last argument to a macro call and must be a string literal. e.g. assert_matches_inline_snapshot!(12345, "12345"); ^ ^ | | snapshot literal Given macro call: ``` wrong_macro!(stuff, not_a_literal) ``` "# ); } } k9-0.12.0/src/snapshot/mod.rs000064400000000000000000000000421046102023000137760ustar 00000000000000pub mod ast; pub mod source_code; k9-0.12.0/src/snapshot/source_code.rs000064400000000000000000000130621046102023000155170ustar 00000000000000#[derive(Debug, PartialEq, Eq)] // Struct that represents a range of text in a source code file content pub struct Range { pub start: LineColumn, pub end: LineColumn, } /// Lines and Columns start form 1 /// /// 12345678910 15 20 25... /// hello_world /// /// {line: 1, column: 1} -> {line: 1, column: 6} => "hello" /// {line: 1, column: 2} -> {line: 1, column: 6} => "ello" /// {line: 1, column: 1} -> {line: 1, column: 1} => "" #[derive(Debug, PartialEq, Eq)] pub struct LineColumn { pub line: usize, pub column: usize, } /// Given a source file, extract a substring from it at the given range pub fn extract_range(s: &str, range: &Range) -> String { let mut result = String::new(); for (i, line) in s.lines().enumerate() { let line_number = i + 1; if line_number >= range.start.line && line_number <= range.end.line { if !result.is_empty() { result.push('\n'); } for (j, char) in line.chars().enumerate() { let column = j + 1; if !((line_number == range.start.line && column < range.start.column) || (line_number == range.end.line && column >= range.end.column)) { result.push(char) } } } } result } /// Given a source code file content and a Vec, split content /// into chunks while also removing the content within provided ranges, so that /// it can later be replaced with something else. pub fn split_by_ranges(content: String, ranges: Vec<&Range>) -> Vec { let mut iter = ranges.iter().peekable(); // ranges must be pre-sorted while let Some(range) = iter.next() { if let Some(next_range) = iter.peek() { #[allow(clippy::all)] if range.end.line >= next_range.start.line { panic!("overlapping ranges! can be only one inline snapshot macro per line"); } } } let mut ranges_iter = ranges.into_iter(); let mut chunks = vec![]; let mut next_chunk = String::new(); let mut next_range = ranges_iter.next(); for (i, line) in content.lines().enumerate() { let line_number = i + 1; if let Some(range) = next_range { match line_number { n if n < range.start.line => { next_chunk.push_str(line); next_chunk.push('\n'); } n if n == range.start.line => { let chars = line.chars().collect::>(); let mut chars_before = chars; let mut rest = chars_before.split_off(range.start.column - 1); let str_before: String = chars_before.iter().collect(); next_chunk.push_str(&str_before); // The range is in a single line if n == range.end.line { let chars_after = rest.split_off(range.end.column - 1 - chars_before.len()); let str_after: String = chars_after.iter().collect(); chunks.push(next_chunk); next_chunk = String::new(); next_range = ranges_iter.next(); next_chunk.push_str(&str_after); next_chunk.push('\n'); } } n if n > range.start.line && n < range.end.line => {} n if n == range.end.line => { chunks.push(next_chunk); next_chunk = String::new(); next_range = ranges_iter.next(); let mut chars = line.chars().collect::>(); let after_chars = chars.split_off(range.end.column - 1); let after_str: String = after_chars.iter().collect(); next_chunk.push_str(&after_str); next_chunk.push('\n'); } _ => panic!( "invalid range or file. Line: `{}` Range: {:?}", line_number, range ), }; } else { next_chunk.push_str(line); next_chunk.push('\n'); } } chunks.push(next_chunk); chunks } #[cfg(test)] mod tests { use super::*; const CONTENT: &str = r##" Hello World Random Subset 1234567 "##; #[test] fn extracting_range() { k9_stable::assert_equal!( extract_range( CONTENT, &Range { start: LineColumn { line: 2, column: 1 }, end: LineColumn { line: 2, column: 6 } } ) .as_str(), "Hello" ); } #[test] fn empty_range() { k9_stable::assert_equal!( extract_range( CONTENT, &Range { start: LineColumn { line: 2, column: 1 }, end: LineColumn { line: 2, column: 1 } } ) .as_str(), "" ); } #[test] fn overflow() { k9_stable::assert_equal!( extract_range( CONTENT, &Range { start: LineColumn { line: 99, column: 1 }, end: LineColumn { line: 199, column: 1 } } ) .as_str(), "" ); } } k9-0.12.0/src/string_diff.rs000064400000000000000000000013151046102023000136620ustar 00000000000000use colored::*; use diff::{lines, Result}; use std::fmt::Write; pub fn colored_diff(left: &str, right: &str) -> Option { let mut result = String::new(); if left == right { return None; } let lines = lines(left, right); result.push('\n'); for line in lines { match line { Result::Left(l) => { writeln!(result, "{} {}", "-".red(), &l.red()).unwrap(); } Result::Right(r) => { writeln!(result, "{} {}", "+".green(), &r.green()).unwrap(); } Result::Both(l, _r) => { writeln!(result, " {}", &l.dimmed()).unwrap(); } } } Some(result) } k9-0.12.0/src/types.rs000064400000000000000000000000341046102023000125250ustar 00000000000000pub type FilePath = String; k9-0.12.0/src/utils.rs000064400000000000000000000007071046102023000125300ustar 00000000000000pub fn add_linebreaks(s: &str) -> String { format!("\n{}\n", s) } pub fn terminal_separator_line() -> String { let width_override = crate::config::terminal_width_override(); let width = if width_override != 0 { width_override } else if let Some((width, _)) = terminal_size::terminal_size() { width.0 as usize } else { 100 // default width if we can't determine terminal width }; "━".repeat(width) }