diff --git a/CHANGELOG.md b/CHANGELOG.md index da2f7cdad2..32f300b666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Fixed - `account delete` command: It is no longer necessary to provide the `--url` argument each time. Either the `--url` or `--network` argument must be provided, but not both, as they are mutually exclusive. +### Forge + +#### Changed + +- When using test name filter with `--exact` flag, forge will try to compile only the selected test. + ## [0.31.0] - 2024-09-26 ### Cast diff --git a/Cargo.lock b/Cargo.lock index 6c5bb88e20..3e5ff6d54e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4763,6 +4763,7 @@ dependencies = [ "num-bigint", "regex", "serde_json", + "shared", "smol_str", "url", ] diff --git a/crates/forge/src/pretty_printing.rs b/crates/forge/src/pretty_printing.rs index 896e39686c..65b9e7f14f 100644 --- a/crates/forge/src/pretty_printing.rs +++ b/crates/forge/src/pretty_printing.rs @@ -26,21 +26,33 @@ pub(crate) fn print_running_tests(test_target_location: TestTargetLocation, test println!("{}", style(plain_text).bold()); } -pub(crate) fn print_test_summary(summaries: &[TestTargetSummary], filtered: usize) { +// TODO(#2574): Bring back "filtered out" number in tests summary when running with `--exact` flag +pub(crate) fn print_test_summary(summaries: &[TestTargetSummary], filtered: Option) { let passed: usize = summaries.iter().map(TestTargetSummary::count_passed).sum(); let failed: usize = summaries.iter().map(TestTargetSummary::count_failed).sum(); let skipped: usize = summaries.iter().map(TestTargetSummary::count_skipped).sum(); let ignored: usize = summaries.iter().map(TestTargetSummary::count_ignored).sum(); - println!( - "{}: {} passed, {} failed, {} skipped, {} ignored, {} filtered out", - style("Tests").bold(), - passed, - failed, - skipped, - ignored, - filtered, - ); + if let Some(filtered) = filtered { + println!( + "{}: {} passed, {} failed, {} skipped, {} ignored, {} filtered out", + style("Tests").bold(), + passed, + failed, + skipped, + ignored, + filtered + ); + } else { + println!( + "{}: {} passed, {} failed, {} skipped, {} ignored, other filtered out", + style("Tests").bold(), + passed, + failed, + skipped, + ignored + ); + } } pub(crate) fn print_test_seed(seed: u64) { diff --git a/crates/forge/src/run_tests/package.rs b/crates/forge/src/run_tests/package.rs index 389df1035d..c114531783 100644 --- a/crates/forge/src/run_tests/package.rs +++ b/crates/forge/src/run_tests/package.rs @@ -11,7 +11,7 @@ use crate::{ load_test_artifacts, should_compile_starknet_contract_target, }, shared_cache::FailedTestsCache, - test_filter::TestsFilter, + test_filter::{NameFilter, TestsFilter}, warn::{ warn_if_available_gas_used_with_incompatible_scarb_version, warn_if_incompatible_rpc_version, @@ -172,8 +172,13 @@ pub async fn run_for_package( } } - let filtered = all_tests - not_filtered; - pretty_printing::print_test_summary(&summaries, filtered); + // TODO(#2574): Bring back "filtered out" number in tests summary when running with `--exact` flag + if let NameFilter::ExactMatch(_) = tests_filter.name_filter { + pretty_printing::print_test_summary(&summaries, None); + } else { + let filtered = all_tests - not_filtered; + pretty_printing::print_test_summary(&summaries, Some(filtered)); + } let any_fuzz_test_was_run = summaries.iter().any(|test_target_summary| { test_target_summary diff --git a/crates/forge/src/run_tests/workspace.rs b/crates/forge/src/run_tests/workspace.rs index d7ff34eb58..bac7fc7171 100644 --- a/crates/forge/src/run_tests/workspace.rs +++ b/crates/forge/src/run_tests/workspace.rs @@ -16,6 +16,7 @@ use scarb_api::{ target_dir_for_workspace, ScarbCommand, }; use scarb_ui::args::PackagesFilter; +use shared::consts::SNFORGE_TEST_FILTER; use std::env; #[allow(clippy::too_many_lines)] @@ -44,6 +45,15 @@ pub async fn run_for_workspace(args: TestArgs) -> Result { let filter = PackagesFilter::generate_for::(packages.iter()); + if args.exact { + let test_filter = args.test_filter.clone(); + if let Some(last_filter) = + test_filter.and_then(|filter| filter.split("::").last().map(String::from)) + { + set_forge_test_filter(last_filter); + } + } + build_artifacts_with_scarb( filter.clone(), args.features.clone(), @@ -80,6 +90,10 @@ pub async fn run_for_workspace(args: TestArgs) -> Result { pretty_printing::print_latest_blocks_numbers(block_number_map.get_url_to_latest_block_number()); pretty_printing::print_failures(&all_failed_tests); + if args.exact { + unset_forge_test_filter(); + } + Ok(if all_failed_tests.is_empty() { ExitStatus::Success } else { @@ -101,3 +115,11 @@ fn extract_failed_tests( ) }) } + +fn set_forge_test_filter(test_filter: String) { + env::set_var(SNFORGE_TEST_FILTER, test_filter); +} + +fn unset_forge_test_filter() { + env::remove_var(SNFORGE_TEST_FILTER); +} diff --git a/crates/forge/src/test_filter.rs b/crates/forge/src/test_filter.rs index 59b13058bd..25bb478d7f 100644 --- a/crates/forge/src/test_filter.rs +++ b/crates/forge/src/test_filter.rs @@ -7,7 +7,7 @@ use forge_runner::TestCaseFilter; // Specifies what tests should be included pub struct TestsFilter { // based on name - name_filter: NameFilter, + pub(crate) name_filter: NameFilter, // based on `#[ignore]` attribute ignored_filter: IgnoredFilter, // based on rerun_failed flag diff --git a/crates/forge/tests/data/duplicated_test_names/Scarb.toml b/crates/forge/tests/data/duplicated_test_names/Scarb.toml new file mode 100644 index 0000000000..1bc65e01db --- /dev/null +++ b/crates/forge/tests/data/duplicated_test_names/Scarb.toml @@ -0,0 +1,10 @@ +[package] +name = "duplicated_test_names" +version = "0.1.0" +edition = "2023_11" + +[dev-dependencies] +snforge_std = { path = "../../../../../snforge_std" } + +[scripts] +test = "snforge test" diff --git a/crates/forge/tests/data/duplicated_test_names/src/lib.cairo b/crates/forge/tests/data/duplicated_test_names/src/lib.cairo new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/forge/tests/data/duplicated_test_names/src/lib.cairo @@ -0,0 +1 @@ + diff --git a/crates/forge/tests/data/duplicated_test_names/tests/tests_a.cairo b/crates/forge/tests/data/duplicated_test_names/tests/tests_a.cairo new file mode 100644 index 0000000000..771717f38e --- /dev/null +++ b/crates/forge/tests/data/duplicated_test_names/tests/tests_a.cairo @@ -0,0 +1,4 @@ +#[test] +fn test_simple() { + assert(1 == 1, 'simple check'); +} diff --git a/crates/forge/tests/data/duplicated_test_names/tests/tests_b.cairo b/crates/forge/tests/data/duplicated_test_names/tests/tests_b.cairo new file mode 100644 index 0000000000..771717f38e --- /dev/null +++ b/crates/forge/tests/data/duplicated_test_names/tests/tests_b.cairo @@ -0,0 +1,4 @@ +#[test] +fn test_simple() { + assert(1 == 1, 'simple check'); +} diff --git a/crates/forge/tests/e2e/forking.rs b/crates/forge/tests/e2e/forking.rs index 8f719c8ae7..2dc4a26882 100644 --- a/crates/forge/tests/e2e/forking.rs +++ b/crates/forge/tests/e2e/forking.rs @@ -64,7 +64,7 @@ fn with_cache() { Failure data: 0x42616c616e63652073686f756c642062652030 ('Balance should be 0') - Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 4 filtered out + Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, other filtered out Failures: forking::tests::test_fork_simple @@ -96,7 +96,7 @@ fn with_clean_cache() { Collected 1 test(s) from forking package Running 1 test(s) from src/ [PASS] forking::tests::test_fork_simple [..] - Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 4 filtered out + Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, other filtered out "}, ); } @@ -124,7 +124,7 @@ fn printing_latest_block_number() { Collected 1 test(s) from forking package Running 1 test(s) from src/ [PASS] forking::tests::print_block_number_when_latest [..] - Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 4 filtered out + Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, other filtered out Latest block number = [..] for url = {node_rpc_url} "}, diff --git a/crates/forge/tests/e2e/running.rs b/crates/forge/tests/e2e/running.rs index 846bc16c14..7a664e727d 100644 --- a/crates/forge/tests/e2e/running.rs +++ b/crates/forge/tests/e2e/running.rs @@ -226,7 +226,33 @@ fn with_exact_filter() { Running 0 test(s) from src/ Running 1 test(s) from tests/ [PASS] simple_package_integrationtest::test_simple::test_two [..] - Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 12 filtered out + Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, other filtered out + "}, + ); +} + +#[test] +fn with_exact_filter_and_duplicated_test_names() { + let temp = setup_package("duplicated_test_names"); + + let output = test_runner(&temp) + .arg("duplicated_test_names_integrationtest::tests_a::test_simple") + .arg("--exact") + .assert() + .success(); + + assert_stdout_contains( + output, + indoc! {r" + [..]Compiling[..] + [..]Finished[..] + + + Collected 1 test(s) from duplicated_test_names package + Running 0 test(s) from src/ + Running 1 test(s) from tests/ + [PASS] duplicated_test_names_integrationtest::tests_a::test_simple [..] + Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, other filtered out "}, ); } diff --git a/crates/shared/src/consts.rs b/crates/shared/src/consts.rs index 612cefae44..d94812861f 100644 --- a/crates/shared/src/consts.rs +++ b/crates/shared/src/consts.rs @@ -1,2 +1,3 @@ pub const EXPECTED_RPC_VERSION: &str = "0.7.0"; pub const RPC_URL_VERSION: &str = "v0_7"; +pub const SNFORGE_TEST_FILTER: &str = "SNFORGE_TEST_FILTER"; diff --git a/crates/snforge-scarb-plugin/Cargo.toml b/crates/snforge-scarb-plugin/Cargo.toml index 794b10bb71..755a81d14b 100644 --- a/crates/snforge-scarb-plugin/Cargo.toml +++ b/crates/snforge-scarb-plugin/Cargo.toml @@ -21,6 +21,7 @@ indoc.workspace = true serde_json.workspace = true smol_str.workspace = true num-bigint.workspace = true +shared.workspace = true [dev-dependencies] lazy_static = "1.4.0" diff --git a/crates/snforge-scarb-plugin/src/attributes/test.rs b/crates/snforge-scarb-plugin/src/attributes/test.rs index 224db6874a..b8573f3cde 100644 --- a/crates/snforge-scarb-plugin/src/attributes/test.rs +++ b/crates/snforge-scarb-plugin/src/attributes/test.rs @@ -4,9 +4,11 @@ use crate::{ common::{into_proc_macro_result, with_parsed_values}, }; use cairo_lang_macro::{Diagnostic, Diagnostics, ProcMacroResult, TokenStream}; -use cairo_lang_syntax::node::{ast::FunctionWithBody, db::SyntaxGroup, TypedSyntaxNode}; +use cairo_lang_syntax::node::{ast::FunctionWithBody, db::SyntaxGroup, Terminal, TypedSyntaxNode}; use indoc::formatdoc; +use shared::consts::SNFORGE_TEST_FILTER; +use std::env::{self, VarError}; struct TestCollector; impl AttributeInfo for TestCollector { @@ -34,14 +36,33 @@ fn test_internal( let config = InternalConfigStatementCollector::ATTR_NAME; let func_item = func.as_syntax_node().get_text(db); + let name = func.declaration(db).name(db).text(db).to_string(); - let result = formatdoc!( - " + let test_filter = get_forge_test_filter().ok(); + + let should_run_test = match test_filter { + Some(ref filter) => name.contains(filter), + None => true, + }; + + if should_run_test { + Ok(formatdoc!( + " #[snforge_internal_test_executable] #[{config}] {func_item} " - ); + )) + } else { + Ok(formatdoc!( + " + #[{config}] + {func_item} + " + )) + } +} - Ok(result) +fn get_forge_test_filter() -> Result { + env::var(SNFORGE_TEST_FILTER) } diff --git a/docs/src/getting-started/first-steps.md b/docs/src/getting-started/first-steps.md index 8f883cab6f..36036d2a66 100644 --- a/docs/src/getting-started/first-steps.md +++ b/docs/src/getting-started/first-steps.md @@ -40,7 +40,7 @@ Running 0 test(s) from src/ Running 2 test(s) from tests/ [PASS] tests::test_contract::test_increase_balance (gas: ~170) [PASS] tests::test_contract::test_cannot_increase_balance_with_zero_value (gas: ~104) -Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out +Tests: 2 passed, 0 failed, 0 skipped, 0 ignored ``` ## Using `snforge` With Existing Scarb Projects diff --git a/docs/src/testing/running-tests.md b/docs/src/testing/running-tests.md index 44b5a6fb14..e5cb0eaaa2 100644 --- a/docs/src/testing/running-tests.md +++ b/docs/src/testing/running-tests.md @@ -32,12 +32,17 @@ Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 1 filtered out To run a specific test, you can pass a filter string along with an `--exact` flag. Note, you have to use a fully qualified test name, including a module name. +> 📝 **Note** +> +> Running a specific test results in optimized compilation. `snforge` will try to compile only the desired test, unlike the case of running all tests where all of them are compiled. +> + ```shell $ snforge test package_name::tests::calling --exact Collected 1 test(s) from package_name package Running 1 test(s) from src/ [PASS] package_name::tests::calling -Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 2 filtered out +Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, other filtered out ``` ## Stopping Test Execution After First Failed Test diff --git a/docs/src/testing/testing.md b/docs/src/testing/testing.md index 9d712e00f4..17654dc639 100644 --- a/docs/src/testing/testing.md +++ b/docs/src/testing/testing.md @@ -60,20 +60,20 @@ To mark a test as expected to fail, use the `#[should_panic]` attribute. You can specify the expected failure message in three ways: 1. **With ByteArray**: - ```rust +```rust {{#include ../../listings/snforge_overview/crates/writing_tests/tests/expected_failures.cairo:byte_array}} - ``` - With this format, the expected error message needs to be a substring of the actual error message. This is particularly useful when the error message includes dynamic data such as a hash or address. +``` +With this format, the expected error message needs to be a substring of the actual error message. This is particularly useful when the error message includes dynamic data such as a hash or address. 2. **With felt** - ```rust +```rust {{#include ../../listings/snforge_overview/crates/writing_tests/tests/expected_failures.cairo:felt}} - ``` +``` 3. **With tuple of felts**: - ```rust +```rust {{#include ../../listings/snforge_overview/crates/writing_tests/tests/expected_failures.cairo:tuple}} - ``` +``` ```shell