From c2a6d2aadb11ab3a5d74c53ba3ede688eb5e6cdb Mon Sep 17 00:00:00 2001 From: Alex Povel Date: Sun, 3 Nov 2024 21:08:53 +0100 Subject: [PATCH] feat(cli): Dry run mode ( `--dry-run` flag) (#156) This change adds a new flag `--dry-run` that when given, prevents otherwise destructive file overwrite operations and instead prints a rich diff. The diff contains names of files which would be modified, and a git-like (word) diff of what modifications *would* be made. The diff is human-readable, and not designed to be easily machine-readable. It leverages the existing search mode. Closes #152. --- Cargo.lock | 4 +- README.md | 7 ++ src/actions/style.rs | 22 ++++ src/iterext.rs | 105 ++++++++++++++++ src/lib.rs | 3 + src/main.rs | 113 +++++++++++++----- tests/cli.rs | 30 +++-- ...s__files-inplace-python-dry-run-linux.snap | 29 +++++ ...s__files-inplace-python-dry-run-macos.snap | 29 +++++ ..._files-inplace-python-dry-run-windows.snap | 29 +++++ ...nd-files-inplace-python-dry-run-linux.snap | 25 ++++ ...nd-files-inplace-python-dry-run-macos.snap | 25 ++++ ...-files-inplace-python-dry-run-windows.snap | 25 ++++ ...-scoping-inplace-python-dry-run-linux.snap | 31 +++++ ...-scoping-inplace-python-dry-run-macos.snap | 31 +++++ ...coping-inplace-python-dry-run-windows.snap | 31 +++++ 16 files changed, 497 insertions(+), 42 deletions(-) create mode 100644 src/iterext.rs create mode 100644 tests/snapshots/cli__tests__files-inplace-python-dry-run-linux.snap create mode 100644 tests/snapshots/cli__tests__files-inplace-python-dry-run-macos.snap create mode 100644 tests/snapshots/cli__tests__files-inplace-python-dry-run-windows.snap create mode 100644 tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-linux.snap create mode 100644 tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-macos.snap create mode 100644 tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-windows.snap create mode 100644 tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-linux.snap create mode 100644 tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-macos.snap create mode 100644 tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-windows.snap diff --git a/Cargo.lock b/Cargo.lock index 5b1ab807..d0fd8ad7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1124,9 +1124,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "similar" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" [[package]] name = "slug" diff --git a/README.md b/README.md index 9f929a36..405bdfc6 100644 --- a/README.md +++ b/README.md @@ -1457,6 +1457,13 @@ Options (global): Processing no files is not an error condition in itself, but might be an unexpected outcome in some contexts. This flag makes the condition explicit. + --dry-run + Do not destructively overwrite files, instead print rich diff only. + + The diff details the names of files which would be modified, alongside all + changes inside those files which would be performed outside of dry running. + It is similar to git diff with word diffing enabled. + -i, --invert Undo the effects of passed actions, where applicable. diff --git a/src/actions/style.rs b/src/actions/style.rs index 0b99255f..2f09c55d 100644 --- a/src/actions/style.rs +++ b/src/actions/style.rs @@ -13,6 +13,28 @@ pub struct Style { pub styles: Vec, } +impl Style { + /// Creates a style with red foreground color and bold font weight. + #[must_use] + pub fn red_bold() -> Self { + Self { + fg: Some(Color::Red), + bg: None, + styles: vec![Styles::Bold], + } + } + + /// Creates a style with green foreground color and bold font weight. + #[must_use] + pub fn green_bold() -> Self { + Self { + fg: Some(Color::Green), + bg: None, + styles: vec![Styles::Bold], + } + } +} + impl Action for Style { fn act(&self, input: &str) -> String { const NEWLINE: char = '\n'; diff --git a/src/iterext.rs b/src/iterext.rs new file mode 100644 index 00000000..c5d22355 --- /dev/null +++ b/src/iterext.rs @@ -0,0 +1,105 @@ +/// Extension trait that adds parallel zipping functionality to iterators over iterators. +pub trait ParallelZipExt: Iterator { + /// Zips multiple iterators in parallel, such that the nth invocation yields a + /// [`Vec`] of all nth items of the subiterators. + fn parallel_zip(self) -> ParallelZip + where + Self: Sized, + Self::Item: Iterator; +} + +/// An iterator similar to [`std::iter::zip`], but instead it zips over *multiple +/// iterators* in parallel, such that the nth invocation yields a [`Vec`] of all +/// nth items of its subiterators. +#[derive(Debug)] +pub struct ParallelZip(Vec); + +impl Iterator for ParallelZip { + type Item = Vec; + + fn next(&mut self) -> Option { + if self.0.is_empty() { + return None; + } + + self.0.iter_mut().map(Iterator::next).collect() + } +} + +// Implement the extension trait for any iterator whose items are themselves iterators +impl ParallelZipExt for T +where + T: Iterator, + T::Item: Iterator, +{ + fn parallel_zip(self) -> ParallelZip { + ParallelZip(self.collect()) + } +} + +#[cfg(test)] +mod tests { + use itertools::Itertools; + use rstest::rstest; + + use super::*; + + #[rstest] + #[case::empty_once( + Vec::>::new(), + Vec::new(), + )] + #[case::empty_twice( + vec![ + vec![], + vec![], + ], + vec![], + )] + #[case::zips_to_shortest( + vec![ + vec![0, 1, 2], + vec![3, 4], + ], + vec![ + vec![0, 3], + vec![1, 4], + ] + )] + #[case::base_case( + vec![ + vec![1, 2], + vec![3, 4], + vec![5, 6] + ], + vec![ + vec![1, 3, 5], + vec![2, 4, 6] + ] + )] + #[case::transpose_horizontal( + vec![ + vec![1, 2, 3], + ], + vec![ + vec![1], + vec![2], + vec![3], + ] + )] + #[case::transpose_vertical( + vec![ + vec![1], + vec![2], + vec![3] + ], + vec![ + vec![1, 2, 3] + ] + )] + fn test_parallel_zip(#[case] input: Vec>, #[case] expected: Vec>) { + let iters = input.into_iter().map(IntoIterator::into_iter).collect_vec(); + let res = iters.into_iter().parallel_zip().collect_vec(); + assert_eq!(res, expected); + } +} diff --git a/src/lib.rs b/src/lib.rs index e6654c14..3ba00fd7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -195,3 +195,6 @@ pub const GLOBAL_SCOPE: &str = r".*"; /// The type of regular expression used throughout the crate. Abstracts away the /// underlying implementation. pub use fancy_regex::Regex as RegexPattern; + +/// Custom iterator extensions. +pub mod iterext; diff --git a/src/main.rs b/src/main.rs index 67c93986..f0d7bfed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use std::sync::{Arc, Mutex}; use std::{env, fmt}; use anyhow::{Context, Result}; -use colored::{Color, Colorize, Styles}; +use colored::Colorize; use ignore::{WalkBuilder, WalkState}; use itertools::Itertools; use log::{debug, error, info, trace, LevelFilter}; @@ -23,6 +23,7 @@ use srgn::actions::{ }; #[cfg(feature = "symbols")] use srgn::actions::{Symbols, SymbolsInversion}; +use srgn::iterext::ParallelZipExt; use srgn::scoping::langs::LanguageScoper; use srgn::scoping::literal::{Literal, LiteralError}; use srgn::scoping::regex::{Regex, RegexError}; @@ -161,15 +162,15 @@ fn main() -> Result<()> { // Only have this kick in if a language scoper is in play; otherwise, we'd just be a // poor imitation of ripgrep itself. Plus, this retains the `tr`-like behavior, // setting it apart from other utilities. - let search_mode = actions.is_empty() && language_scopers.is_some(); + let search_mode = actions.is_empty() && language_scopers.is_some() || options.dry_run; if search_mode { info!("Will use search mode."); // Modelled after ripgrep! - let style = Style { - fg: Some(Color::Red), - styles: vec![Styles::Bold], - ..Default::default() + let style = if options.dry_run { + Style::green_bold() // "Would change to this", like git diff + } else { + Style::red_bold() // "Found!", like ripgrep }; actions.push(Box::new(style)); @@ -185,6 +186,15 @@ fn main() -> Result<()> { ); } + let pipeline = if options.dry_run { + let action: Box = Box::new(Style::red_bold()); + let color_only = vec![action]; + vec![color_only, actions] + } else { + vec![actions] + }; + + let pipeline: Vec<&[Box]> = pipeline.iter().map(Vec::as_slice).collect(); let language_scopers = language_scopers.unwrap_or_default(); // Now write out @@ -196,7 +206,7 @@ fn main() -> Result<()> { standalone_action, &general_scoper, &language_scopers, - &actions, + &pipeline, )?; } (Input::WalkOn(validator), false) => { @@ -207,7 +217,7 @@ fn main() -> Result<()> { &validator, &general_scoper, &language_scopers, - &actions, + &pipeline, search_mode, options.threads.map_or_else( || std::thread::available_parallelism().map_or(1, std::num::NonZero::get), @@ -223,7 +233,7 @@ fn main() -> Result<()> { &validator, &general_scoper, &language_scopers, - &actions, + &pipeline, search_mode, )?; } @@ -262,6 +272,12 @@ enum StandaloneAction { None, } +/// A "pipeline" in that there's not just a single sequence (== slice) of actions, but +/// instead multiple. These can be used in parallel (on the same or different views), +/// and the different results then used for advanced use cases. For example, diffing +/// different results against one another. +type Pipeline<'a> = &'a [&'a [Box]]; + /// Main entrypoint for simple `stdin` -> `stdout` processing. #[allow(clippy::borrowed_box)] // Used throughout, not much of a pain fn handle_actions_on_stdin( @@ -269,7 +285,7 @@ fn handle_actions_on_stdin( standalone_action: StandaloneAction, general_scoper: &Box, language_scopers: &[Box], - actions: &[Box], + pipeline: Pipeline<'_>, ) -> Result<(), ProgramError> { info!("Will use stdin to stdout."); let mut source = String::new(); @@ -283,7 +299,7 @@ fn handle_actions_on_stdin( &mut destination, general_scoper, language_scopers, - actions, + pipeline, )?; stdout().lock().write_all(destination.as_bytes())?; @@ -306,7 +322,7 @@ fn handle_actions_on_many_files_sorted( validator: &Validator, general_scoper: &Box, language_scopers: &[Box], - actions: &[Box], + pipeline: Pipeline<'_>, search_mode: bool, ) -> Result<(), ProgramError> { let root = env::current_dir()?; @@ -334,7 +350,7 @@ fn handle_actions_on_many_files_sorted( validator, general_scoper, language_scopers, - actions, + pipeline, search_mode, ); @@ -413,7 +429,7 @@ fn handle_actions_on_many_files_threaded( validator: &Validator, general_scoper: &Box, language_scopers: &[Box], - actions: &[Box], + pipeline: Pipeline<'_>, search_mode: bool, n_threads: usize, ) -> Result<(), ProgramError> { @@ -448,7 +464,7 @@ fn handle_actions_on_many_files_threaded( validator, general_scoper, language_scopers, - actions, + pipeline, search_mode, ); @@ -544,7 +560,7 @@ fn process_path( validator: &Validator, general_scoper: &Box, language_scopers: &[Box], - actions: &[Box], + pipeline: Pipeline<'_>, search_mode: bool, ) -> std::result::Result<(), PathProcessingError> { if !path.is_file() { @@ -578,7 +594,7 @@ fn process_path( &mut destination, general_scoper, language_scopers, - actions, + pipeline, )?; (destination, filesize, changed) @@ -612,9 +628,15 @@ fn process_path( if changed { debug!("Got new file contents, writing to file: {:?}", path); + assert!( + !global_options.dry_run, + // Dry run leverages search mode, so should never get here. Assert for + // extra safety. + "Dry running, but attempted to write file!" + ); fs::write(&path, new_contents.as_bytes())?; - // Confirm after successful write. + // Confirm after successful processing. writeln!(stdout, "{}", path.display())?; } else { debug!( @@ -644,7 +666,7 @@ fn apply( destination: &mut String, general_scoper: &Box, language_scopers: &[Box], - actions: &[Box], + pipeline: Pipeline<'_>, ) -> std::result::Result { debug!("Building view."); let mut builder = ScopedViewBuilder::new(source); @@ -676,28 +698,50 @@ fn apply( view.squeeze(); } - for action in actions { - view.map_with_context(action)?; + // Give each pipeline its own fresh view + let mut views = vec![view; pipeline.len()]; + + for (actions, view) in pipeline.iter().zip_eq(&mut views) { + for action in *actions { + view.map_with_context(action)?; + } } debug!("Writing to destination."); let line_based = global_options.only_matching || global_options.line_numbers; if line_based { - for (i, line) in view.lines().into_iter().enumerate() { + let line_based_views = views.iter().map(|v| v.lines().into_iter()).collect_vec(); + + for (i, lines) in line_based_views.into_iter().parallel_zip().enumerate() { let i = i + 1; - if !global_options.only_matching || line.has_any_in_scope() { - if global_options.line_numbers { - // `ColoredString` needs to be 'evaluated' to do anything; make sure - // to not forget even if this is moved outside of `format!`. - #[allow(clippy::to_string_in_format_args)] - destination.push_str(&format!("{}:", i.to_string().green().to_string())); - } + for line in lines { + if !global_options.only_matching || line.has_any_in_scope() { + if global_options.line_numbers { + // `ColoredString` needs to be 'evaluated' to do anything; make sure + // to not forget even if this is moved outside of `format!`. + #[allow(clippy::to_string_in_format_args)] + destination.push_str(&format!("{}:", i.to_string().green().to_string())); + } - destination.push_str(&line.to_string()); + destination.push_str(&line.to_string()); + } } } } else { - destination.push_str(&view.to_string()); + assert_eq!( + views.len(), + 1, + // Multiple views are useful for e.g. diffing, which works line-based (see + // `dry_run`). When not line-based, they *currently* do not make sense, as + // there's neither any code path where there *would* be multiple views at + // this point, *nor* a valid use case. Printing multiple views here would + // probably wreak havoc. + "Multiple views at this stage make no sense." + ); + + for view in views { + destination.push_str(&view.to_string()); + } }; debug!("Done writing to destination."); @@ -1048,6 +1092,13 @@ mod cli { /// unexpected outcome in some contexts. This flag makes the condition explicit. #[arg(long, verbatim_doc_comment, alias = "fail-empty-glob")] pub fail_no_files: bool, + /// Do not destructively overwrite files, instead print rich diff only. + /// + /// The diff details the names of files which would be modified, alongside all + /// changes inside those files which would be performed outside of dry running. + /// It is similar to git diff with word diffing enabled. + #[arg(long, verbatim_doc_comment)] + pub dry_run: bool, /// Undo the effects of passed actions, where applicable. /// /// Requires a 1:1 mapping between replacements and original, which is currently diff --git a/tests/cli.rs b/tests/cli.rs index 3ef96340..003d928f 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -401,31 +401,37 @@ Heizoelrueckstossabdaempfung. #[case] input: PathBuf, #[case] args: &[&str], #[case] skip_output_check: bool, + #[values(true, false)] dry_run: bool, // Check all permutations for all inputs ) -> anyhow::Result<()> { - use std::mem::ManuallyDrop; - let args = args.iter().map(ToString::to_string).collect_vec(); // Arrange let mut cmd = get_cmd(); - let baseline = { + let baseline = if dry_run { + // Stays the same! In dry runs, we compare against the very same directory, + // as it should not change. + input.clone() + } else { let mut baseline = input.clone(); baseline.pop(); baseline.push("out"); baseline }; - let candidate = ManuallyDrop::new(copy_to_tmp(&input)); + let candidate = copy_to_tmp(&input); drop(input); // Prevent misuse - cmd.current_dir(&*candidate); + cmd.current_dir(&candidate); cmd.args( // Override; `Command` is detected as providing stdin but we're working on // files here. ["--stdin-override-to", "false"], ); cmd.args(&args); + if dry_run { + cmd.arg("--dry-run"); + } // Act let output = cmd.output().expect("failed to execute binary under test"); @@ -435,15 +441,20 @@ Heizoelrueckstossabdaempfung. // Thing itself works assert!(output.status.success(), "Binary execution itself failed"); - // Results are correct Do not drop on panic, to keep tmpdir in place for manual - // inspection. Can then diff directories. + // Do not drop on panic, to keep tmpdir in place for manual inspection. Can then + // diff directories. check_directories_equality(baseline, candidate.path().to_owned())?; - // Test was successful: ok to drop. - drop(ManuallyDrop::into_inner(candidate)); + // Test was successful: ok to drop. Caveat: fails test if deletion fails, which + // is unwarranted coupling? + candidate.close()?; // Let's look at command output now. if !skip_output_check { + if dry_run { + snapshot_name.push_str("-dry-run"); + } + // These are inherently platform-specific, as they deal with file paths. snapshot_name.push('-'); snapshot_name.push_str(std::env::consts::OS); @@ -957,6 +968,7 @@ right contents: let tmp_dir = tempfile::Builder::new() .prefix(pkg) + .keep(true) // Keep for manual inspection if needed .tempdir() .expect("Failed to create temporary directory"); diff --git a/tests/snapshots/cli__tests__files-inplace-python-dry-run-linux.snap b/tests/snapshots/cli__tests__files-inplace-python-dry-run-linux.snap new file mode 100644 index 00000000..9fc1e52e --- /dev/null +++ b/tests/snapshots/cli__tests__files-inplace-python-dry-run-linux.snap @@ -0,0 +1,29 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--glob" + - "**/*.py" + - foo + - baz +stdin: ~ +stdout: + - "1.py\n" + - "1:# This string is found and touched: foo\n" + - "1:# This string is found and touched: baz\n" + - "4:def foo(bar: int) -> int:\n" + - "4:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/subdir/3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0 diff --git a/tests/snapshots/cli__tests__files-inplace-python-dry-run-macos.snap b/tests/snapshots/cli__tests__files-inplace-python-dry-run-macos.snap new file mode 100644 index 00000000..9fc1e52e --- /dev/null +++ b/tests/snapshots/cli__tests__files-inplace-python-dry-run-macos.snap @@ -0,0 +1,29 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--glob" + - "**/*.py" + - foo + - baz +stdin: ~ +stdout: + - "1.py\n" + - "1:# This string is found and touched: foo\n" + - "1:# This string is found and touched: baz\n" + - "4:def foo(bar: int) -> int:\n" + - "4:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/subdir/3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0 diff --git a/tests/snapshots/cli__tests__files-inplace-python-dry-run-windows.snap b/tests/snapshots/cli__tests__files-inplace-python-dry-run-windows.snap new file mode 100644 index 00000000..227837c5 --- /dev/null +++ b/tests/snapshots/cli__tests__files-inplace-python-dry-run-windows.snap @@ -0,0 +1,29 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--glob" + - "**/*.py" + - foo + - baz +stdin: ~ +stdout: + - "1.py\n" + - "1:# This string is found and touched: foo\n" + - "1:# This string is found and touched: baz\n" + - "4:def foo(bar: int) -> int:\n" + - "4:def baz(bar: int) -> int:\n" + - "\n" + - "subdir\\2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir\\subdir\\3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0 diff --git a/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-linux.snap b/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-linux.snap new file mode 100644 index 00000000..64dd3fb6 --- /dev/null +++ b/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-linux.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--python" + - function-names + - "--glob" + - subdir/**/*.py + - foo + - baz +stdin: ~ +stdout: + - "subdir/2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/subdir/3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0 diff --git a/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-macos.snap b/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-macos.snap new file mode 100644 index 00000000..64dd3fb6 --- /dev/null +++ b/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-macos.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--python" + - function-names + - "--glob" + - subdir/**/*.py + - foo + - baz +stdin: ~ +stdout: + - "subdir/2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/subdir/3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0 diff --git a/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-windows.snap b/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-windows.snap new file mode 100644 index 00000000..f79aa0f1 --- /dev/null +++ b/tests/snapshots/cli__tests__language-scoping-and-files-inplace-python-dry-run-windows.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--python" + - function-names + - "--glob" + - subdir/**/*.py + - foo + - baz +stdin: ~ +stdout: + - "subdir\\2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir\\subdir\\3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0 diff --git a/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-linux.snap b/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-linux.snap new file mode 100644 index 00000000..670e9a05 --- /dev/null +++ b/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-linux.snap @@ -0,0 +1,31 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--python" + - function-names + - foo + - baz +stdin: ~ +stdout: + - "1-shebanged\n" + - "9:def foo(bar: int) -> int:\n" + - "9:def baz(bar: int) -> int:\n" + - "\n" + - "1.py\n" + - "4:def foo(bar: int) -> int:\n" + - "4:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/subdir/3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0 diff --git a/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-macos.snap b/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-macos.snap new file mode 100644 index 00000000..670e9a05 --- /dev/null +++ b/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-macos.snap @@ -0,0 +1,31 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--python" + - function-names + - foo + - baz +stdin: ~ +stdout: + - "1-shebanged\n" + - "9:def foo(bar: int) -> int:\n" + - "9:def baz(bar: int) -> int:\n" + - "\n" + - "1.py\n" + - "4:def foo(bar: int) -> int:\n" + - "4:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir/subdir/3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0 diff --git a/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-windows.snap b/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-windows.snap new file mode 100644 index 00000000..39281c88 --- /dev/null +++ b/tests/snapshots/cli__tests__language-scoping-inplace-python-dry-run-windows.snap @@ -0,0 +1,31 @@ +--- +source: tests/cli.rs +expression: "CommandSnap\n{\n args, stdin: None, stdout:\n stdout.split_inclusive('\\n').map(ToOwned::to_owned).collect_vec(),\n exit_code,\n}" +info: + stderr: [] +--- +args: + - "--sorted" + - "--python" + - function-names + - foo + - baz +stdin: ~ +stdout: + - "1-shebanged\n" + - "9:def foo(bar: int) -> int:\n" + - "9:def baz(bar: int) -> int:\n" + - "\n" + - "1.py\n" + - "4:def foo(bar: int) -> int:\n" + - "4:def baz(bar: int) -> int:\n" + - "\n" + - "subdir\\2.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" + - "subdir\\subdir\\3.py\n" + - "1:def foo(bar: int) -> int:\n" + - "1:def baz(bar: int) -> int:\n" + - "\n" +exit_code: 0