From 64b73b049c2301b335562e3ee6fd8fb9f215438d Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sat, 10 Jun 2023 17:06:41 -0700 Subject: [PATCH 1/6] WIP generic container/collection recursion --- NEWS.md | 2 +- book/src/mutants.md | 1 + src/visit.rs | 66 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/NEWS.md b/NEWS.md index daf57fd4..b3afa5c1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,7 +11,7 @@ - Recurse into return types, so that for example `Result` can generate `Ok(true)` and `Ok(false)`, and `Some` generates `None` and every generated - value of `T`. Similarly for `Box`, and `Vec`. + value of `T`. Similarly for `Box`, `Vec`, `Rc`, `Arc`. - Generate specific values for integers: `[0, 1]` for unsigned integers, `[0, 1, -1]` for signed integers; `[1]` for NonZero unsigned integers and diff --git a/book/src/mutants.md b/book/src/mutants.md index 7fac9d3e..4a477835 100644 --- a/book/src/mutants.md +++ b/book/src/mutants.md @@ -30,6 +30,7 @@ More mutation genres and patterns will be added in future releases. | `Box` | `Box::new(...)` | | `Vec` | `vec![]`, `vec![...]` | | `Arc` | `Arc::new(...)` | +| `Rc` | `Rc::new(...)` | | `[T; L]` | `[r; L]` for all replacements of T | | `&T` | `&...` (all replacements for T) | | (any other) | `Default::default()` | diff --git a/src/visit.rs b/src/visit.rs index e867aec3..538e5b67 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -18,8 +18,8 @@ use quote::{quote, ToTokens}; use syn::ext::IdentExt; use syn::visit::Visit; use syn::{ - AngleBracketedGenericArguments, Attribute, Expr, GenericArgument, ItemFn, Path, PathArguments, - ReturnType, Type, TypeArray, TypeTuple, + AngleBracketedGenericArguments, Attribute, Expr, GenericArgument, Ident, ItemFn, Path, + PathArguments, ReturnType, Type, TypeArray, TypeTuple, }; use tracing::{debug, debug_span, trace, trace_span, warn}; @@ -318,6 +318,11 @@ fn return_type_replacements(return_type: &ReturnType, error_exprs: &[Expr]) -> V /// /// This is really the heart of cargo-mutants. fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { + // This could probably change to run from some configuration rather than + // hardcoding various types, which would make it easier to support tree-specific + // mutation values, and perhaps reduce duplication. However, it seems better + // to support all the core cases with direct code first to learn what generalizations + // are needed. let mut reps = Vec::new(); match type_ { Type::Path(syn::TypePath { path, .. }) => { @@ -388,7 +393,9 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { quote! { vec![#rep] } }), ) - } else if let Some(inner_type) = match_first_type_arg(path, "Arc") { + } else if let Some((ident, inner_type)) = could_be_simple_container(path) { + // Something like Arc, Mutex, etc. + // TODO: Ideally we should use the path without relying on it being // imported, but we must strip or rewrite the arguments, so that // `std::sync::Arc` becomes either `std::sync::Arc::::new` @@ -400,6 +407,22 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { quote! { Arc::new(#rep) } }), ) + } else if let Some(inner_type) = match_first_type_arg(path, "Rc") { + reps.extend( + type_replacements(inner_type, error_exprs) + .into_iter() + .map(|rep| { + quote! { Rc::new(#rep) } + }), + ) + } else if let Some(inner_type) = match_first_type_arg(path, "Mutex") { + reps.extend( + type_replacements(inner_type, error_exprs) + .into_iter() + .map(|rep| { + quote! { Mutex::new(#rep) } + }), + ) } else { reps.push(quote! { Default::default() }); } @@ -472,6 +495,25 @@ fn path_ends_with(path: &Path, ident: &str) -> bool { path.segments.last().map_or(false, |s| s.ident == ident) } +/// If the type has a single type argument then, perhaps it's a simple container +/// like Box, Cell, Mutex, etc, that can be constructed with `T::new(inner_val)`. +/// +/// If so, return the short name (like "Box") and the inner type. +fn could_be_simple_container(path: &Path) -> Option<(&Ident, &Type)> { + let last = path.segments.last()?; + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = + &last.arguments + { + // TODO: Skip lifetime args. + if args.len() == 1 { + if let Some(GenericArgument::Type(inner_type)) = args.first() { + return Some((&last.ident, inner_type)); + } + } + } + None +} + fn path_is_float(path: &Path) -> bool { ["f32", "f64"].iter().any(|s| path.is_ident(s)) } @@ -873,15 +915,15 @@ mod test { ); } - // #[test] - // fn rc_replacement() { - // // Also checks that it matches the path, even using an atypical path. - // // TODO: Ideally this would be fully qualified like `alloc::sync::Rc::new(String::new())`. - // assert_eq!( - // replace(&parse_quote! { -> alloc::sync::Rc }, &[]), - // &["Rc::new(String::new())", "Rc::new(\"xyzzy\".into())"] - // ); - // } + #[test] + fn rc_replacement() { + // Also checks that it matches the path, even using an atypical path. + // TODO: Ideally this would be fully qualified like `alloc::sync::Rc::new(String::new())`. + assert_eq!( + replace(&parse_quote! { -> alloc::sync::Rc }, &[]), + &["Rc::new(String::new())", "Rc::new(\"xyzzy\".into())"] + ); + } fn replace(return_type: &ReturnType, error_exprs: &[Expr]) -> Vec { return_type_replacements(return_type, error_exprs) From 2516fcc445707277497954cfdaa475b695e65b87 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 11 Jun 2023 11:46:51 -0700 Subject: [PATCH 2/6] Factor out generic matching for containers --- src/visit.rs | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/visit.rs b/src/visit.rs index 538e5b67..35117006 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -365,14 +365,6 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { reps.extend(error_exprs.iter().map(|error_expr| { quote! { Err(#error_expr) } })); - } else if let Some(boxed_type) = match_first_type_arg(path, "Box") { - reps.extend( - type_replacements(boxed_type, error_exprs) - .into_iter() - .map(|rep| { - quote! { Box::new(#rep) } - }), - ) } else if let Some(some_type) = match_first_type_arg(path, "Option") { reps.push(quote! { None }); reps.extend( @@ -393,7 +385,7 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { quote! { vec![#rep] } }), ) - } else if let Some((ident, inner_type)) = could_be_simple_container(path) { + } else if let Some((container_type, inner_type)) = known_simple_container(path) { // Something like Arc, Mutex, etc. // TODO: Ideally we should use the path without relying on it being @@ -404,23 +396,7 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { type_replacements(inner_type, error_exprs) .into_iter() .map(|rep| { - quote! { Arc::new(#rep) } - }), - ) - } else if let Some(inner_type) = match_first_type_arg(path, "Rc") { - reps.extend( - type_replacements(inner_type, error_exprs) - .into_iter() - .map(|rep| { - quote! { Rc::new(#rep) } - }), - ) - } else if let Some(inner_type) = match_first_type_arg(path, "Mutex") { - reps.extend( - type_replacements(inner_type, error_exprs) - .into_iter() - .map(|rep| { - quote! { Mutex::new(#rep) } + quote! { #container_type::new(#rep) } }), ) } else { @@ -499,12 +475,19 @@ fn path_ends_with(path: &Path, ident: &str) -> bool { /// like Box, Cell, Mutex, etc, that can be constructed with `T::new(inner_val)`. /// /// If so, return the short name (like "Box") and the inner type. -fn could_be_simple_container(path: &Path) -> Option<(&Ident, &Type)> { +fn known_simple_container(path: &Path) -> Option<(&Ident, &Type)> { let last = path.segments.last()?; + if !["Box", "Cell", "RefCell", "Arc", "Rc", "Mutex"] + .iter() + .any(|v| last.ident == v) + { + return None; + } if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &last.arguments { // TODO: Skip lifetime args. + // TODO: Return the path with args stripped out. if args.len() == 1 { if let Some(GenericArgument::Type(inner_type)) = args.first() { return Some((&last.ident, inner_type)); From 2727de21cd92718b85ea792d801b5ae6f89f407d Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 11 Jun 2023 12:09:26 -0700 Subject: [PATCH 3/6] Match some known simple containers --- NEWS.md | 5 ++ book/src/mutants.md | 1 + src/visit.rs | 55 ++++++++++++++++++- testdata/tree/well_tested/src/lib.rs | 1 + testdata/tree/well_tested/src/sets.rs | 13 +++++ tests/cli/main.rs | 4 +- .../cli__list_files_json_well_tested.snap | 5 ++ .../cli__list_files_text_well_tested.snap | 2 + ...li__list_mutants_in_all_trees_as_json.snap | 27 +++++++++ ...li__list_mutants_in_all_trees_as_text.snap | 3 + .../cli__list_mutants_json_well_tested.snap | 28 ++++++++++ .../cli__list_mutants_well_tested.snap | 4 ++ ...tants_well_tested_exclude_name_filter.snap | 3 + .../cli__well_tested_tree_check_only.snap | 7 ++- ...i__well_tested_tree_finds_no_problems.snap | 7 ++- ...ed_tree_finds_no_problems__caught.txt.snap | 3 + .../cli__well_tested_tree_quiet.snap | 4 +- 17 files changed, 162 insertions(+), 10 deletions(-) create mode 100644 testdata/tree/well_tested/src/sets.rs diff --git a/NEWS.md b/NEWS.md index b3afa5c1..693a832b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # cargo-mutants changelog +## Unreleased + +- Mutate `BinaryHeap`, `BTreeSet`, `HashSet`, `LinkedList`, and `VecDeque` to + generate empty and one-element containers. + ## 23.6.0 - Generate `Box::leak(Box::new(...))` as a mutation of functions returning diff --git a/book/src/mutants.md b/book/src/mutants.md index 4a477835..a4dc9baa 100644 --- a/book/src/mutants.md +++ b/book/src/mutants.md @@ -31,6 +31,7 @@ More mutation genres and patterns will be added in future releases. | `Vec` | `vec![]`, `vec![...]` | | `Arc` | `Arc::new(...)` | | `Rc` | `Rc::new(...)` | +| `BinaryHeap`, `BTreeSet`, `HashSet`, `LinkedList`, `VecDeque` | empty and one-element collections | | `[T; L]` | `[r; L]` for all replacements of T | | `&T` | `&...` (all replacements for T) | | (any other) | `Default::default()` | diff --git a/src/visit.rs b/src/visit.rs index 35117006..9103f982 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -385,7 +385,7 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { quote! { vec![#rep] } }), ) - } else if let Some((container_type, inner_type)) = known_simple_container(path) { + } else if let Some((container_type, inner_type)) = known_container(path) { // Something like Arc, Mutex, etc. // TODO: Ideally we should use the path without relying on it being @@ -399,6 +399,15 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { quote! { #container_type::new(#rep) } }), ) + } else if let Some((collection_type, inner_type)) = known_collection(path) { + reps.push(quote! { #collection_type::new() }); + reps.extend( + type_replacements(inner_type, error_exprs) + .into_iter() + .map(|rep| { + quote! { #collection_type::from_iter([#rep]) } + }), + ); } else { reps.push(quote! { Default::default() }); } @@ -475,7 +484,7 @@ fn path_ends_with(path: &Path, ident: &str) -> bool { /// like Box, Cell, Mutex, etc, that can be constructed with `T::new(inner_val)`. /// /// If so, return the short name (like "Box") and the inner type. -fn known_simple_container(path: &Path) -> Option<(&Ident, &Type)> { +fn known_container(path: &Path) -> Option<(&Ident, &Type)> { let last = path.segments.last()?; if !["Box", "Cell", "RefCell", "Arc", "Rc", "Mutex"] .iter() @@ -497,6 +506,36 @@ fn known_simple_container(path: &Path) -> Option<(&Ident, &Type)> { None } +/// Match known simple collections that can be empty or constructed from an +/// iterator. +fn known_collection(path: &Path) -> Option<(&Ident, &Type)> { + let last = path.segments.last()?; + if ![ + "BinaryHeap", + "BTreeSet", + "HashSet", + "LinkedList", + "VecDeque", + ] + .iter() + .any(|v| last.ident == v) + { + return None; + } + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = + &last.arguments + { + // TODO: Skip lifetime args. + // TODO: Return the path with args stripped out. + if args.len() == 1 { + if let Some(GenericArgument::Type(inner_type)) = args.first() { + return Some((&last.ident, inner_type)); + } + } + } + None +} + fn path_is_float(path: &Path) -> bool { ["f32", "f64"].iter().any(|s| path.is_ident(s)) } @@ -908,6 +947,18 @@ mod test { ); } + #[test] + fn btreeset_replacement() { + assert_eq!( + replace(&parse_quote! { -> std::collections::BTreeSet }, &[]), + &[ + "BTreeSet::new()", + "BTreeSet::from_iter([String::new()])", + "BTreeSet::from_iter([\"xyzzy\".into()])" + ] + ); + } + fn replace(return_type: &ReturnType, error_exprs: &[Expr]) -> Vec { return_type_replacements(return_type, error_exprs) .into_iter() diff --git a/testdata/tree/well_tested/src/lib.rs b/testdata/tree/well_tested/src/lib.rs index c3895b71..a47a6aab 100644 --- a/testdata/tree/well_tested/src/lib.rs +++ b/testdata/tree/well_tested/src/lib.rs @@ -16,5 +16,6 @@ mod methods; mod nested_function; mod numbers; mod result; +mod sets; pub mod simple_fns; mod struct_with_lifetime; diff --git a/testdata/tree/well_tested/src/sets.rs b/testdata/tree/well_tested/src/sets.rs new file mode 100644 index 00000000..ff93188f --- /dev/null +++ b/testdata/tree/well_tested/src/sets.rs @@ -0,0 +1,13 @@ +use std::collections::BTreeSet; + +fn make_a_set() -> BTreeSet { + let mut s = BTreeSet::new(); + s.insert("one".into()); + s.insert("two".into()); + s +} + +#[test] +fn set_has_two_elements() { + assert_eq!(make_a_set().len(), 2); +} diff --git a/tests/cli/main.rs b/tests/cli/main.rs index bea3bfed..5e198898 100644 --- a/tests/cli/main.rs +++ b/tests/cli/main.rs @@ -622,8 +622,8 @@ fn well_tested_tree_quiet() { fs::read_to_string(tmp_src_dir.path().join("mutants.out/outcomes.json")).unwrap(); println!("outcomes.json:\n{outcomes_json}"); let outcomes: serde_json::Value = outcomes_json.parse().unwrap(); - assert_eq!(outcomes["total_mutants"], 27); - assert_eq!(outcomes["caught"], 27); + assert_eq!(outcomes["total_mutants"], 30); + assert_eq!(outcomes["caught"], 30); assert_eq!(outcomes["unviable"], 0); assert_eq!(outcomes["missed"], 0); } diff --git a/tests/cli/snapshots/cli__list_files_json_well_tested.snap b/tests/cli/snapshots/cli__list_files_json_well_tested.snap index ad90b187..1016afd4 100644 --- a/tests/cli/snapshots/cli__list_files_json_well_tested.snap +++ b/tests/cli/snapshots/cli__list_files_json_well_tested.snap @@ -1,5 +1,6 @@ --- source: tests/cli/main.rs +assertion_line: 73 expression: "String::from_utf8_lossy(&output.stdout)" --- [ @@ -39,6 +40,10 @@ expression: "String::from_utf8_lossy(&output.stdout)" "package": "cargo-mutants-testdata-well-tested", "path": "src/result.rs" }, + { + "package": "cargo-mutants-testdata-well-tested", + "path": "src/sets.rs" + }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/simple_fns.rs" diff --git a/tests/cli/snapshots/cli__list_files_text_well_tested.snap b/tests/cli/snapshots/cli__list_files_text_well_tested.snap index 1918d3be..ced53712 100644 --- a/tests/cli/snapshots/cli__list_files_text_well_tested.snap +++ b/tests/cli/snapshots/cli__list_files_text_well_tested.snap @@ -1,5 +1,6 @@ --- source: tests/cli/main.rs +assertion_line: 73 expression: "String::from_utf8_lossy(&output.stdout)" --- src/lib.rs @@ -11,6 +12,7 @@ src/methods.rs src/nested_function.rs src/numbers.rs src/result.rs +src/sets.rs src/simple_fns.rs src/struct_with_lifetime.rs diff --git a/tests/cli/snapshots/cli__list_mutants_in_all_trees_as_json.snap b/tests/cli/snapshots/cli__list_mutants_in_all_trees_as_json.snap index 33d28793..28ab71c6 100644 --- a/tests/cli/snapshots/cli__list_mutants_in_all_trees_as_json.snap +++ b/tests/cli/snapshots/cli__list_mutants_in_all_trees_as_json.snap @@ -873,6 +873,33 @@ expression: buf "replacement": "Ok(Default::default())", "genre": "FnValue" }, + { + "package": "cargo-mutants-testdata-well-tested", + "file": "src/sets.rs", + "line": 3, + "function": "make_a_set", + "return_type": "-> BTreeSet", + "replacement": "BTreeSet::new()", + "genre": "FnValue" + }, + { + "package": "cargo-mutants-testdata-well-tested", + "file": "src/sets.rs", + "line": 3, + "function": "make_a_set", + "return_type": "-> BTreeSet", + "replacement": "BTreeSet::from_iter([String::new()])", + "genre": "FnValue" + }, + { + "package": "cargo-mutants-testdata-well-tested", + "file": "src/sets.rs", + "line": 3, + "function": "make_a_set", + "return_type": "-> BTreeSet", + "replacement": "BTreeSet::from_iter([\"xyzzy\".into()])", + "genre": "FnValue" + }, { "package": "cargo-mutants-testdata-well-tested", "file": "src/simple_fns.rs", diff --git a/tests/cli/snapshots/cli__list_mutants_in_all_trees_as_text.snap b/tests/cli/snapshots/cli__list_mutants_in_all_trees_as_text.snap index 78027aaf..6206807e 100644 --- a/tests/cli/snapshots/cli__list_mutants_in_all_trees_as_text.snap +++ b/tests/cli/snapshots/cli__list_mutants_in_all_trees_as_text.snap @@ -220,6 +220,9 @@ src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("") src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") src/result.rs:9: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:17: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::new() +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) src/simple_fns.rs:7: replace returns_unit with () src/simple_fns.rs:12: replace returns_42u32 -> u32 with 0 src/simple_fns.rs:12: replace returns_42u32 -> u32 with 1 diff --git a/tests/cli/snapshots/cli__list_mutants_json_well_tested.snap b/tests/cli/snapshots/cli__list_mutants_json_well_tested.snap index 9e974f64..a4314d27 100644 --- a/tests/cli/snapshots/cli__list_mutants_json_well_tested.snap +++ b/tests/cli/snapshots/cli__list_mutants_json_well_tested.snap @@ -1,5 +1,6 @@ --- source: tests/cli/main.rs +assertion_line: 73 expression: "String::from_utf8_lossy(&output.stdout)" --- [ @@ -165,6 +166,33 @@ expression: "String::from_utf8_lossy(&output.stdout)" "replacement": "Ok(Default::default())", "genre": "FnValue" }, + { + "package": "cargo-mutants-testdata-well-tested", + "file": "src/sets.rs", + "line": 3, + "function": "make_a_set", + "return_type": "-> BTreeSet", + "replacement": "BTreeSet::new()", + "genre": "FnValue" + }, + { + "package": "cargo-mutants-testdata-well-tested", + "file": "src/sets.rs", + "line": 3, + "function": "make_a_set", + "return_type": "-> BTreeSet", + "replacement": "BTreeSet::from_iter([String::new()])", + "genre": "FnValue" + }, + { + "package": "cargo-mutants-testdata-well-tested", + "file": "src/sets.rs", + "line": 3, + "function": "make_a_set", + "return_type": "-> BTreeSet", + "replacement": "BTreeSet::from_iter([\"xyzzy\".into()])", + "genre": "FnValue" + }, { "package": "cargo-mutants-testdata-well-tested", "file": "src/simple_fns.rs", diff --git a/tests/cli/snapshots/cli__list_mutants_well_tested.snap b/tests/cli/snapshots/cli__list_mutants_well_tested.snap index f7467059..d07946ff 100644 --- a/tests/cli/snapshots/cli__list_mutants_well_tested.snap +++ b/tests/cli/snapshots/cli__list_mutants_well_tested.snap @@ -1,5 +1,6 @@ --- source: tests/cli/main.rs +assertion_line: 73 expression: "String::from_utf8_lossy(&output.stdout)" --- src/arc.rs:3: replace return_arc -> Arc with Arc::new(String::new()) @@ -20,6 +21,9 @@ src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("") src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") src/result.rs:9: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:17: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::new() +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) src/simple_fns.rs:7: replace returns_unit with () src/simple_fns.rs:12: replace returns_42u32 -> u32 with 0 src/simple_fns.rs:12: replace returns_42u32 -> u32 with 1 diff --git a/tests/cli/snapshots/cli__list_mutants_well_tested_exclude_name_filter.snap b/tests/cli/snapshots/cli__list_mutants_well_tested_exclude_name_filter.snap index 796388e8..d737c6d4 100644 --- a/tests/cli/snapshots/cli__list_mutants_well_tested_exclude_name_filter.snap +++ b/tests/cli/snapshots/cli__list_mutants_well_tested_exclude_name_filter.snap @@ -20,6 +20,9 @@ src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("") src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") src/result.rs:9: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:17: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::new() +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) src/struct_with_lifetime.rs:14: replace Lex<'buf>::buf_len -> usize with 0 src/struct_with_lifetime.rs:14: replace Lex<'buf>::buf_len -> usize with 1 diff --git a/tests/cli/snapshots/cli__well_tested_tree_check_only.snap b/tests/cli/snapshots/cli__well_tested_tree_check_only.snap index c6d384cc..9b5ce147 100644 --- a/tests/cli/snapshots/cli__well_tested_tree_check_only.snap +++ b/tests/cli/snapshots/cli__well_tested_tree_check_only.snap @@ -2,7 +2,7 @@ source: tests/cli/main.rs expression: stdout --- -Found 27 mutants to test +Found 30 mutants to test Unmutated baseline ... ok src/arc.rs:3: replace return_arc -> Arc with Arc::new(String::new()) ... ok src/arc.rs:3: replace return_arc -> Arc with Arc::new("xyzzy".into()) ... ok @@ -22,6 +22,9 @@ src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("") . src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") ... ok src/result.rs:9: replace error_if_negative -> Result<(), ()> with Ok(()) ... ok src/result.rs:17: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) ... ok +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::new() ... ok +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) ... ok +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) ... ok src/simple_fns.rs:7: replace returns_unit with () ... ok src/simple_fns.rs:12: replace returns_42u32 -> u32 with 0 ... ok src/simple_fns.rs:12: replace returns_42u32 -> u32 with 1 ... ok @@ -31,5 +34,5 @@ src/simple_fns.rs:26: replace double_string -> String with String::new() ... ok src/simple_fns.rs:26: replace double_string -> String with "xyzzy".into() ... ok src/struct_with_lifetime.rs:14: replace Lex<'buf>::buf_len -> usize with 0 ... ok src/struct_with_lifetime.rs:14: replace Lex<'buf>::buf_len -> usize with 1 ... ok -27 mutants tested: 27 succeeded +30 mutants tested: 30 succeeded diff --git a/tests/cli/snapshots/cli__well_tested_tree_finds_no_problems.snap b/tests/cli/snapshots/cli__well_tested_tree_finds_no_problems.snap index aa686a1e..105cffd0 100644 --- a/tests/cli/snapshots/cli__well_tested_tree_finds_no_problems.snap +++ b/tests/cli/snapshots/cli__well_tested_tree_finds_no_problems.snap @@ -2,7 +2,7 @@ source: tests/cli/main.rs expression: stdout --- -Found 27 mutants to test +Found 30 mutants to test Unmutated baseline ... ok src/arc.rs:3: replace return_arc -> Arc with Arc::new(String::new()) ... caught src/arc.rs:3: replace return_arc -> Arc with Arc::new("xyzzy".into()) ... caught @@ -22,6 +22,9 @@ src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("") . src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") ... caught src/result.rs:9: replace error_if_negative -> Result<(), ()> with Ok(()) ... caught src/result.rs:17: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) ... caught +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::new() ... caught +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) ... caught +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) ... caught src/simple_fns.rs:7: replace returns_unit with () ... caught src/simple_fns.rs:12: replace returns_42u32 -> u32 with 0 ... caught src/simple_fns.rs:12: replace returns_42u32 -> u32 with 1 ... caught @@ -31,5 +34,5 @@ src/simple_fns.rs:26: replace double_string -> String with String::new() ... cau src/simple_fns.rs:26: replace double_string -> String with "xyzzy".into() ... caught src/struct_with_lifetime.rs:14: replace Lex<'buf>::buf_len -> usize with 0 ... caught src/struct_with_lifetime.rs:14: replace Lex<'buf>::buf_len -> usize with 1 ... caught -27 mutants tested: 27 caught +30 mutants tested: 30 caught diff --git a/tests/cli/snapshots/cli__well_tested_tree_finds_no_problems__caught.txt.snap b/tests/cli/snapshots/cli__well_tested_tree_finds_no_problems__caught.txt.snap index de94e050..4d9f2095 100644 --- a/tests/cli/snapshots/cli__well_tested_tree_finds_no_problems__caught.txt.snap +++ b/tests/cli/snapshots/cli__well_tested_tree_finds_no_problems__caught.txt.snap @@ -20,6 +20,9 @@ src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("") src/result.rs:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") src/result.rs:9: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:17: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::new() +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) +src/sets.rs:3: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) src/simple_fns.rs:7: replace returns_unit with () src/simple_fns.rs:12: replace returns_42u32 -> u32 with 0 src/simple_fns.rs:12: replace returns_42u32 -> u32 with 1 diff --git a/tests/cli/snapshots/cli__well_tested_tree_quiet.snap b/tests/cli/snapshots/cli__well_tested_tree_quiet.snap index 5be0187d..d8d3237f 100644 --- a/tests/cli/snapshots/cli__well_tested_tree_quiet.snap +++ b/tests/cli/snapshots/cli__well_tested_tree_quiet.snap @@ -2,7 +2,7 @@ source: tests/cli/main.rs expression: stdout --- -Found 27 mutants to test +Found 30 mutants to test Unmutated baseline ... ok -27 mutants tested: 27 caught +30 mutants tested: 30 caught From eb9b918fa78137796bb5fd77a3988295e69a4005 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Mon, 12 Jun 2023 07:33:33 -0700 Subject: [PATCH 4/6] Recognize any T or T<'a, A> --- NEWS.md | 3 +++ src/visit.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/NEWS.md b/NEWS.md index 693a832b..c028df6a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,9 @@ - Mutate `BinaryHeap`, `BTreeSet`, `HashSet`, `LinkedList`, and `VecDeque` to generate empty and one-element containers. +- Generically recognize functions returning `T` or `T<'a, A>'` and try + to construct them from an `A`. + ## 23.6.0 - Generate `Box::leak(Box::new(...))` as a mutation of functions returning diff --git a/src/visit.rs b/src/visit.rs index 9103f982..f35519da 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -333,6 +333,9 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { } else if path.is_ident("String") { reps.push(quote! { String::new() }); reps.push(quote! { "xyzzy".into() }); + } else if path.is_ident("str") { + reps.push(quote! { "" }); + reps.push(quote! { "xyzzy" }); } else if path_is_unsigned(path) { reps.push(quote! { 0 }); reps.push(quote! { 1 }); @@ -408,6 +411,23 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec { quote! { #collection_type::from_iter([#rep]) } }), ); + } else if let Some((collection_type, inner_type)) = maybe_collection_or_container(path) + { + // Something like `T` or `T<'a, A>`, when we don't know exactly how + // to call it, but we strongly suspect that you could construct it from + // an `A`. For example, `Cow`. + reps.push(quote! { #collection_type::new() }); + reps.extend( + type_replacements(inner_type, error_exprs) + .into_iter() + .flat_map(|rep| { + [ + quote! { #collection_type::from_iter([#rep]) }, + quote! { #collection_type::new(#rep) }, + quote! { #collection_type::from(#rep) }, + ] + }), + ); } else { reps.push(quote! { Default::default() }); } @@ -536,6 +556,27 @@ fn known_collection(path: &Path) -> Option<(&Ident, &Type)> { None } +/// Match a type with one type argument, which might be a container or collection. +fn maybe_collection_or_container(path: &Path) -> Option<(&Ident, &Type)> { + let last = path.segments.last()?; + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = + &last.arguments + { + let type_args: Vec<_> = args + .iter() + .filter_map(|a| match a { + GenericArgument::Type(t) => Some(t), + _ => None, + }) + .collect(); + // TODO: Return the path with args stripped out. + if type_args.len() == 1 { + return Some((&last.ident, type_args.first().unwrap())); + } + } + None +} + fn path_is_float(path: &Path) -> bool { ["f32", "f64"].iter().any(|s| path.is_ident(s)) } @@ -959,6 +1000,22 @@ mod test { ); } + #[test] + fn cow_replacement() { + assert_eq!( + replace(&parse_quote! { -> Cow<'static, str> }, &[]), + &[ + "Cow::new()", + "Cow::from_iter([\"\"])", + "Cow::new(\"\")", + "Cow::from(\"\")", + "Cow::from_iter([\"xyzzy\"])", + "Cow::new(\"xyzzy\")", + "Cow::from(\"xyzzy\")", + ] + ); + } + fn replace(return_type: &ReturnType, error_exprs: &[Expr]) -> Vec { return_type_replacements(return_type, error_exprs) .into_iter() From 26cfd8d860032b469386f5dfa47237ebb5d8f4e8 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Thu, 14 Sep 2023 16:39:44 -0600 Subject: [PATCH 5/6] Doc --- src/visit.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/visit.rs b/src/visit.rs index d1a9e7ba..86da9944 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -116,6 +116,9 @@ fn walk_file( /// `syn` visitor that recursively traverses the syntax tree, accumulating places /// that could be mutated. +/// +/// As it walks the tree, it accumulates within itself a list of mutation opportunities, +/// and other files referenced by `mod` statements that should be visited later. struct DiscoveryVisitor<'o> { /// All the mutants generated by visiting the file. mutants: Vec, From 65c1eaccd9d7f43f7304571f081afac7d1163c40 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Thu, 14 Sep 2023 16:53:42 -0600 Subject: [PATCH 6/6] clean up --- src/visit.rs | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/visit.rs b/src/visit.rs index 86da9944..d3fda331 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -247,15 +247,13 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { /// Visit `mod foo { ... }` or `mod foo;`. fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) { + // TODO: Maybe while visiting the file we should only collect the + // `mod` statements, and then find the files separately, to keep IO + // effects away from parsing the file. let mod_name = &node.ident.unraw().to_string(); - let _span = trace_span!( - "mod", - line = node.mod_token.span.start().line, - name = mod_name - ) - .entered(); + let _span = trace_span!("mod", line = node.mod_token.span.start().line, mod_name).entered(); if attrs_excluded(&node.attrs) { - trace!("mod {:?} excluded by attrs", node.ident,); + trace!("mod excluded by attrs"); return; } // If there's no content in braces, then this is a `mod foo;` @@ -511,20 +509,19 @@ fn path_ends_with(path: &Path, ident: &str) -> bool { /// If so, return the short name (like "Box") and the inner type. fn known_container(path: &Path) -> Option<(&Ident, &Type)> { let last = path.segments.last()?; - if !["Box", "Cell", "RefCell", "Arc", "Rc", "Mutex"] + if ["Box", "Cell", "RefCell", "Arc", "Rc", "Mutex"] .iter() .any(|v| last.ident == v) { - return None; - } - if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = - &last.arguments - { - // TODO: Skip lifetime args. - // TODO: Return the path with args stripped out. - if args.len() == 1 { - if let Some(GenericArgument::Type(inner_type)) = args.first() { - return Some((&last.ident, inner_type)); + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = + &last.arguments + { + // TODO: Skip lifetime args. + // TODO: Return the path with args stripped out. + if args.len() == 1 { + if let Some(GenericArgument::Type(inner_type)) = args.first() { + return Some((&last.ident, inner_type)); + } } } }