Skip to content

Commit

Permalink
Merge pull request #9 from nfurfaro/release/v1.1.1
Browse files Browse the repository at this point in the history
Release/v1.1.1
  • Loading branch information
nfurfaro authored Feb 14, 2024
2 parents 98f3a0e + 30420dd commit 12fc1d2
Show file tree
Hide file tree
Showing 14 changed files with 475 additions and 646 deletions.
448 changes: 44 additions & 404 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hunter"
version = "1.1.0"
version = "1.1.1"
edition = "2021"
rust-version = "1.74.0"
authors = ["Nick Furfaro <furnic@skiff.com>"]
Expand All @@ -12,16 +12,16 @@ path = "src/main.rs"

[dependencies]
anyhow = "1.0"
chrono = "0.4.31"
clap = { version = "4.4.7", features = ["derive"] }
colored = "2.0.4"
ctrlc = "3.1.0"
dialoguer = "0.11.0"
indicatif = {version = "*", features = ["rayon"]}
lazy_static = "1.4.0"
prettytable-rs = "^0.10"
rand = "0.8.5"
rayon = "1.5"
regex = "1.10.2"
sled = "0.34.7"
tempdir = "0.3.7"
tempfile = "3.8.1"
tokio = { version = "1.8.0", features = ["macros", "rt-multi-thread"] }
toml = "0.8.8"

40 changes: 23 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ A Rust CLI mutation-testing tool for Noir source code.
## Disclaimer

> !!! Note: Hunter is currently in its alpha stage of development. While functional, it is still under active development. This means that there may be bugs and/or significant changes. It is not recommended to use this tool in a production environment or for securing code that protects valuable assets. Hunter, like many mutation testing tools, is designed to assist in writing improved tests. It is NOT a substitute for creating tests or conducting thorough code reviews.
## Inspiration

[Vertigo](https://github.com/JoranHonig/vertigo) A mutation testing tool for Solidity x Truffle by Joran Honig

[Guide to mutation testing](https://testrigor.com/blog/understanding-mutation-testing-a-comprehensive-guide/) bt Artem Golubev

[Mutation Testing at scale](https://homes.cs.washington.edu/~rjust/publ/practical_mutation_testing_tr_2021.pdf) by Goran Petrovi´c, Marko Ivankovi´c, Gordon Fraser, René Just

## Overview

Expand All @@ -13,17 +20,19 @@ At a high level, Hunter exposes a CLI with 2 primary commands, scan and mutate.
## Installation

There are 2 ways to install Hunter: via cURL or by building from source.

### cURL

1. Download the binary using cURL:
`curl -LO https://github.com/nfurfaro/hunter/releases/download/v1.1.0/install.sh | bash`
1. Download the installation script using cURL:
`curl -LO https://github.com/nfurfaro/hunter/blob/master/scripts/install.sh | bash`

2. Make the binary executable:
`chmod +x hunter`
2. Make the script executable:
`chmod +x install.sh`

3. Move the binary to a directory in your PATH (i.e: /usr/local/bin):
`mv hunter /usr/local/bin`
3. run the script:
`./install.sh`

This will install the binary to `/usr/local/bin`.
You should now be able to run the program by typing `hunter` in your terminal!

### Build from source
Expand All @@ -47,16 +56,18 @@ The simplest way to get started with Hunter is to `$ cd` into the root of the pr
`$ hunter scan`. By default, this will scan the current directory and all subdirectories for Noir source files. It will then print a summary of the results to the terminal.

The next step is to run the mutate command:
`$ hunter mutate`. This will apply the mutations to the source code, run the tests, and generate a report. Pass the `--verbose`/`-v` flag to print a report to the terminal. If the scan command indicated that there is a high number of test runs required, you may want to refer to the [filtering options](#filtering-options) section to limit the scope of the source code analysed.
`$ hunter mutate`. This will apply the mutations to the source code, run the tests, and generate a report. If the `scan` command indicated that there is a high number of test runs required, you may want to refer to the [filtering options](#filtering-options) section to limit the scope of the source code analysed.

## Help

To see Hunter's help menu, run `hunter --help`.

`hunter --info` will give some more context on the results of the `mutate` command.

## About Mutation Testing

At a high level, mutation testing is a way to measure the quality of a test suite.
It is possible to have %100 test coverage and still have poor quality/incomplete tests. Mutation testing helps to identify these cases.
It is possible to have 100% test coverage and still have poor quality/incomplete tests. Mutation testing helps to identify these cases.


Mutation testing involves modifying the source code of a program in small ways. Specifically, it modifies the program by replacing an operator with another operator. Each modification is called a mutant. For each mutant, we run the existing test suite against the mutated code. If at least one test fails, the mutant is "killed". If all tests pass, the mutant "survives". The mutation score is the percentage of mutants that are killed by the test suite, calculated as follows:
Expand All @@ -76,7 +87,7 @@ Hunter assumes the following:

The larger the project and test suites are, the longer the mutation testing run will take. By default, Hunter will run your entire test suite for each mutant generated (in parallel). See the [filtering options](#filtering-options) section for ways to limit the number of tests run per mutant.

> Note: Hunter currently only targets in-language unit tests written in the same file as the source they test. It does not currently support tests written in separate files or in a separate directory, but this is definitely a feature that is planned for a future release!
> Note: Hunter currently only targets in-language unit tests written in the same file as the source they test. It does not currently support tests written in separate files or in a separate directory, but this is definitely a feature that is planned for the next release!
## Mutations

Expand All @@ -94,10 +105,6 @@ Hunter currently supports the following mutations:

`==`, `!=`, `>`, `>=`, `<`, and `<=`.

### Logical operators

`&` and `|`.

### Shorthand operators

`+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, and `>>=`.
Expand All @@ -108,8 +115,6 @@ Hunter currently takes the approach of using deterministic rules to determine wh

To see how Hunter currently determines which mutations to apply, check out the `./src/token.rs::token_transformer()` function.

> Note: there is now a optional --random flag that can be passed to the mutate command. This will apply mutations randomly instead of using the deterministic rules. This is a WIP feature, and is likely to increase the number of mutants with an Unbuildable status.
## Output & Reporting Options

By default, Hunter will output all reports to the terminal.
Expand All @@ -128,6 +133,7 @@ By using this targeted approach methodically, you can incrementally test your co

Hunter ignores the following directories by default:

`./temp`, `./target`, `./test`, `./lib`, `./script`
`./temp`, `./target`, & `./test`

If you want to test source files in any of these directories, simply cd into the directory and run the `hunter scan` command from there.
If you want to test source files in any of these directories, simply cd into the directory and run the `scan` or `mutate` command from there.
You will be prompted to choose if you want to continue, just select `yes`.
6 changes: 4 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct Args {
pub random: bool,
/// The path to the source files directory
#[clap(short, long, default_value = ".")]
pub source_path: Option<std::path::PathBuf>,
pub source_path: std::path::PathBuf,
/// The path to the output file (.md extension recommended)
#[clap(short = 'o', long)]
pub output_path: Option<std::path::PathBuf>,
Expand Down Expand Up @@ -67,8 +67,10 @@ pub async fn run_cli() -> Result<()> {
Some(Subcommand::Mutate) => {
let result = handlers::scanner::scan(args.clone(), config.clone_box());
if let Ok(mut result) = result {
let final_result =
handlers::mutator::mutate(args.clone(), config.clone_box(), &mut result);
let _ = print_scan_results(&mut result.clone(), config.clone_box());
handlers::mutator::mutate(args.clone(), config.clone_box(), &mut result)
final_result
} else {
Err(result.unwrap_err())
}
Expand Down
5 changes: 3 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::languages;
use crate::languages::common::Language;
use std::{io, path::PathBuf, process};
use tempfile::TempDir;

pub trait LanguageConfig {
fn language(&self) -> languages::common::Language;
Expand All @@ -10,9 +11,9 @@ pub trait LanguageConfig {
fn test_command(&self) -> &'static str;
fn build_command(&self) -> &'static str;
fn manifest_name(&self) -> &'static str;
fn is_test_failed(&self, stderr: &str) -> bool;
// fn is_test_failed(&self, stderr: &str) -> bool;
fn excluded_dirs(&self) -> Vec<&'static str>;
fn setup_test_infrastructure(&self) -> io::Result<(PathBuf, PathBuf)>;
fn setup_test_infrastructure(&self) -> io::Result<(TempDir, PathBuf)>;
fn test_mutant_project(&self) -> Box<process::Output>;
fn build_mutant_project(&self) -> Box<process::Output>;
fn clone_box(&self) -> Box<dyn LanguageConfig + Send + Sync>;
Expand Down
105 changes: 87 additions & 18 deletions src/file_manager.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,77 @@
use crate::{
config::LanguageConfig, handlers::mutator::Mutant, token::token_as_bytes, utils::replace_bytes,
};
use colored::*;
use dialoguer::Confirm;
use lazy_static::lazy_static;
use std::sync::Mutex;
use std::{
fs::{self, File, OpenOptions},
io::{self, Read, Result, Write},
path::{Path, PathBuf},
};

pub struct Defer<T: FnOnce()>(pub Option<T>);
// a wrapper around a closure that is called when the Defer object is dropped.
impl<T: FnOnce()> Drop for Defer<T> {
fn drop(&mut self) {
if let Some(f) = self.0.take() {
f();
// pub struct Defer<T: FnOnce()>(pub Option<T>);
// // a wrapper around a closure that is called when the Defer object is dropped.
// impl<T: FnOnce()> Drop for Defer<T> {
// fn drop(&mut self) {
// if let Some(f) = self.0.take() {
// f();
// }
// }
// }

pub fn scan_for_excluded_dirs<'a>(
dir_path: &'a Path,
config: &'a dyn LanguageConfig,
) -> Result<Vec<PathBuf>> {
// Check if the current directory is in the list of excluded directories
let current_dir = std::env::current_dir()?;

if let Some(current_dir_name) = current_dir.file_name() {
let current_dir_name = current_dir_name.to_string_lossy();
if config
.excluded_dirs()
.iter()
.any(|dir| dir.trim_end_matches('/') == &*current_dir_name)
{
eprintln!(
"{}",
format!(
"Warning: You are attempting to use Hunter in an excluded directory: {}",
current_dir.display()
)
.red()
);
eprintln!(
"{}",
format!(
"Excluded directories are set in the languages/{}.rs file",
config.name().to_lowercase()
)
.yellow()
);

if !Confirm::new()
.with_prompt("Do you want to proceed?")
.interact()
.unwrap()
{
// User does not want to proceed, exit the program
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"User chose not to proceed",
));
}
}
}

let base_dir = std::env::current_dir()?;
find_source_file_paths(&base_dir, dir_path, config)
}

pub fn find_source_file_paths<'a>(
base_dir: &'a Path,
dir_path: &'a Path,
config: &'a dyn LanguageConfig,
) -> Result<Vec<PathBuf>> {
Expand All @@ -28,15 +82,15 @@ pub fn find_source_file_paths<'a>(
let entry = entry?;
let path_buf = entry.path();
if path_buf.is_dir() {
// Skipped directories are not included in the results
// Skip directories that are in the list of excluded directories
if config
.excluded_dirs()
.iter()
.any(|&dir| path_buf.ends_with(dir) || path_buf.starts_with(dir))
{
continue;
}
let path_result = find_source_file_paths(&path_buf, config);
let path_result = find_source_file_paths(base_dir, &path_buf, config);
match path_result {
Ok(sub_results_paths) => {
paths.extend(sub_results_paths.clone());
Expand All @@ -47,18 +101,29 @@ pub fn find_source_file_paths<'a>(
.extension()
.map_or(false, |extension| extension == config.ext())
{
paths.push(path_buf);
let relative_path = path_buf.strip_prefix(base_dir).unwrap_or(&path_buf);
paths.push(relative_path.to_path_buf());
}
}

if paths.is_empty() {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No files found",
))
} else {
Ok(paths)
}
} else {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Input path is not a directory",
))
}
if paths.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No files found",
));
}
}

Ok(paths)
lazy_static! {
static ref LIB_FILE_MUTEX: Mutex<()> = Mutex::new(());
}

pub fn copy_src_to_temp_file(
Expand All @@ -69,8 +134,11 @@ pub fn copy_src_to_temp_file(
let temp_file = src_dir.join(format!("mutation_{}.{}", mutant.id(), lang_ext));
fs::copy(mutant.path(), &temp_file)?;

// Lock the mutex before writing to the file
let _guard = LIB_FILE_MUTEX.lock().unwrap();

let mut lib_file = OpenOptions::new()
.append(true)
.write(true)
.open(src_dir.join(format!("lib.{}", lang_ext)))?;
writeln!(lib_file, "mod mutation_{};", mutant.id())?;

Expand All @@ -83,11 +151,12 @@ pub fn mutate_temp_file(temp_file: &std::path::PathBuf, m: &mut Mutant) {
file.read_to_string(&mut contents).unwrap();

let mut original_bytes = contents.into_bytes();

replace_bytes(
&mut original_bytes,
m.span_start() as usize,
&m.bytes(),
token_as_bytes(&m.token()).unwrap(),
token_as_bytes(&m.mutation()).unwrap(),
);
contents = String::from_utf8_lossy(original_bytes.as_slice()).into_owned();

Expand Down
13 changes: 6 additions & 7 deletions src/handlers/scanner.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
cli::Args,
config::LanguageConfig,
file_manager::find_source_file_paths,
file_manager::scan_for_excluded_dirs,
filters::test_regex,
handlers::mutator::{mutants, Mutant},
reporter::count_tests,
Expand All @@ -11,7 +11,7 @@ use crate::{
use colored::*;
use std::{
io::{Error, ErrorKind, Result},
path::{Path, PathBuf},
path::PathBuf,
};

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -62,14 +62,12 @@ impl ScanResult {
}

pub fn scan(args: Args, config: Box<dyn LanguageConfig>) -> Result<ScanResult> {
let source_path = args
.source_path
.clone()
.unwrap_or(Path::new(".").to_path_buf());
let source_path = args.source_path;

let paths = if source_path.is_file() {
vec![source_path]
} else {
find_source_file_paths(source_path.as_path(), &*config).map_err(|_| {
scan_for_excluded_dirs(source_path.as_path(), &*config).map_err(|_| {
let err_msg = format!(
"No {} files found... Are you in the right directory?",
config.name().red()
Expand All @@ -90,6 +88,7 @@ pub fn scan(args: Args, config: Box<dyn LanguageConfig>) -> Result<ScanResult> {
}

// @todo consider adding a switch here to mutate all tokens in source files, or only those in files with unit tests
// @todo improve error message to comunicate that no tokens were found, no target source files were found, or that no unit tests were found in the source files.
let meta_tokens = collect_tokens(contains_unit_tests.clone(), config).expect("No tokens found");

let mutants = mutants(&meta_tokens, args.random);
Expand Down
Loading

0 comments on commit 12fc1d2

Please sign in to comment.