diff --git a/src/mutate.rs b/src/mutate.rs index d07074e2..f609118e 100644 --- a/src/mutate.rs +++ b/src/mutate.rs @@ -237,8 +237,9 @@ mod test { use indoc::indoc; use itertools::Itertools; use pretty_assertions::assert_eq; - use test_util::copy_of_testdata; + use crate::test_util::copy_of_testdata; + use crate::visit::mutate_source_str; use crate::*; #[test] @@ -326,6 +327,23 @@ mod test { ); } + #[test] + fn always_skip_constructors_called_new() { + let code = indoc! { r#" + struct S { + x: i32, + } + + impl S { + fn new(x: i32) -> Self { + Self { x } + } + } + "# }; + let mutants = mutate_source_str(code, &Options::default()).unwrap(); + assert_eq!(mutants, []); + } + #[test] fn mutate_factorial() -> Result<()> { let temp = copy_of_testdata("factorial"); diff --git a/src/source.rs b/src/source.rs index b441e428..0240cedd 100644 --- a/src/source.rs +++ b/src/source.rs @@ -2,6 +2,7 @@ //! Access to a Rust source tree and files. +use std::fs::read_to_string; use std::sync::Arc; use anyhow::{Context, Result}; @@ -42,33 +43,52 @@ impl SourceFile { /// Construct a SourceFile representing a file within a tree. /// /// This eagerly loads the text of the file. - pub fn new( + /// + /// This also skip files outside of the tree, returning `Ok(None)`. + pub fn load( tree_path: &Utf8Path, - tree_relative_path: Utf8PathBuf, + tree_relative_path: &Utf8Path, package_name: &str, is_top: bool, ) -> Result> { - if ascent(&tree_relative_path) > 0 { + // TODO: Perhaps the caller should be responsible for checking this? + if ascent(tree_relative_path) > 0 { warn!( "skipping source outside of tree: {:?}", tree_relative_path.to_slash_path() ); return Ok(None); } - let full_path = tree_path.join(&tree_relative_path); + let full_path = tree_path.join(tree_relative_path); let code = Arc::new( - std::fs::read_to_string(&full_path) + read_to_string(&full_path) .with_context(|| format!("failed to read source of {full_path:?}"))? .replace("\r\n", "\n"), ); Ok(Some(SourceFile { - tree_relative_path, + tree_relative_path: tree_relative_path.to_owned(), code, package_name: package_name.to_owned(), is_top, })) } + /// Construct from in-memory text. + #[cfg(test)] + pub fn from_str( + tree_relative_path: &Utf8Path, + code: &str, + package_name: &str, + is_top: bool, + ) -> SourceFile { + SourceFile { + tree_relative_path: tree_relative_path.to_owned(), + code: Arc::new(code.to_owned()), + package_name: package_name.to_owned(), + is_top, + } + } + /// Return the path of this file relative to the tree root, with forward slashes. pub fn tree_relative_slashes(&self) -> String { self.tree_relative_path.to_slash_path() @@ -109,9 +129,9 @@ mod test { .write_all(b"fn main() {\r\n 640 << 10;\r\n}\r\n") .unwrap(); - let source_file = SourceFile::new( + let source_file = SourceFile::load( temp_dir_path, - file_name.parse().unwrap(), + Utf8Path::new(file_name), "imaginary-package", true, ) @@ -122,9 +142,9 @@ mod test { #[test] fn skips_files_outside_of_workspace() { - let source_file = SourceFile::new( - &Utf8PathBuf::from("unimportant"), - "../outside_workspace.rs".parse().unwrap(), + let source_file = SourceFile::load( + Utf8Path::new("unimportant"), + Utf8Path::new("../outside_workspace.rs"), "imaginary-package", true, ) diff --git a/src/visit.rs b/src/visit.rs index a4e45717..642fe887 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -58,11 +58,7 @@ pub fn walk_tree( options: &Options, console: &Console, ) -> Result { - let error_exprs = options - .error_values - .iter() - .map(|e| syn::parse_str(e).with_context(|| format!("Failed to parse error value {e:?}"))) - .collect::>>()?; + let error_exprs = options.parsed_error_exprs()?; console.walk_tree_start(); let mut file_queue: VecDeque = top_source_files.iter().cloned().collect(); let mut mutants = Vec::new(); @@ -77,9 +73,9 @@ pub fn walk_tree( // `--list-files`. for mod_namespace in &external_mods { if let Some(mod_path) = find_mod_source(workspace_dir, &source_file, mod_namespace)? { - file_queue.extend(SourceFile::new( + file_queue.extend(SourceFile::load( workspace_dir, - mod_path, + &mod_path, &source_file.package_name, false, )?) @@ -135,6 +131,21 @@ fn walk_file( Ok((visitor.mutants, visitor.external_mods)) } +/// For testing: parse and generate mutants from one single file provided as a string. +/// +/// The source code is assumed to be named `src/main.rs` with a fixed package name. +#[cfg(test)] +pub fn mutate_source_str(code: &str, options: &Options) -> Result> { + let source_file = SourceFile::from_str( + Utf8Path::new("src/main.rs"), + code, + "cargo-mutants-testdata-internal", + true, + ); + let (mutants, _) = walk_file(&source_file, &options.parsed_error_exprs()?)?; + Ok(mutants) +} + /// Reference to an external module from a source file. /// /// This is approximately a list of namespace components like `["foo", "bar"]` for @@ -853,4 +864,21 @@ mod test { Err("/leading_slash/../and_dots.rs".to_owned()) ); } + + /// Demonstrate that we can generate mutants from a string, without needing a whole tree. + #[test] + fn mutants_from_test_str() { + let options = Options::default(); + let mutants = mutate_source_str( + indoc! {" + fn always_true() -> bool { true } + "}, + &options, + ) + .expect("walk_file_string"); + assert_eq!( + mutants.iter().map(|m| m.name(false, false)).collect_vec(), + ["src/main.rs: replace always_true -> bool with false"] + ); + } }