diff --git a/Cargo.lock b/Cargo.lock index 788f7cf8d38..fffe32029e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -974,6 +974,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "crossbeam-utils", + "memoffset 0.8.0", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.15" @@ -1844,6 +1868,7 @@ dependencies = [ "fuel-tx", "fuel-vm", "rand", + "rayon", "sway-core", "sway-types", ] @@ -3296,6 +3321,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + [[package]] name = "miden" version = "0.3.0" @@ -4215,6 +4249,28 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -4437,7 +4493,7 @@ checksum = "4f77412a3d1f26af0c0783c23b3555a301b1a49805cba7bf9a7827a9e9e285f0" dependencies = [ "countme", "hashbrown 0.11.2", - "memoffset", + "memoffset 0.6.5", "rustc-hash", "text-size", ] diff --git a/docs/book/src/testing/unit-testing.md b/docs/book/src/testing/unit-testing.md index b91ba8e06f5..913b845c8e0 100644 --- a/docs/book/src/testing/unit-testing.md +++ b/docs/book/src/testing/unit-testing.md @@ -104,3 +104,13 @@ Example `Forc.toml` for contract above: ```toml {{#include ../../../../examples/multi_contract_calls/caller/Forc.toml:multi_contract_call_toml}} ``` + +## Running Tests in Parallel or Serially + +By default, all unit tests in your project are run in parallel. Note that this does not lead to any data races in storage because each unit test has its own storage space that is not shared by any other unit test. + +By default, `forc test` will use all the available threads in your system. To request that a specific number of threads be used, the flag `--test-threads ` can be provided to `forc test`. + +```console +forc test --test-threads 1 +``` diff --git a/forc-test/Cargo.toml b/forc-test/Cargo.toml index e166146ba95..c2402483e39 100644 --- a/forc-test/Cargo.toml +++ b/forc-test/Cargo.toml @@ -15,5 +15,6 @@ fuel-abi-types = "0.2" fuel-tx = { workspace = true, features = ["builder"] } fuel-vm = { workspace = true, features = ["random"] } rand = "0.8" +rayon = "1.7.0" sway-core = { version = "0.36.1", path = "../sway-core" } -sway-types = { version = "0.36.1", path = "../sway-types" } +sway-types = { version = "0.36.1", path = "../sway-types" } \ No newline at end of file diff --git a/forc-test/src/lib.rs b/forc-test/src/lib.rs index a990aae78fa..8c34400343f 100644 --- a/forc-test/src/lib.rs +++ b/forc-test/src/lib.rs @@ -7,6 +7,7 @@ use fuel_vm::{self as vm, fuel_asm, prelude::Instruction}; use pkg::TestPassCondition; use pkg::{Built, BuiltPackage}; use rand::{Rng, SeedableRng}; +use rayon::prelude::*; use std::{collections::HashMap, fs, path::PathBuf, sync::Arc}; use sway_core::BuildTarget; use sway_types::Span; @@ -272,54 +273,59 @@ impl<'a> PackageTests { } /// Run all tests for this package and collect their results. - pub(crate) fn run_tests(&self) -> anyhow::Result { + pub(crate) fn run_tests( + &self, + test_runners: &rayon::ThreadPool, + ) -> anyhow::Result { let pkg_with_tests = self.built_pkg_with_tests(); - // TODO: We can easily parallelise this, but let's wait until testing is stable first. - let tests = pkg_with_tests - .bytecode - .entries - .iter() - .filter_map(|entry| entry.kind.test().map(|test| (entry, test))) - .map(|(entry, test_entry)| { - let offset = u32::try_from(entry.finalized.imm) - .expect("test instruction offset out of range"); - let name = entry.finalized.fn_name.clone(); - let test_setup = self.setup()?; - let (state, duration, receipts) = - exec_test(&pkg_with_tests.bytecode.bytes, offset, test_setup); - - let gas_used = *receipts - .iter() - .find_map(|receipt| match receipt { - tx::Receipt::ScriptResult { gas_used, .. } => Some(gas_used), - _ => None, - }) - .ok_or_else(|| { - anyhow::anyhow!("missing used gas information from test execution") - })?; - - // Only retain `Log` and `LogData` receipts. - let logs = receipts - .into_iter() - .filter(|receipt| { - matches!(receipt, fuel_tx::Receipt::Log { .. }) - || matches!(receipt, fuel_tx::Receipt::LogData { .. }) + let tests = test_runners.install(|| { + pkg_with_tests + .bytecode + .entries + .par_iter() + .filter_map(|entry| entry.kind.test().map(|test| (entry, test))) + .map(|(entry, test_entry)| { + let offset = u32::try_from(entry.finalized.imm) + .expect("test instruction offset out of range"); + let name = entry.finalized.fn_name.clone(); + let test_setup = self.setup()?; + let (state, duration, receipts) = + exec_test(&pkg_with_tests.bytecode.bytes, offset, test_setup); + + let gas_used = *receipts + .iter() + .find_map(|receipt| match receipt { + tx::Receipt::ScriptResult { gas_used, .. } => Some(gas_used), + _ => None, + }) + .ok_or_else(|| { + anyhow::anyhow!("missing used gas information from test execution") + })?; + + // Only retain `Log` and `LogData` receipts. + let logs = receipts + .into_iter() + .filter(|receipt| { + matches!(receipt, fuel_tx::Receipt::Log { .. }) + || matches!(receipt, fuel_tx::Receipt::LogData { .. }) + }) + .collect(); + + let span = test_entry.span.clone(); + let condition = test_entry.pass_condition.clone(); + Ok(TestResult { + name, + duration, + span, + state, + condition, + logs, + gas_used, }) - .collect(); - - let span = test_entry.span.clone(); - let condition = test_entry.pass_condition.clone(); - Ok(TestResult { - name, - duration, - span, - state, - condition, - logs, - gas_used, }) - }) - .collect::>()?; + .collect::>() + })?; + let tested_pkg = TestedPackage { built: Box::new(pkg_with_tests.clone()), tests, @@ -413,6 +419,13 @@ impl TestResult { } } +/// Used to control test runner count for forc-test. Number of runners to use can be specified using +/// `Manual` or can be left forc-test to decide by using `Auto`. +pub enum TestRunnerCount { + Manual(usize), + Auto, +} + impl BuiltTests { /// The total number of tests. pub fn test_count(&self) -> usize { @@ -433,8 +446,14 @@ impl BuiltTests { } /// Run all built tests, return the result. - pub fn run(self) -> anyhow::Result { - run_tests(self) + pub fn run(self, test_runner_count: TestRunnerCount) -> anyhow::Result { + let test_runners = match test_runner_count { + TestRunnerCount::Manual(runner_count) => rayon::ThreadPoolBuilder::new() + .num_threads(runner_count) + .build(), + TestRunnerCount::Auto => rayon::ThreadPoolBuilder::new().build(), + }?; + run_tests(self, &test_runners) } } @@ -506,16 +525,16 @@ fn deployment_transaction( } /// Build the given package and run its tests, returning the results. -fn run_tests(built: BuiltTests) -> anyhow::Result { +fn run_tests(built: BuiltTests, test_runners: &rayon::ThreadPool) -> anyhow::Result { match built { BuiltTests::Package(pkg) => { - let tested_pkg = pkg.run_tests()?; + let tested_pkg = pkg.run_tests(test_runners)?; Ok(Tested::Package(Box::new(tested_pkg))) } BuiltTests::Workspace(workspace) => { let tested_pkgs = workspace .into_iter() - .map(|pkg| pkg.run_tests()) + .map(|pkg| pkg.run_tests(test_runners)) .collect::>>()?; Ok(Tested::Workspace(tested_pkgs)) } diff --git a/forc/src/cli/commands/test.rs b/forc/src/cli/commands/test.rs index 61782e5ec14..76f6b7ae198 100644 --- a/forc/src/cli/commands/test.rs +++ b/forc/src/cli/commands/test.rs @@ -3,7 +3,7 @@ use ansi_term::Colour; use anyhow::{bail, Result}; use clap::Parser; use forc_pkg as pkg; -use forc_test::TestedPackage; +use forc_test::{TestRunnerCount, TestedPackage}; use forc_util::format_log_receipts; use tracing::info; @@ -32,6 +32,10 @@ pub struct Command { pub test_print: TestPrintOpts, /// When specified, only tests containing the given string will be executed. pub filter: Option, + #[clap(long)] + /// Number of threads to utilize when running the tests. By default, this is the number of + /// threads available in your system. + pub test_threads: Option, } /// The set of options provided for controlling output of a test. @@ -50,12 +54,17 @@ pub(crate) fn exec(cmd: Command) -> Result<()> { bail!("unit test filter not yet supported"); } + let test_runner_count = match cmd.test_threads { + Some(runner_count) => TestRunnerCount::Manual(runner_count), + None => TestRunnerCount::Auto, + }; + let test_print_opts = cmd.test_print.clone(); let opts = opts_from_cmd(cmd); let built_tests = forc_test::build(opts)?; let start = std::time::Instant::now(); info!(" Running {} tests", built_tests.test_count()); - let tested = built_tests.run()?; + let tested = built_tests.run(test_runner_count)?; let duration = start.elapsed(); // Eventually we'll print this in a fancy manner, but this will do for testing. diff --git a/test/src/e2e_vm_tests/harness.rs b/test/src/e2e_vm_tests/harness.rs index 657732db522..7f5ef2f45bf 100644 --- a/test/src/e2e_vm_tests/harness.rs +++ b/test/src/e2e_vm_tests/harness.rs @@ -268,7 +268,7 @@ pub(crate) async fn compile_and_run_unit_tests( }, ..Default::default() })?; - let tested = built_tests.run()?; + let tested = built_tests.run(forc_test::TestRunnerCount::Auto)?; match tested { forc_test::Tested::Package(tested_pkg) => Ok(vec![*tested_pkg]),