Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recognize and mutate containers and collections #148

Merged
merged 8 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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<Mutant>,
Expand Down Expand Up @@ -244,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;`
Expand Down Expand Up @@ -318,6 +319,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 @@ -328,6 +334,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 @@ -362,14 +371,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 @@ -390,7 +391,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 @@ -399,9 +402,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 @@ -474,6 +503,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 @@ -883,15 +988,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
Loading