Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement forc-test filter #4566

Merged
merged 9 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ members = [
]
exclude = [
"examples/*",
"swayfmt/test_macros"
"swayfmt/test_macros",
"forc-test/test_data"
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
]

[profile.dev.package.sway-lsp]
Expand Down
216 changes: 198 additions & 18 deletions forc-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ pub struct TestDetails {
pub line_number: usize,
}

/// The filter to be used to only run matching tests.
pub struct TestFilter<'a> {
/// The phrase used for filtering, a `&str` searched/matched with test name.
pub filter_phrase: &'a str,
/// If set `true`, a complete "match" is required with test name for the test to be executed,
/// otherwise a test_name should "contain" the `filter_phrase`.
pub exact_match: bool,
}

/// The result of executing a single test within a single package.
#[derive(Debug)]
pub struct TestResult {
Expand Down Expand Up @@ -384,10 +393,11 @@ impl<'a> PackageTests {
}
}

/// Run all tests for this package and collect their results.
/// Run all tests after applying the provided filter and collect their results.
pub(crate) fn run_tests(
&self,
test_runners: &rayon::ThreadPool,
test_filter: Option<&TestFilter>,
) -> anyhow::Result<TestedPackage> {
let pkg_with_tests = self.built_pkg_with_tests();
let tests = test_runners.install(|| {
Expand All @@ -396,6 +406,14 @@ impl<'a> PackageTests {
.entries
.par_iter()
.filter_map(|entry| entry.kind.test().map(|test| (entry, test)))
.filter(|(entry, _)| {
// If a test filter is specified, only the tests containing the filter phrase in
// their name are going to be executed.
match &test_filter {
Some(filter) => filter.filter(&entry.finalized.fn_name),
None => true,
}
})
.map(|(entry, test_entry)| {
let offset = u32::try_from(entry.finalized.imm)
.expect("test instruction offset out of range");
Expand Down Expand Up @@ -543,34 +561,66 @@ pub enum TestRunnerCount {
Auto,
}

pub struct TestCount {
pub total: usize,
pub filtered: usize,
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
}

impl<'a> TestFilter<'a> {
fn filter(&self, fn_name: &str) -> bool {
if self.exact_match {
fn_name == self.filter_phrase
} else {
fn_name.contains(self.filter_phrase)
}
}
}

impl BuiltTests {
/// The total number of tests.
pub fn test_count(&self) -> usize {
pub fn test_count(&self, test_filter: Option<&TestFilter>) -> TestCount {
let pkgs: Vec<&PackageTests> = match self {
BuiltTests::Package(pkg) => vec![pkg],
BuiltTests::Workspace(workspace) => workspace.iter().collect(),
};
pkgs.iter()
.map(|pkg| {
pkg.built_pkg_with_tests()
.bytecode
.entries
.iter()
.filter_map(|entry| entry.kind.test().map(|test| (entry, test)))
.count()
})
.sum()
let mut num_total = 0;
let mut num_ignored = 0;
for (pkg_entry, _) in pkgs.iter().flat_map(|pkg| {
pkg.built_pkg_with_tests()
.bytecode
.entries
.iter()
.filter_map(|entry| entry.kind.test().map(|test| (entry, test)))
}) {
let ignored = match &test_filter {
Some(filter) => !filter.filter(&pkg_entry.finalized.fn_name),
None => false,
};
if ignored {
num_ignored += 1;
}
num_total += 1;
}

TestCount {
total: num_total,
filtered: num_ignored,
}
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
}

/// Run all built tests, return the result.
pub fn run(self, test_runner_count: TestRunnerCount) -> anyhow::Result<Tested> {
pub fn run(
self,
test_runner_count: TestRunnerCount,
test_filter: Option<TestFilter>,
) -> anyhow::Result<Tested> {
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)
run_tests(self, &test_runners, test_filter)
}
}

Expand Down Expand Up @@ -641,17 +691,23 @@ fn deployment_transaction(
(contract_id, tx)
}

/// Build the given package and run its tests, returning the results.
fn run_tests(built: BuiltTests, test_runners: &rayon::ThreadPool) -> anyhow::Result<Tested> {
/// Build the given package and run its tests after applying the filter provided.
///
/// Returns the result of test execution.
fn run_tests(
built: BuiltTests,
test_runners: &rayon::ThreadPool,
test_filter: Option<TestFilter>,
) -> anyhow::Result<Tested> {
match built {
BuiltTests::Package(pkg) => {
let tested_pkg = pkg.run_tests(test_runners)?;
let tested_pkg = pkg.run_tests(test_runners, test_filter.as_ref())?;
Ok(Tested::Package(Box::new(tested_pkg)))
}
BuiltTests::Workspace(workspace) => {
let tested_pkgs = workspace
.into_iter()
.map(|pkg| pkg.run_tests(test_runners))
.map(|pkg| pkg.run_tests(test_runners, test_filter.as_ref()))
.collect::<anyhow::Result<Vec<TestedPackage>>>()?;
Ok(Tested::Workspace(tested_pkgs))
}
Expand Down Expand Up @@ -760,3 +816,127 @@ fn exec_test(

(state, duration, receipts)
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;

use crate::{build, BuiltTests, Opts, TestFilter, TestResult};

/// Name of the folder containing required data for tests to run, such as an example forc
/// project.
const TEST_DATA_FOLDER_NAME: &str = "test_data";
/// Name of the library package in the "CARGO_MANIFEST_DIR/TEST_DATA_FOLDER_NAME".
const TEST_LIBRARY_PACKAGE_NAME: &str = "test_library";

/// Build the tests in the test library located at
/// "CARGO_MANIFEST_DIR/TEST_DATA_FOLDER_NAME/TEST_LIBRARY_PACKAGE_NAME".
fn test_library_built_tests() -> anyhow::Result<BuiltTests> {
let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR");
let library_package_dir = PathBuf::from(cargo_manifest_dir)
.join(TEST_DATA_FOLDER_NAME)
.join(TEST_LIBRARY_PACKAGE_NAME);
let library_package_dir_string = library_package_dir.to_string_lossy().to_string();
let build_options = Opts {
pkg: forc_pkg::PkgOpts {
path: Some(library_package_dir_string),
..Default::default()
},
..Default::default()
};
build(build_options)
}

fn test_library_test_results(
test_filter: Option<TestFilter>,
) -> anyhow::Result<Vec<TestResult>> {
let built_tests = test_library_built_tests()?;
let test_runner_count = crate::TestRunnerCount::Auto;
let tested = built_tests.run(test_runner_count, test_filter)?;
match tested {
crate::Tested::Package(tested_pkg) => Ok(tested_pkg.tests),
crate::Tested::Workspace(_) => {
unreachable!("test_library is a package, not a workspace.")
}
}
}

#[test]
fn test_filter_exact_match() {
let filter_phrase = "test_bam";
let test_filter = TestFilter {
filter_phrase,
exact_match: true,
};

let test_results = test_library_test_results(Some(test_filter)).unwrap();
let tested_package_test_count = test_results.len();

assert_eq!(tested_package_test_count, 1)
}

#[test]
fn test_filter_exact_match_all_ignored() {
let filter_phrase = "test_ba";
let test_filter = TestFilter {
filter_phrase,
exact_match: true,
};

let test_results = test_library_test_results(Some(test_filter)).unwrap();
let tested_package_test_count = test_results.len();

assert_eq!(tested_package_test_count, 0)
}

#[test]
fn test_filter_match_all_ignored() {
let filter_phrase = "this_test_does_not_exists";
let test_filter = TestFilter {
filter_phrase,
exact_match: false,
};

let test_results = test_library_test_results(Some(test_filter)).unwrap();
let tested_package_test_count = test_results.len();

assert_eq!(tested_package_test_count, 0)
}

#[test]
fn test_filter_one_match() {
let filter_phrase = "test_ba";
let test_filter = TestFilter {
filter_phrase,
exact_match: false,
};

let test_results = test_library_test_results(Some(test_filter)).unwrap();
let tested_package_test_count = test_results.len();

assert_eq!(tested_package_test_count, 1)
}

#[test]
fn test_filter_all_match() {
let filter_phrase = "test_b";
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
let test_filter = TestFilter {
filter_phrase,
exact_match: false,
};

let test_results = test_library_test_results(Some(test_filter)).unwrap();
let tested_package_test_count = test_results.len();

assert_eq!(tested_package_test_count, 2)
}

#[test]
fn test_no_filter() {
let test_filter = None;
let test_results = test_library_test_results(test_filter).unwrap();
let tested_package_test_count = test_results.len();

assert_eq!(tested_package_test_count, 2)
}
}
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions forc-test/test_data/test_library/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
13 changes: 13 additions & 0 deletions forc-test/test_data/test_library/Forc.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[[package]]
name = 'core'
source = 'path+from-root-B2871353A775FFA4'

[[package]]
name = 'std'
source = 'path+from-root-B2871353A775FFA4'
dependencies = ['core']

[[package]]
name = 'test_library'
source = 'member'
dependencies = ['std']
8 changes: 8 additions & 0 deletions forc-test/test_data/test_library/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "lib.sw"
license = "Apache-2.0"
name = "test_library"

[dependencies]
std = { path = "../../../sway-lib-std/" }
11 changes: 11 additions & 0 deletions forc-test/test_data/test_library/src/lib.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
library;

#[test]
fn test_bam() {
assert(1 == 1)
}

#[test]
fn test_bum() {
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
assert(1 == 1)
}
25 changes: 17 additions & 8 deletions forc/src/cli/commands/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use crate::cli;
use ansi_term::Colour;
use clap::Parser;
use forc_pkg as pkg;
use forc_test::{TestRunnerCount, TestedPackage};
use forc_util::{forc_result_bail, format_log_receipts, ForcError, ForcResult};
use forc_test::{TestFilter, TestRunnerCount, TestedPackage};
use forc_util::{format_log_receipts, ForcError, ForcResult};
use tracing::info;

/// Run the Sway unit tests for the current project.
Expand Down Expand Up @@ -32,6 +32,9 @@ pub struct Command {
/// When specified, only tests containing the given string will be executed.
pub filter: Option<String>,
#[clap(long)]
/// When specified, only the test exactly matching the given string will be executed.
pub exact: bool,
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
#[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<usize>,
Expand All @@ -49,21 +52,27 @@ pub struct TestPrintOpts {
}

pub(crate) fn exec(cmd: Command) -> ForcResult<()> {
if let Some(ref _filter) = cmd.filter {
forc_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 test_filter_phrase = cmd.filter.clone();
let test_filter = test_filter_phrase.as_ref().map(|filter_phrase| TestFilter {
filter_phrase,
exact_match: cmd.exact,
});
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(test_runner_count)?;
let test_count = built_tests.test_count(test_filter.as_ref());
info!(
" Running {} tests, filtered {} tests",
test_count.total - test_count.filtered,
test_count.filtered
);
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
let tested = built_tests.run(test_runner_count, test_filter)?;
let duration = start.elapsed();

// Eventually we'll print this in a fancy manner, but this will do for testing.
Expand Down
3 changes: 2 additions & 1 deletion test/src/e2e_vm_tests/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ pub(crate) async fn compile_and_run_unit_tests(
},
..Default::default()
})?;
let tested = built_tests.run(forc_test::TestRunnerCount::Auto)?;
let test_filter = None;
let tested = built_tests.run(forc_test::TestRunnerCount::Auto, test_filter)?;

match tested {
forc_test::Tested::Package(tested_pkg) => Ok(vec![*tested_pkg]),
Expand Down