diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..085d398 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,28 @@ +name: Rust CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Build + run: cargo build --verbose + - name: Format + run: cargo fmt -- --check + - name: Clippy + run: cargo clippy -- -D warnings + - name: Test + run: cargo test --verbose diff --git a/.gitignore b/.gitignore index 7908654..2e23c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -target/ temp/ -campaigns.db/ \ No newline at end of file +**/*.rs.bk \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ce52673..0e20c55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,7 +422,7 @@ checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hunter" -version = "0.1.0" +version = "1.0.0" dependencies = [ "anyhow", "chrono", @@ -430,6 +430,7 @@ dependencies = [ "colored", "indicatif", "prettytable-rs", + "rand 0.8.5", "rayon", "regex", "sled", @@ -658,6 +659,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "prettytable-rs" version = "0.10.0" @@ -703,6 +710,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -718,6 +746,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rayon" version = "1.8.0" @@ -925,7 +962,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" dependencies = [ - "rand", + "rand 0.4.6", "remove_dir_all", ] diff --git a/Cargo.toml b/Cargo.toml index d9a5cab..5ec7b0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "hunter" -version = "0.1.0" +version = "1.0.0" edition = "2021" +authors = ["Nick Furfaro "] [[bin]] name = "hunter" @@ -9,22 +10,16 @@ path = "src/main.rs" [dependencies] anyhow = "1.0" +chrono = "0.4.31" clap = { version = "4.4.7", features = ["derive"] } colored = "2.0.4" indicatif = {version = "*", features = ["rayon"]} prettytable-rs = "^0.10" -tokio = { version = "1.8.0", features = ["macros", "rt-multi-thread"] } +rand = "0.8.5" rayon = "1.5" -tempfile = "3.8.1" -tempdir = "0.3.7" -sled = "0.34.7" - -# switch to git dependency -# noirc_frontend = { path = "../../noir_dev/noir/compiler/noirc_frontend" } -# noirc_errors = { path = "../../noir_dev/noir/compiler/noirc_errors"} 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" -chrono = "0.4.31" - -# noirc_frontend = { path = "../../../../../..//Users/furnic/dev/noir_dev/noir/compiler/noirc_frontend" } -# noirc_errors = { path = "../../../../../..//Users/furnic/dev/noir_dev/noir/compiler/noirc_errors"} diff --git a/README.md b/README.md index 174620e..fbefac5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A Rust CLI mutation-testing tool for Noir source code. ## Disclaimer -> !!! Note: Hunter is currently in its alpha stage of development. Although it's functional and can be utilized, it's still under active development. This means that there may be significant changes, potential bugs, and evolving methodologies. It's 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 should not be considered a substitute for creating tests or conducting thorough code reviews. +> !!! 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. ## Overview @@ -12,13 +12,42 @@ 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///releases/download//hunter` + +2. Make the binary executable: +`chmod +x hunter` + +3. Move the binary to a directory in your PATH (i.e: /usr/local/bin): +`mv hunter /usr/local/bin` + +You should now be able to run the program by typing `hunter` in your terminal! + +### Build from source + +Alternatively, you can build Hunter from source. To do so, you'll need to have Rust installed. If you don't have Rust installed, you can install it by following the instructions [here](https://www.rust-lang.org/tools/install). + +1. Clone the repo: +`git clone git@github.com:nfurfaro/hunter.git` + +2. Build the project: +`cargo build --release` + +3. Move the binary to a directory in your PATH (i.e: /usr/local/bin): +`mv ./target/release/hunter /usr/local/bin` + +You should now be able to run the program by typing `hunter` in your terminal! + ## Quickstart -The simplest way to get started with Hunter is to `$ cd` into the root of the project you want to test. From there, it is reccommended to run the scan command: +The simplest way to get started with Hunter is to `$ cd` into the root of the project you want to test. From there, it is recommended to run the scan command: `$ 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 my 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. 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. ## Help @@ -27,52 +56,48 @@ To see Hunter's help menu, run `hunter --help`. ## 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 a 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: ```mutation_score = (killed_mutants / total_mutants) * 100``` -The closer the score is to %100, the better the test suite is at detecting changes or errors in the source code. - +The closer the score is to 100%, the better the test suite is at detecting changes or errors in the source code. To learn more about mutation testing, check out [this article](https://www.joranhonig.nl/introduction-into-mutation/). - ## Assumptions and Limitations Hunter assumes the following: - - the user has Nargo installed in their PATH. - - the test suite for the project you want to mutate is currently all passing. If there are failing tests, you're not ready to perform mutation testing yet! + +- the user has Nargo installed in their PATH. +- the test suite for the project you want to mutate is currently all passing. If there are failing tests, you're not ready to perform mutation testing yet! 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. Hunter currently only targets unit tests written in Noir, in the same file as the source they test. - ## Mutations Hunter currently supports the following mutations: -### Arithmetic operators: +### Arithmetic operators `+`, `-`, `*`, `/`, and `%`. -### Bitwise operators: +### Bitwise operators `!`, `&`, `|`, `^`, `<<`, and `>>`. -### Predicate/Comparison operators: +### Predicate/Comparison operators `==`, `!=`, `>`, `>=`, `<`, and `<=`. - -### Logical operators: +### Logical operators `&` and `|`. - ### Shorthand operators `+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, and `>>=`. @@ -89,13 +114,13 @@ By default, Hunter will output all reports to the terminal. ![Alt text](image-1.png) -For a larger project it can be helpful to generate a report file. This can be achieved by passing the `--output-path` (`-o`) flag to the `mutate` command, and specifying a path to a file. For example, `hunter mutate --output-path ./hunter_report.md`. This will generate a markdown file with the report in table format mimicing the tables printed to stdout by default. +For a larger project, it can be helpful to generate a report file. This can be achieved by passing the `--output-path` (`-o`) flag to the `mutate` command, and specifying a path to a file. For example, `hunter mutate --output-path ./hunter_report.md`. This will generate a markdown file with the report in table format mimicking the tables printed to stdout by default. ## Filtering Options -Hunter currently provides an option to filter the number of mutants generated by limiting the scope of source code analysed. This can be useful for larger projects with a large number of tests. Using the `--source-path`(`-s`) flag, you can specify a path to a directory containing the source code you want to mutate. For example, `hunter mutate --source-path ./src/main.nr`will limit the scope of the source code analysed to the `./src/main.nr` file. +Hunter currently provides an option to filter the number of mutants generated by limiting the scope of source code analyzed. This can be useful for larger projects with a large number of tests. Using the `--source-path`(`-s`) flag, you can specify a path to a directory containing the source code you want to mutate. For example, `hunter mutate --source-path ./src/main.nr`will limit the scope of the source code analysed to the `./src/main.nr` file. By using this targeted approach methodically, you can incrementally test your codebase and improve your test suite. -> Note: This is in contrast to the approach taken by some other mutation testing tools which is to optionally set the sample size, which then (non-deterministially) limits the number of mutants generated to cut down the run time. +> Note: This is in contrast to the approach taken by some other mutation testing tools which is to optionally set the sample size, which then (non-deterministically) limits the number of mutants generated to cut down the run time. ## Excluded Directories @@ -109,4 +134,4 @@ If you want to test source files in any of these directories, simply cd into the It should be relatively straightforward to add support for a new language to Hunter. The primary place you'll need to make modifications is in `src/config.rs`. Search for the `@extendable` tag in the comments to see where you'll need to make changes. -The Rust compiler should guide you to any other places you need to modify. \ No newline at end of file +The Rust compiler should guide you to any other places you need to modify. diff --git a/src/cli.rs b/src/cli.rs index 9150cba..263a742 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,8 +1,4 @@ -use crate::{ - config::{config, Language}, - handlers, - reporter::print_scan_results, -}; +use crate::{config::config, handlers, languages::common::Language, reporter::print_scan_results}; use clap::Parser; use colored::*; use std::io::Result; @@ -18,19 +14,19 @@ pub enum Subcommand { /// Mutate Noir code and run tests against each mutation. #[derive(Parser, PartialEq, Default, Clone, Debug)] pub struct Args { - /// The target language. + /// The target language #[clap(short, long, default_value = "Noir")] language: Option, /// The path to the source files directory #[clap(short, long, default_value = ".")] pub source_path: Option, - /// The path to the output file, defaults to ./hunter_report.txt if not provided + /// The path to the output file (.md extension recommended) #[clap(short = 'o', long)] pub output_path: Option, // Display information about the program #[clap(short, long)] info: bool, - // Collect info about number of mutants found without running tests + // Choose between running the scan or mutate subcommands #[clap(subcommand)] subcommand: Option, } @@ -41,33 +37,29 @@ pub async fn run_cli() -> Result<()> { if args.info { println!( "{}", - "Welcome to Hunter, a multi-language mutation-testing tool.".cyan() + "Welcome to Hunter, a mutation-testing tool for Noir source code.".cyan() ); return Ok(()); } - let language = args.language.clone().unwrap(); - - let config = config(language); + let config = config(args.language.clone().unwrap()); match args.subcommand { Some(Subcommand::Scan) => { - let results = handlers::scanner::scan(args.clone(), &config); - if let Ok(results) = results { - print_scan_results(&mut results.clone(), &config) + let result = handlers::scanner::scan(args.clone(), config.clone_box()); + if let Ok(result) = result { + print_scan_results(&mut result.clone(), config) } else { - eprintln!("{}", results.unwrap_err()); - Ok(()) + Err(result.unwrap_err()) } } Some(Subcommand::Mutate) => { - let result = handlers::scanner::scan(args.clone(), &config); - if let Ok(mut results) = result { - let _ = print_scan_results(&mut results.clone(), &config); - handlers::mutator::mutate(args.clone(), config.clone(), &mut results) + let result = handlers::scanner::scan(args.clone(), config.clone_box()); + if let Ok(mut result) = result { + let _ = print_scan_results(&mut result.clone(), config.clone_box()); + handlers::mutator::mutate(args.clone(), config.clone_box(), &mut result) } else { - eprintln!("{}", result.unwrap_err()); - Ok(()) + Err(result.unwrap_err()) } } None => { diff --git a/src/config.rs b/src/config.rs index 889d127..38f984e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,90 +1,29 @@ -use std::str::FromStr; - -#[derive(Clone, Debug, PartialEq)] -pub struct Config { - language: Language, - test_runner: &'static str, - test_command: &'static str, - build_command: &'static str, - manifest_name: &'static str, -} - -impl Config { - pub fn language(&self) -> Language { - self.language.clone() - } - - pub fn test_runner(&self) -> &'static str { - self.test_runner - } - - pub fn test_command(&self) -> &'static str { - self.test_command - } - - pub fn build_command(&self) -> &'static str { - self.build_command - } - - pub fn manifest_name(&self) -> &'static str { - self.manifest_name - } -} - -// @extendable: add a new variant here to support a new language -#[derive(Clone, Debug, PartialEq)] -pub enum Language { - Noir, +use crate::languages; +use crate::languages::common::Language; +use std::{io, path::PathBuf}; + +pub trait LanguageConfig { + fn language(&self) -> languages::common::Language; + fn name(&self) -> &'static str; + fn ext(&self) -> &'static str; + fn test_runner(&self) -> &'static str; + 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 excluded_dirs(&self) -> Vec<&'static str>; + fn setup_test_infrastructure(&self) -> io::Result<(PathBuf, PathBuf)>; + fn clone_box(&self) -> Box; } -impl FromStr for Language { - type Err = &'static str; - - // @extendable: add a new match arm here to support a new language - fn from_str(s: &str) -> std::result::Result { - match s.to_lowercase().as_str() { - "noir" => Ok(Language::Noir), - _ => Err("no matching languages supported (yet)."), - } - } -} - -impl Language { - // @extendable: add a new match arm here to support a new language - pub fn name(&self) -> &'static str { - match self { - Language::Noir => "Noir", - } - } - - // @extendable: add a new match arm here to support a new language - pub fn ext(&self) -> &'static str { - match self { - Language::Noir => "nr", - } - } -} - -// @extendable: add a new match arm here to support a new language -pub fn config(language: Language) -> Config { - match language { - Language::Noir => Config { - language: Language::Noir, - test_runner: "nargo", - test_command: "test", - build_command: "build", - manifest_name: "Nargo.toml", - }, - } -} +// pub trait TestSetup { +// fn setup_temp_dirs(&self) -> io::Result<(PathBuf, PathBuf)>; +// } // @extendable: add a new match arm here to support a new language -pub fn is_test_failed(stderr: &str, language: &Language) -> bool { +pub fn config(language: Language) -> Box { match language { - Language::Noir => { - stderr.contains("test failed") - || stderr.contains("FAILED") - || stderr.contains("Failed constraint") - } + Language::Noir => Box::new(languages::noir::NoirConfig), + // other languages... } } diff --git a/src/file_manager.rs b/src/file_manager.rs index 205f941..6bc4989 100644 --- a/src/file_manager.rs +++ b/src/file_manager.rs @@ -1,7 +1,4 @@ -use crate::{ - config::{Config, Language}, - handlers::mutator::Mutant, -}; +use crate::{config::LanguageConfig, handlers::mutator::Mutant, languages::common::Language}; use std::{ fs::{self, File, OpenOptions}, io::{self, Result, Write}, @@ -18,7 +15,10 @@ impl Drop for Defer { } } -pub fn find_source_file_paths<'a>(dir_path: &'a Path, config: &'a Config) -> Result> { +pub fn find_source_file_paths<'a>( + dir_path: &'a Path, + config: &'a dyn LanguageConfig, +) -> Result> { let mut paths: Vec = vec![]; if dir_path.is_dir() { @@ -26,10 +26,9 @@ pub fn find_source_file_paths<'a>(dir_path: &'a Path, config: &'a Config) -> Res let entry = entry?; let path_buf = entry.path(); if path_buf.is_dir() { - // Skipped directories - let excluded_dirs = ["./temp", "./target", "./test", "./lib", "./script"]; - - if excluded_dirs + // Skipped directories are not included in the results + if config + .excluded_dirs() .iter() .any(|&dir| path_buf.ends_with(dir) || path_buf.starts_with(dir)) { @@ -44,7 +43,7 @@ pub fn find_source_file_paths<'a>(dir_path: &'a Path, config: &'a Config) -> Res } } else if path_buf .extension() - .map_or(false, |extension| extension == config.language().ext()) + .map_or(false, |extension| extension == config.ext()) { paths.push(path_buf); } @@ -82,7 +81,7 @@ pub fn setup_temp_dirs(language: Language) -> io::Result<(PathBuf, PathBuf)> { name = "hunter_temp" type = "lib" authors = ["Hunter"] - compiler_version = "0.22.2" + compiler_version = "0.22.0" "# )?; let _ = File::create(src_dir.join("lib.nr"))?; @@ -95,18 +94,14 @@ pub fn setup_temp_dirs(language: Language) -> io::Result<(PathBuf, PathBuf)> { pub fn write_mutation_to_temp_file( mutant: &Mutant, src_dir: PathBuf, - config: Config, + lang_ext: &'static str, ) -> io::Result { - let temp_file = src_dir.join(format!( - "mutation_{}.{}", - mutant.id(), - config.language().ext() - )); + let temp_file = src_dir.join(format!("mutation_{}.{}", mutant.id(), lang_ext)); fs::copy(mutant.path(), &temp_file)?; let mut lib_file = OpenOptions::new() .append(true) - .open(src_dir.join(format!("lib.{}", config.language().ext())))?; + .open(src_dir.join(format!("lib.{}", lang_ext)))?; writeln!(lib_file, "mod mutation_{};", mutant.id())?; Ok(temp_file) diff --git a/src/filters.rs b/src/filters.rs index 6b06856..0cc79d6 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,4 +1,4 @@ -use crate::config::Language; +use crate::languages::common::Language; use regex::Regex; pub fn test_regex(language: &Language) -> Regex { diff --git a/src/handlers/mutator.rs b/src/handlers/mutator.rs index e6d277d..8305019 100644 --- a/src/handlers/mutator.rs +++ b/src/handlers/mutator.rs @@ -1,9 +1,9 @@ use crate::cli::Args; -use crate::config::Config; +use crate::config::LanguageConfig; use crate::handlers::scanner::ScanResult; use crate::processor::process_mutants; use crate::reporter::{print_table, surviving_mutants_table}; -use crate::token::{token_as_bytes, token_transformer, Token}; +use crate::token::{random_token, token_as_bytes, token_transformer, MetaToken, Token}; use colored::*; use std::{ fmt, @@ -82,13 +82,36 @@ impl Mutant { } } +pub fn mutants(meta_tokens: &Vec, random: bool) -> Vec { + let mut mutants: Vec = vec![]; + for entry in meta_tokens { + let path = entry.src().clone(); + let maybe_mutant = mutant_builder( + entry.id(), + entry.token().clone(), + entry.span(), + path, + random, + ); + match maybe_mutant { + None => continue, + Some(m) => mutants.push(m), + } + } + mutants +} + pub fn mutant_builder( id: u32, token: Token, span: (u32, u32), src_path: PathBuf, + random: bool, ) -> Option { - let mutation = token_transformer(token.clone()).unwrap(); + let mutation = match random { + true => random_token(), + false => token_transformer(token.clone()).unwrap(), + }; match token { Token::Equal => Some(Mutant { id, @@ -342,11 +365,11 @@ pub fn mutant_builder( } } -pub fn mutate(args: Args, config: Config, results: &mut ScanResult) -> Result<()> { +pub fn mutate(args: Args, config: Box, results: &mut ScanResult) -> Result<()> { let mutants = results.mutants(); println!("{}", "Running tests...".green()); - process_mutants(mutants, args.clone(), config.clone()); + process_mutants(mutants, args.clone(), config.clone_box()); if mutants .iter() diff --git a/src/handlers/scanner.rs b/src/handlers/scanner.rs index 92a90d5..a75bf18 100644 --- a/src/handlers/scanner.rs +++ b/src/handlers/scanner.rs @@ -1,9 +1,9 @@ use crate::{ cli::Args, - config::Config, + config::LanguageConfig, file_manager::find_source_file_paths, filters::test_regex, - handlers::mutator::{mutant_builder, Mutant}, + handlers::mutator::{mutants, Mutant}, reporter::count_tests, token::MetaToken, utils::collect_tokens, @@ -17,6 +17,7 @@ use std::{ #[derive(Debug, Clone)] pub struct ScanResult { paths: Vec, + contains_unit_tests: Vec, meta_tokens: Vec, test_count: usize, mutants: Vec, @@ -25,12 +26,14 @@ pub struct ScanResult { impl ScanResult { pub fn new( paths: Vec, + contains_unit_tests: Vec, meta_tokens: Vec, test_count: usize, mutants: Vec, ) -> ScanResult { ScanResult { paths, + contains_unit_tests, meta_tokens, test_count, mutants, @@ -41,6 +44,10 @@ impl ScanResult { &self.paths } + pub fn contains_unit_tests(&self) -> &Vec { + &self.contains_unit_tests + } + pub fn meta_tokens(&self) -> &Vec { &self.meta_tokens } @@ -54,7 +61,7 @@ impl ScanResult { } } -pub fn scan(args: Args, config: &Config) -> Result { +pub fn scan(args: Args, config: Box) -> Result { let source_path = args .source_path .clone() @@ -62,27 +69,36 @@ pub fn scan(args: Args, config: &Config) -> Result { let paths = if source_path.is_file() { vec![source_path] } else { - find_source_file_paths(source_path.as_path(), config).map_err(|_| { + find_source_file_paths(source_path.as_path(), &*config).map_err(|_| { let err_msg = format!( "No {} files found... Are you in the right directory?", - config.language().name().red() + config.name().red() ); Error::new(ErrorKind::Other, err_msg) })? }; - let test_count = count_tests(paths.clone(), test_regex(&config.language()), config); - let meta_tokens = collect_tokens(paths.clone(), config).expect("No tokens found"); + let mut test_count = 0; + let mut contains_unit_tests: Vec = vec![]; - let mut mutants: Vec = vec![]; - for entry in &meta_tokens { - let path = entry.src().clone(); - let maybe_mutant = mutant_builder(entry.id(), entry.token().clone(), entry.span(), path); - match maybe_mutant { - None => continue, - Some(m) => mutants.push(m), + for path in &paths { + let num_tests = count_tests(path, test_regex(&config.language())); + if num_tests > 0 { + contains_unit_tests.push(path.clone()); + test_count += num_tests; } } - Ok(ScanResult::new(paths, meta_tokens, test_count, mutants)) + // @todo consider adding a switch here to mutate all tokens in source files, or only those in files with unit tests + let meta_tokens = collect_tokens(contains_unit_tests.clone(), config).expect("No tokens found"); + + let mutants = mutants(&meta_tokens, false); + + Ok(ScanResult::new( + paths, + contains_unit_tests, + meta_tokens, + test_count, + mutants, + )) } diff --git a/src/languages/common.rs b/src/languages/common.rs new file mode 100644 index 0000000..29afa33 --- /dev/null +++ b/src/languages/common.rs @@ -0,0 +1,19 @@ +use std::str::FromStr; + +#[derive(Clone, Debug, PartialEq)] +// @extendable: add a new variant here to support a new language +pub enum Language { + Noir, +} + +impl FromStr for Language { + type Err = &'static str; + + // @extendable: add a new match arm here to support a new language + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "noir" => Ok(Language::Noir), + _ => Err("no matching languages supported (yet)."), + } + } +} diff --git a/src/languages/mod.rs b/src/languages/mod.rs new file mode 100644 index 0000000..ffc1df4 --- /dev/null +++ b/src/languages/mod.rs @@ -0,0 +1,2 @@ +pub mod common; +pub mod noir; diff --git a/src/languages/noir.rs b/src/languages/noir.rs new file mode 100644 index 0000000..c1749cc --- /dev/null +++ b/src/languages/noir.rs @@ -0,0 +1,82 @@ +use crate::config::LanguageConfig; +use crate::languages::common::Language; +use std::{ + fs::{self, File}, + io::{self, Write}, + path::PathBuf, +}; + +#[derive(Clone)] +pub struct NoirConfig; + +impl LanguageConfig for NoirConfig { + fn language(&self) -> Language { + Language::Noir + } + + fn name(&self) -> &'static str { + "Noir" + } + + fn ext(&self) -> &'static str { + "nr" + } + + fn test_runner(&self) -> &'static str { + "nargo" + } + + fn test_command(&self) -> &'static str { + "test" + } + + fn build_command(&self) -> &'static str { + "build" + } + + fn manifest_name(&self) -> &'static str { + "Nargo.toml" + } + + fn is_test_failed(&self, stderr: &str) -> bool { + stderr.contains("test failed") + || stderr.contains("FAILED") + || stderr.contains("Failed constraint") + } + + fn excluded_dirs(&self) -> Vec<&'static str> { + vec![ + "./temp", "./target", "./test", "./tests", "./lib", "./script", + ] + } + + fn setup_test_infrastructure(&self) -> io::Result<(PathBuf, PathBuf)> { + // Create a ./temp directory + let temp_dir = PathBuf::from("./temp"); + fs::create_dir_all(&temp_dir)?; + + // Inside /temp, create a src/ directory + let src_dir = temp_dir.join("src"); + fs::create_dir_all(&src_dir)?; + + let mut manifest = File::create(temp_dir.join("Nargo.toml"))?; + + write!( + manifest, + r#" + [package] + name = "hunter_temp" + type = "lib" + authors = ["Hunter"] + compiler_version = "0.22.0" + "# + )?; + let _ = File::create(src_dir.join("lib.nr"))?; + + Ok((temp_dir, src_dir)) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/src/main.rs b/src/main.rs index 8aef9d0..4dbfbee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ -use anyhow::Result; +use std::io::Result; +use std::process; pub mod config; pub mod file_manager; pub mod filters; pub mod handlers; +pub mod languages; pub mod processor; pub mod reporter; pub mod token; @@ -11,6 +13,12 @@ pub mod utils; mod cli; #[tokio::main] -async fn main() -> Result<()> { - Ok(cli::run_cli().await?) +async fn main() { + let result: Result<()> = cli::run_cli().await; + if let Err(e) = result { + eprintln!("{}", e); + process::exit(1); + } else { + process::exit(0); + } } diff --git a/src/processor.rs b/src/processor.rs index ca6fe25..68b986d 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -6,12 +6,15 @@ use std::{ atomic::{AtomicUsize, Ordering}, Arc, }, + // os::unix::process, }; +use std::process; + use crate::{ cli::Args, - config::{is_test_failed, Config}, - file_manager::{setup_temp_dirs, write_mutation_to_temp_file, Defer}, + config::LanguageConfig, + file_manager::{write_mutation_to_temp_file, Defer}, handlers::mutator::{calculate_mutation_score, Mutant, MutationStatus}, reporter::{mutants_progress_bar, mutation_test_summary_table, print_table}, utils::*, @@ -20,21 +23,31 @@ use crate::{ use colored::*; use rayon::prelude::*; -pub fn process_mutants(mutants: &mut Vec, args: Args, config: Config) { +pub fn process_mutants( + mutants: &mut Vec, + args: Args, + config: Box, +) { let original_dir = std::env::current_dir().unwrap(); let total_mutants = mutants.len(); let bar = mutants_progress_bar(total_mutants); let destroyed = Arc::new(AtomicUsize::new(0)); let survived = Arc::new(AtomicUsize::new(0)); let pending = Arc::new(AtomicUsize::new(total_mutants)); - let (temp_dir, temp_src_dir) = setup_temp_dirs(config.language()).unwrap(); - // handles cleanup of the temp directories after this function returns. + let (temp_dir, temp_src_dir) = config.setup_test_infrastructure().unwrap(); + + // @note handles cleanup of the temp directories after this function returns. let _cleanup = Defer(Some(|| { let _ = fs::remove_dir_all(&temp_dir); })); + let extension = config.ext(); + let test_runner = config.test_runner(); + let build_command = config.build_command(); + let test_command = config.test_command(); + mutants.par_iter_mut().for_each(|m| { - let temp_file = write_mutation_to_temp_file(m, temp_src_dir.clone(), config.clone()) + let temp_file = write_mutation_to_temp_file(m, temp_src_dir.clone(), extension) .expect("Failed to setup test infrastructure"); let mut contents = String::new(); @@ -52,43 +65,41 @@ pub fn process_mutants(mutants: &mut Vec, args: Args, config: Config) { file.write_all(contents.as_bytes()).unwrap(); // Build the project - let build_output = Command::new(config.test_runner()) - .arg(config.build_command()) + let build_output = Command::new(test_runner) + .arg(build_command) .output() .expect("Failed to execute build command"); // run_test_suite - let output = Command::new(config.test_runner()) - .arg(config.test_command()) + let output = Command::new(test_runner) + .arg(test_command) .output() .expect("Failed to execute command"); match build_output.status.code() { - Some(0) => { - match output.status.code() { - Some(0) => { - println!("Build was successful"); - println!("Test suite passed"); - m.set_status(MutationStatus::Survived); - survived.fetch_add(1, Ordering::SeqCst); + Some(0) => match output.status.code() { + Some(0) => { + println!("Build was successful"); + println!("Test suite passed"); + m.set_status(MutationStatus::Survived); + survived.fetch_add(1, Ordering::SeqCst); + pending.fetch_sub(1, Ordering::SeqCst); + } + Some(_) => { + println!("Test suite failed"); + let stderr = String::from_utf8_lossy(&output.stderr); + println!("stderr: {}", stderr); + if config.is_test_failed(&stderr) { + destroyed.fetch_add(1, Ordering::SeqCst); pending.fetch_sub(1, Ordering::SeqCst); - } - Some(_) => { - println!("Test suite failed"); - let stderr = String::from_utf8_lossy(&output.stderr); - println!("stderr: {}", stderr); - if is_test_failed(&stderr, &config.language()) { - destroyed.fetch_add(1, Ordering::SeqCst); - pending.fetch_sub(1, Ordering::SeqCst); - m.set_status(MutationStatus::Killed); - } - } - None => { - println!("Test suite was killed by a signal or crashed"); - // @todo Handle this case + m.set_status(MutationStatus::Killed); } } - } + None => { + eprintln!("Test suite was killed by a signal or crashed"); + process::exit(1); + } + }, Some(_) => { destroyed.fetch_add(1, Ordering::SeqCst); pending.fetch_sub(1, Ordering::SeqCst); @@ -96,7 +107,7 @@ pub fn process_mutants(mutants: &mut Vec, args: Args, config: Config) { } None => { println!("Build was killed by a signal or crashed"); - // @todo Handle this case + process::exit(1); } } @@ -105,7 +116,7 @@ pub fn process_mutants(mutants: &mut Vec, args: Args, config: Config) { eprintln!("Failed to change back to the original directory: {}", e); } - // Note: the /temp dir and its contents will be deleted automatically, + // @note the /temp dir and its contents will be deleted automatically, // so this might seem redundant. However, Hunter deletes the file // as soon as possible to help prevent running out of space when testing very large projects. if let Err(e) = std::fs::remove_file(&temp_file) { diff --git a/src/reporter.rs b/src/reporter.rs index d8ff47a..710cb92 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -1,5 +1,5 @@ use crate::{ - config::Config, + config::LanguageConfig, handlers::{ mutator::{Mutant, MutationStatus}, scanner::ScanResult, @@ -16,12 +16,12 @@ use std::{ path::{Path, PathBuf}, }; -pub fn print_scan_results(results: &mut ScanResult, config: &Config) -> Result<()> { +pub fn print_scan_results(results: &mut ScanResult, config: Box) -> Result<()> { println!("{}", "Initiating source file analysis...".green()); println!( "{}", - format!("Searching for {} files", config.language().name()).green() + format!("Searching for {} files", config.name()).green() ); println!( @@ -29,7 +29,35 @@ pub fn print_scan_results(results: &mut ScanResult, config: &Config) -> Result<( format!("Files found: {}", results.paths().len()).cyan() ); - for path in results.paths() { + // for path in results.paths() { + // println!("{}", format!("{}", path.display()).red()); + // } + + let noir_files_without_unit_tests = results.paths().len() - results.contains_unit_tests().len(); + + println!("Hunter currently only mutates files containing unit tests."); + + println!( + "{}", + format!( + "Skipping {} {} files.", + noir_files_without_unit_tests, + config.name(), + ) + .magenta() + ); + + println!( + "{}", + format!( + "{} files containing unit tests: {}", + config.name(), + results.contains_unit_tests().len() + ) + .cyan() + ); + + for path in results.contains_unit_tests() { println!("{}", format!("{}", path.display()).red()); } @@ -43,7 +71,7 @@ pub fn print_scan_results(results: &mut ScanResult, config: &Config) -> Result<( ); println!( "{}", - format!("Test runs required: {}", num_mutants * results.test_count()).magenta() + format!("tests to run: {}", num_mutants * results.test_count()).magenta() ); Ok(()) @@ -171,21 +199,13 @@ pub fn mutants_progress_bar(total_mutants: usize) -> ProgressBar { bar } -pub fn count_tests(paths: Vec, pattern: Regex, _config: &Config) -> usize { +pub fn count_tests(path: &Path, pattern: Regex) -> usize { let mut test_count = 0; - - if paths.is_empty() { - 0 - } else { - for path in paths { - let file = File::open(path.clone()).expect("Unable to open file"); - let mut buf_reader = BufReader::new(file); - let mut contents = String::new(); - let _res = buf_reader.read_to_string(&mut contents); - - let test_matches = pattern.find_iter(&contents).count(); - test_count += test_matches; - } - test_count - } + let file = File::open(path).expect("Unable to open file"); + let mut buf_reader = BufReader::new(file); + let mut contents = String::new(); + let _res = buf_reader.read_to_string(&mut contents); + let test_matches = pattern.find_iter(&contents).count(); + test_count += test_matches; + test_count } diff --git a/src/token.rs b/src/token.rs index 583f787..a09b150 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,3 +1,4 @@ +use rand::seq::SliceRandom; use regex::Regex; use std::path::PathBuf; @@ -192,6 +193,49 @@ pub fn raw_string_as_token(raw: &str) -> Option { } } +pub fn all_tokens() -> Vec { + vec![ + Token::Equal, + Token::NotEqual, + Token::Less, + Token::LessEqual, + Token::Greater, + Token::GreaterEqual, + Token::Ampersand, + Token::Pipe, + Token::Caret, + Token::ShiftLeft, + Token::ShiftRight, + Token::Plus, + Token::Minus, + Token::Star, + Token::Slash, + Token::Percent, + Token::Increment, + Token::Decrement, + Token::PlusEquals, + Token::MinusEquals, + Token::StarEquals, + Token::SlashEquals, + Token::PercentEquals, + Token::AmpersandEquals, + Token::PipeEquals, + Token::CaretEquals, + Token::ShiftLeftEquals, + Token::ShiftRightEquals, + Token::DoublePipe, + Token::DoubleAmpersand, + Token::Bang, + ] +} + +pub fn random_token() -> Token { + all_tokens() + .choose(&mut rand::thread_rng()) + .unwrap() + .clone() +} + pub fn token_transformer(token: Token) -> Option { match token { Token::Equal => Some(Token::NotEqual), @@ -950,7 +994,7 @@ mod tests { let token = Token::Equal; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::NotEqual); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -967,7 +1011,7 @@ mod tests { let token = Token::NotEqual; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Equal); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -984,7 +1028,7 @@ mod tests { let token = Token::Greater; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::LessEqual); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1001,7 +1045,7 @@ mod tests { let token = Token::GreaterEqual; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Less); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1018,7 +1062,7 @@ mod tests { let token = Token::Less; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::GreaterEqual); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1035,7 +1079,7 @@ mod tests { let token = Token::LessEqual; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Greater); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1052,7 +1096,7 @@ mod tests { let token = Token::Ampersand; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Pipe); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1069,7 +1113,7 @@ mod tests { let token = Token::Pipe; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Ampersand); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1086,7 +1130,7 @@ mod tests { let token = Token::Caret; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Ampersand); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1103,7 +1147,7 @@ mod tests { let token = Token::Plus; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Minus); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1120,7 +1164,7 @@ mod tests { let token = Token::Minus; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Plus); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1137,7 +1181,7 @@ mod tests { let token = Token::Star; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Slash); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1154,7 +1198,7 @@ mod tests { let token = Token::Slash; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Star); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1170,7 +1214,7 @@ mod tests { let token = Token::Percent; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.token(), Token::Star); let bytes_str = String::from_utf8(mutant.bytes().clone()).expect("Failed to convert bytes to string"); @@ -1187,7 +1231,7 @@ mod tests { let token = Token::Increment; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1200,7 +1244,7 @@ mod tests { let token = Token::Decrement; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1213,7 +1257,7 @@ mod tests { let token = Token::PlusEquals; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1226,7 +1270,7 @@ mod tests { let token = Token::MinusEquals; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1239,7 +1283,7 @@ mod tests { let token = Token::StarEquals; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1252,7 +1296,7 @@ mod tests { let token = Token::SlashEquals; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1265,7 +1309,7 @@ mod tests { let token = Token::PercentEquals; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1278,7 +1322,7 @@ mod tests { let token = Token::AmpersandEquals; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1291,7 +1335,7 @@ mod tests { let token = Token::PipeEquals; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); assert_eq!(mutant.path(), path); @@ -1304,7 +1348,7 @@ mod tests { let token = Token::CaretEquals; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); @@ -1318,7 +1362,7 @@ mod tests { let token = Token::ShiftLeftEquals; let span = (0, 3); let id = 42; - let mutant = mutant_builder(id, token.clone(), span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token.clone(), span, path.clone(), false).unwrap(); assert_eq!(mutant.id(), id); assert_eq!(mutant.token(), token_transformer(token.clone()).unwrap()); @@ -1338,7 +1382,7 @@ mod tests { let token = Token::ShiftRightEquals; let span = (0, 3); let id = 42; - let mutant = mutant_builder(id, token, span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token, span, path.clone(), false).unwrap(); assert_eq!(mutant.span_start(), span.0); assert_eq!(mutant.span_end(), span.1); @@ -1352,7 +1396,7 @@ mod tests { let token = Token::DoublePipe; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token.clone(), span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token.clone(), span, path.clone(), false).unwrap(); assert_eq!(mutant.id(), id); assert_eq!(mutant.token(), token_transformer(token.clone()).unwrap()); @@ -1372,7 +1416,7 @@ mod tests { let token = Token::DoubleAmpersand; let span = (0, 2); let id = 42; - let mutant = mutant_builder(id, token.clone(), span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token.clone(), span, path.clone(), false).unwrap(); assert_eq!(mutant.id(), id); assert_eq!(mutant.token(), token_transformer(token.clone()).unwrap()); @@ -1392,7 +1436,7 @@ mod tests { let token = Token::Bang; let span = (0, 1); let id = 42; - let mutant = mutant_builder(id, token.clone(), span, path.clone()).unwrap(); + let mutant = mutant_builder(id, token.clone(), span, path.clone(), false).unwrap(); assert_eq!(mutant.id(), id); assert_eq!(mutant.token(), token_transformer(token.clone()).unwrap()); diff --git a/src/utils.rs b/src/utils.rs index 3a8ed6e..35ba740 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ use crate::{ - config::Config, + config::LanguageConfig, filters::{comment_regex, literal_regex, test_regex}, token::{raw_string_as_token, token_regexes, MetaToken}, }; @@ -16,7 +16,10 @@ fn overlaps(filter: &Range, token: &Range) -> bool { (token.start as usize) > filter.start && (token.end as usize) < filter.end } -pub fn collect_tokens(paths: Vec, config: &Config) -> Option> { +pub fn collect_tokens( + paths: Vec, + config: Box, +) -> Option> { let mut tokens: Vec = Vec::new(); let language = config.language(); @@ -137,7 +140,7 @@ pub fn replace_bytes(original_bytes: &mut Vec, start_index: usize, replaceme #[cfg(test)] mod tests { use super::*; - use crate::config::Language; + use crate::languages::common::Language; #[test] fn test_test_regex_noir() { diff --git a/target/x86_64-unknown-linux-gnu/release/hunter b/target/x86_64-unknown-linux-gnu/release/hunter new file mode 100755 index 0000000..3cda112 Binary files /dev/null and b/target/x86_64-unknown-linux-gnu/release/hunter differ