Skip to content

Commit

Permalink
Match more collection/container types
Browse files Browse the repository at this point in the history
  • Loading branch information
sourcefrog committed Sep 14, 2023
2 parents dd0a3f8 + 65c1eac commit ee86df0
Show file tree
Hide file tree
Showing 17 changed files with 278 additions and 37 deletions.
13 changes: 12 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## Unreleased

- Mutate the known collection types `BinaryHeap`, `BTreeSet`, `HashSet`,
`LinkedList`, and `VecDeque` to generate empty and one-element collections
using `T::new()` and `T::from_iter(..)`.

- Mutate known container types like `Arc`, `Box`, `Cell`, `Mutex`, `Rc`,
`RefCell` into `T::new(a)`.

- Mutate unknown types that look like containers or collections `T<A>` or
`T<'a, A>'` and try to construct them from an `A` with `T::from_iter`,
`T::new`, and `T::from`.

- Minimum Rust version updated to 1.70.

## 23.9.0
Expand All @@ -22,7 +33,7 @@

- Recurse into return types, so that for example `Result<bool>` can generate
`Ok(true)` and `Ok(false)`, and `Some<T>` generates `None` and every generated
value of `T`. Similarly for `Box<T>`, and `Vec<T>`.
value of `T`. Similarly for `Box<T>`, `Vec<T>`, `Rc<T>`, `Arc<T>`.

- Generate specific values for integers: `[0, 1]` for unsigned integers,
`[0, 1, -1]` for signed integers; `[1]` for NonZero unsigned integers and
Expand Down
2 changes: 2 additions & 0 deletions book/src/mutants.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ More mutation genres and patterns will be added in future releases.
| `Box<T>` | `Box::new(...)` |
| `Vec<T>` | `vec![]`, `vec![...]` |
| `Arc<T>` | `Arc::new(...)` |
| `Rc<T>` | `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) |
| `HttpResponse` | `HttpResponse::Ok().finish` |
Expand Down
189 changes: 161 additions & 28 deletions src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -115,6 +115,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<Mutant>,
Expand Down Expand Up @@ -243,15 +246,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;`
Expand Down Expand Up @@ -317,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<TokenStream> {
// 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, .. }) => {
Expand All @@ -327,6 +333,9 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec<TokenStream> {
} 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 });
Expand Down Expand Up @@ -361,14 +370,6 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec<TokenStream> {
}));
} else if path_ends_with(path, "HttpResponse") {
reps.push(quote! { HttpResponse::Ok().finish() });
} 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(
Expand All @@ -389,7 +390,9 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec<TokenStream> {
quote! { vec![#rep] }
}),
)
} else if let Some(inner_type) = match_first_type_arg(path, "Arc") {
} 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
// imported, but we must strip or rewrite the arguments, so that
// `std::sync::Arc<String>` becomes either `std::sync::Arc::<String>::new`
Expand All @@ -398,9 +401,35 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> Vec<TokenStream> {
type_replacements(inner_type, error_exprs)
.into_iter()
.map(|rep| {
quote! { Arc::new(#rep) }
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 if let Some((collection_type, inner_type)) = maybe_collection_or_container(path)
{
// Something like `T<A>` 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() });
}
Expand Down Expand Up @@ -473,6 +502,82 @@ 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 known_container(path: &Path) -> Option<(&Ident, &Type)> {
let last = path.segments.last()?;
if ["Box", "Cell", "RefCell", "Arc", "Rc", "Mutex"]
.iter()
.any(|v| last.ident == v)
{
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
}

/// 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
}

/// 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))
}
Expand Down Expand Up @@ -882,15 +987,43 @@ 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<String> }, &[]),
// &["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<String> }, &[]),
&["Rc::new(String::new())", "Rc::new(\"xyzzy\".into())"]
);
}

#[test]
fn btreeset_replacement() {
assert_eq!(
replace(&parse_quote! { -> std::collections::BTreeSet<String> }, &[]),
&[
"BTreeSet::new()",
"BTreeSet::from_iter([String::new()])",
"BTreeSet::from_iter([\"xyzzy\".into()])"
]
);
}

#[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<String> {
return_type_replacements(return_type, error_exprs)
Expand Down
1 change: 1 addition & 0 deletions testdata/tree/well_tested/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ mod methods;
mod nested_function;
mod numbers;
mod result;
mod sets;
pub mod simple_fns;
mod struct_with_lifetime;
13 changes: 13 additions & 0 deletions testdata/tree/well_tested/src/sets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use std::collections::BTreeSet;

fn make_a_set() -> BTreeSet<String> {
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);
}
4 changes: 2 additions & 2 deletions tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,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);
}
Expand Down
5 changes: 5 additions & 0 deletions tests/cli/snapshots/cli__list_files_json_well_tested.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: tests/cli/main.rs
assertion_line: 73
expression: "String::from_utf8_lossy(&output.stdout)"
---
[
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions tests/cli/snapshots/cli__list_files_text_well_tested.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: tests/cli/main.rs
assertion_line: 73
expression: "String::from_utf8_lossy(&output.stdout)"
---
src/lib.rs
Expand All @@ -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

27 changes: 27 additions & 0 deletions tests/cli/snapshots/cli__list_mutants_in_all_trees_as_json.snap
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,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<String>",
"replacement": "BTreeSet::new()",
"genre": "FnValue"
},
{
"package": "cargo-mutants-testdata-well-tested",
"file": "src/sets.rs",
"line": 3,
"function": "make_a_set",
"return_type": "-> BTreeSet<String>",
"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<String>",
"replacement": "BTreeSet::from_iter([\"xyzzy\".into()])",
"genre": "FnValue"
},
{
"package": "cargo-mutants-testdata-well-tested",
"file": "src/simple_fns.rs",
Expand Down
Loading

0 comments on commit ee86df0

Please sign in to comment.