Skip to content

Commit

Permalink
Add and use mutate_source_str
Browse files Browse the repository at this point in the history
Fixes #433
  • Loading branch information
sourcefrog committed Nov 12, 2024
1 parent d3736f1 commit 2f77802
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 19 deletions.
20 changes: 19 additions & 1 deletion src/mutate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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");
Expand Down
42 changes: 31 additions & 11 deletions src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Option<SourceFile>> {
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()
Expand Down Expand Up @@ -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,
)
Expand All @@ -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,
)
Expand Down
42 changes: 35 additions & 7 deletions src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,7 @@ pub fn walk_tree(
options: &Options,
console: &Console,
) -> Result<Discovered> {
let error_exprs = options
.error_values
.iter()
.map(|e| syn::parse_str(e).with_context(|| format!("Failed to parse error value {e:?}")))
.collect::<Result<Vec<Expr>>>()?;
let error_exprs = options.parsed_error_exprs()?;
console.walk_tree_start();
let mut file_queue: VecDeque<SourceFile> = top_source_files.iter().cloned().collect();
let mut mutants = Vec::new();
Expand All @@ -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,
)?)
Expand Down Expand Up @@ -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<Vec<Mutant>> {
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
Expand Down Expand Up @@ -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"]
);
}
}

0 comments on commit 2f77802

Please sign in to comment.