Skip to content

Commit

Permalink
feat: apply coverage file for specific mutants
Browse files Browse the repository at this point in the history
Added a `--coverage` option to both `move-mutator` and
`move-mutation-test` tools.

This flag can be used to generate more specific mutants that target only
the source code that has unit test coverage.
This can be useful when the test suite lacks tests.
  • Loading branch information
Rqnsom committed Oct 2, 2024
1 parent 0ad435a commit 5eb5217
Show file tree
Hide file tree
Showing 13 changed files with 732 additions and 421 deletions.
759 changes: 395 additions & 364 deletions Cargo.lock

Large diffs are not rendered by default.

33 changes: 18 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ rust-version = "1.78.0"

[workspace.dependencies]
anyhow = "1.0"
aptos = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
aptos-framework = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
aptos-gas-schedule = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
aptos-types = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
aptos-vm = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
# temporary use my fork since we need this commit:
# https://github.com/Rqnsom/aptos-core/commit/2dbeacf7fe0d8e37aa98effd32cda8d40daf0502
aptos = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
aptos-framework = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
aptos-gas-schedule = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
aptos-types = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
aptos-vm = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
clap = { version = "4.5", features = ["derive"] }
codespan = "0.11"
codespan-reporting = "0.11"
Expand All @@ -33,17 +35,18 @@ either = "1.9"
fixed = "= 1.25.1" # required by aptos deps
itertools = "0.13"
log = "0.4"
move-cli = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-command-line-common = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-compiler = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-compiler-v2 = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-model = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-cli = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-command-line-common = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-compiler = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-compiler-v2 = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-coverage = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-model = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-mutator = { path = "move-mutator" }
move-package = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-prover = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-symbol-pool = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-unit-test = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-vm-runtime = { git = "https://github.com/aptos-labs/aptos-core.git", branch = "main" }
move-package = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-prover = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-symbol-pool = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-unit-test = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
move-vm-runtime = { git = "https://github.com/Rqnsom/aptos-core.git", branch = "main" }
num = "0.4"
num-traits = "0.2"
pretty_env_logger = "0.5"
Expand Down
28 changes: 19 additions & 9 deletions move-mutation-test/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ pub struct CLIOptions {

/// This function creates a mutator CLI options from the given mutation-test options.
#[must_use]
pub fn create_mutator_options(options: &CLIOptions) -> move_mutator::cli::CLIOptions {
pub fn create_mutator_options(
options: &CLIOptions,
apply_coverage: bool,
) -> move_mutator::cli::CLIOptions {
move_mutator::cli::CLIOptions {
mutate_functions: options.mutate_functions.clone(),
mutate_modules: options.mutate_modules.clone(),
configuration_file: options.mutator_conf.clone(),
apply_coverage,
// To run tests, compilation must succeed
verify_mutants: true,
..Default::default()
Expand All @@ -64,7 +68,7 @@ pub fn check_mutator_output_path(options: &move_mutator::cli::CLIOptions) -> Opt

/// The configuration options for running the tests.
// Info: this set struct is based on TestPackage in `aptos-core/crates/aptos/src/move_tool/mod.rs`.
#[derive(Parser, Debug)]
#[derive(Parser, Debug, Clone)]
pub struct TestBuildConfig {
/// Options for compiling a move package dir.
// We might move some options out and have our own option struct here - not all options are
Expand All @@ -83,12 +87,10 @@ pub struct TestBuildConfig {
/// A boolean value to skip warnings.
#[clap(long)]
pub ignore_compile_warnings: bool,
// TODO: There is no sense in enabling coverage - we'll have another option in the future
// 'use-coverage-data' or something like that - that is going to be passed to the mutator
// tool
///// Collect coverage information for later use with the various `aptos move coverage` subcommands
//#[clap(long = "coverage")]
//pub compute_coverage: bool,

/// Compute and then use unit test computed coverage to generate mutants only for covered code.
#[clap(long = "coverage")]
pub apply_coverage: bool,
}

impl TestBuildConfig {
Expand All @@ -106,6 +108,12 @@ impl TestBuildConfig {
experiments: experiments_from_opt_level(&self.move_pkg.optimize),
}
}

pub fn disable_coverage(&self) -> Self {
let mut cfg = self.clone();
cfg.apply_coverage = false;
cfg
}
}

/// Get bytecode version.
Expand Down Expand Up @@ -141,7 +149,9 @@ mod tests {
mutator_conf: Some(PathBuf::from("path/to/mutator/conf")),
..Default::default()
};
let mutator_options = create_mutator_options(&options);

let no_apply_coverage = false;
let mutator_options = create_mutator_options(&options, no_apply_coverage);

assert_eq!(mutator_options.mutate_modules, options.mutate_modules);
assert_eq!(mutator_options.configuration_file, options.mutator_conf);
Expand Down
16 changes: 13 additions & 3 deletions move-mutation-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@ pub fn run_mutation_test(
compiler_config: test_config.compiler_config(),
..Default::default()
};
let outdir_mutant = run_mutator(options, &mutator_config, &package_path, &outdir)?;
let outdir_mutant = run_mutator(
options,
test_config.apply_coverage,
&mutator_config,
&package_path,
&outdir,
)?;
benchmarks.mutator.stop();
outdir_mutant
};
Expand All @@ -117,6 +123,9 @@ pub fn run_mutation_test(

// Run tests on mutants:

// Do not calculate the coverage on mutants.
let test_config = test_config.disable_coverage();

benchmarks.mutation_test.start();
let (mutation_test_benchmarks, mini_reports): (Vec<Benchmark>, Vec<MiniReport>) = report
.get_mutants()
Expand Down Expand Up @@ -165,7 +174,7 @@ pub fn run_mutation_test(
let skip_fetch_deps = true;
// No need to print anything to the screen, due to many threads, it might be messy and slow.
let mut error_writer = std::io::sink();
let result = run_tests(test_config, &outdir, skip_fetch_deps, &mut error_writer);
let result = run_tests(&test_config, &outdir, skip_fetch_deps, &mut error_writer);
benchmark.stop();

let mutant_status = if let Err(e) = result {
Expand Down Expand Up @@ -225,12 +234,13 @@ pub fn run_mutation_test(
/// This function runs the Move Mutator tool.
fn run_mutator(
options: &cli::CLIOptions,
apply_coverage: bool,
config: &BuildConfig,
package_path: &Path,
outdir: &Path,
) -> anyhow::Result<PathBuf> {
debug!("Running the move mutator tool");
let mut mutator_conf = cli::create_mutator_options(options);
let mut mutator_conf = cli::create_mutator_options(options, apply_coverage);

let outdir_mutant = if let Some(path) = cli::check_mutator_output_path(&mutator_conf) {
path
Expand Down
4 changes: 1 addition & 3 deletions move-mutation-test/src/mutation_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ pub(crate) fn run_tests<W: WriteColor + Send>(
let natives = aptos_debug_natives(NativeGasParameters::zeros(), MiscGasParameters::zeros());
let cost_table = None;
let gas_limit = None; // unlimited.
// TODO(M2): Add special handling for the coverage computation.
let compute_coverage = false;

let result = move_cli::base::test::run_move_unit_tests(
package_path,
Expand All @@ -75,7 +73,7 @@ pub(crate) fn run_tests<W: WriteColor + Send>(
aptos_test_feature_flags_genesis(),
gas_limit,
cost_table,
compute_coverage,
cfg.apply_coverage,
&mut error_writer,
)
.map_err(|err| Error::msg(format!("failed to run unit tests: {err:#}")))?;
Expand Down
1 change: 1 addition & 0 deletions move-mutator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ log = { workspace = true }
move-command-line-common = { workspace = true }
move-compiler = { workspace = true }
move-compiler-v2 = { workspace = true }
move-coverage = { workspace = true }
move-model = { workspace = true }
move-package = { workspace = true }
move-symbol-pool = { workspace = true }
Expand Down
5 changes: 5 additions & 0 deletions move-mutator/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ pub struct CLIOptions {
/// Optional configuration file. If provided, it will override the default configuration.
#[clap(long, short, value_parser)]
pub configuration_file: Option<PathBuf>,

/// Use the unit test coverage report to generate mutants for source code with unit test coverage.
#[clap(long = "coverage")]
pub apply_coverage: bool,
}

impl Default for CLIOptions {
Expand All @@ -62,6 +66,7 @@ impl Default for CLIOptions {
out_mutant_dir: Some(PathBuf::from(DEFAULT_OUTPUT_DIR)),
verify_mutants: false,
no_overwrite: false,
apply_coverage: false,
downsample_filter: None,
downsampling_ratio_percentage: None,
configuration_file: None,
Expand Down
22 changes: 14 additions & 8 deletions move-mutator/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use move_compiler::{attr_derivation, shared::Flags};
use move_compiler_v2::run_checker;
use move_model::model::GlobalEnv;
use move_package::{
compilation::compiled_package::make_source_and_deps_for_compiler,
compilation::compiled_package::{make_source_and_deps_for_compiler, CompiledPackage},
resolution::resolution_graph::ResolvedTable,
source_package::{layout::SourcePackageLayout, manifest_parser},
BuildConfig,
Expand Down Expand Up @@ -321,26 +321,32 @@ pub fn verify_mutant(
tempdir.path().join(relative_path)
);

let mut compilation_msg = vec![];

// Create a working config, making sure that the test mode is disabled.
// We want just check if the compilation is successful.
let mut working_config = config.clone();
working_config.test_mode = false;
let _ = compile_package(working_config, tempdir.path())?;

Ok(())
}

pub(crate) fn compile_package(
build_config: BuildConfig,
package_path: &Path,
) -> anyhow::Result<CompiledPackage> {
let mut compilation_msg = vec![];

// Compile the package.
//TODO: It might be better to use the different compiler stage to speed up the whole
// process. For the verification purposes it might be suffcient some earlier stage,
// e.g. type-checking.
working_config.compile_package_no_exit(tempdir.path(), &mut compilation_msg)?;
let (compiled_package, _env) =
build_config.compile_package_no_exit(package_path, &mut compilation_msg)?;

info!(
"Compilation status: {}",
String::from_utf8(compilation_msg)
.unwrap_or("Internal error: can't convert compilation error to UTF8".to_string())
);

Ok(())
Ok(compiled_package)
}

/// Rewrite the manifest file to use absolute paths.
Expand Down
14 changes: 8 additions & 6 deletions move-mutator/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

use crate::cli::CLIOptions;
use crate::{cli::CLIOptions, coverage::Coverage};
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
Expand All @@ -15,9 +15,8 @@ pub enum FileType {
JSON,
TOML,
}

/// Mutator configuration for the Move project.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Configuration {
/// Main project options. It's the same as the CLI options.
pub project: CLIOptions,
Expand All @@ -27,6 +26,9 @@ pub struct Configuration {
pub mutation: Option<MutationConfig>,
/// Configuration for the individual files. (optional).
pub individual: Vec<FileConfiguration>,
#[serde(skip)]
/// Coverage report where the optional unit test coverage data is stored.
pub(crate) coverage: Coverage,
}

impl Configuration {
Expand All @@ -38,6 +40,8 @@ impl Configuration {
project_path,
mutation: None,
individual: vec![],
// Coverage is unset by default.
coverage: Coverage::default(),
}
}

Expand Down Expand Up @@ -326,10 +330,8 @@ mod tests {
mutate_functions: FunctionFilter::All,
};
let config = Configuration {
project: CLIOptions::default(),
project_path: None,
mutation: None,
individual: vec![file_config],
..Default::default()
};

let result = config.get_file_configuration(&PathBuf::from("/unknown/path"));
Expand Down
Loading

0 comments on commit 5eb5217

Please sign in to comment.