diff --git a/crates/forge-runner/src/messages.rs b/crates/forge-runner/src/messages.rs index 37eeced090..0f6f9e70c9 100644 --- a/crates/forge-runner/src/messages.rs +++ b/crates/forge-runner/src/messages.rs @@ -13,6 +13,7 @@ enum TestResultStatus { Failed, Ignored, Interrupted, + ExcludedFromPartition, } impl From<&AnyTestCaseSummary> for TestResultStatus { @@ -26,6 +27,10 @@ impl From<&AnyTestCaseSummary> for TestResultStatus { | AnyTestCaseSummary::Fuzzing(TestCaseSummary::Ignored { .. }) => Self::Ignored, AnyTestCaseSummary::Single(TestCaseSummary::Interrupted { .. }) | AnyTestCaseSummary::Fuzzing(TestCaseSummary::Interrupted { .. }) => Self::Interrupted, + AnyTestCaseSummary::Single(TestCaseSummary::ExcludedFromPartition { .. }) + | AnyTestCaseSummary::Fuzzing(TestCaseSummary::ExcludedFromPartition { .. }) => { + Self::ExcludedFromPartition + } } } } @@ -110,7 +115,9 @@ impl TestResultMessage { match self.status { TestResultStatus::Passed => return format!("\n\n{msg}"), TestResultStatus::Failed => return format!("\n\nFailure data:{msg}"), - TestResultStatus::Ignored | TestResultStatus::Interrupted => return String::new(), + TestResultStatus::Ignored + | TestResultStatus::Interrupted + | TestResultStatus::ExcludedFromPartition => return String::new(), } } String::new() @@ -124,6 +131,11 @@ impl TestResultMessage { TestResultStatus::Interrupted => { unreachable!("Interrupted tests should not have visible message representation") } + TestResultStatus::ExcludedFromPartition => { + unreachable!( + "Tests excluded from partition should not have visible message representation" + ) + } } } } diff --git a/crates/forge-runner/src/package_tests/with_config_resolved.rs b/crates/forge-runner/src/package_tests/with_config_resolved.rs index 11597441ff..3e850c84d5 100644 --- a/crates/forge-runner/src/package_tests/with_config_resolved.rs +++ b/crates/forge-runner/src/package_tests/with_config_resolved.rs @@ -13,7 +13,8 @@ pub type TestTargetWithResolvedConfig = TestTarget; pub type TestCaseWithResolvedConfig = TestCase; -fn sanitize_test_case_name(name: &str) -> String { +#[must_use] +pub fn sanitize_test_case_name(name: &str) -> String { // Test names generated by `#[test]` and `#[fuzzer]` macros contain internal suffixes name.replace("__snforge_internal_test_generated", "") .replace("__snforge_internal_fuzzer_generated", "") diff --git a/crates/forge-runner/src/test_case_summary.rs b/crates/forge-runner/src/test_case_summary.rs index 2a8c7111f4..ae7352089c 100644 --- a/crates/forge-runner/src/test_case_summary.rs +++ b/crates/forge-runner/src/test_case_summary.rs @@ -163,6 +163,8 @@ pub enum TestCaseSummary { /// Name of the test case name: String, }, + /// Test case excluded from current partition + ExcludedFromPartition {}, /// Test case skipped due to exit first or execution interrupted, test result is ignored. Interrupted {}, } @@ -182,7 +184,9 @@ impl TestCaseSummary { TestCaseSummary::Failed { name, .. } | TestCaseSummary::Passed { name, .. } | TestCaseSummary::Ignored { name, .. } => Some(name), - TestCaseSummary::Interrupted { .. } => None, + TestCaseSummary::Interrupted { .. } | TestCaseSummary::ExcludedFromPartition { .. } => { + None + } } } @@ -264,6 +268,7 @@ impl TestCaseSummary { }, TestCaseSummary::Ignored { name } => TestCaseSummary::Ignored { name: name.clone() }, TestCaseSummary::Interrupted {} => TestCaseSummary::Interrupted {}, + TestCaseSummary::ExcludedFromPartition {} => TestCaseSummary::ExcludedFromPartition {}, } } } @@ -491,6 +496,15 @@ impl AnyTestCaseSummary { | AnyTestCaseSummary::Fuzzing(TestCaseSummary::Ignored { .. }) ) } + + #[must_use] + pub fn is_excluded_from_partition(&self) -> bool { + matches!( + self, + AnyTestCaseSummary::Single(TestCaseSummary::ExcludedFromPartition { .. }) + | AnyTestCaseSummary::Fuzzing(TestCaseSummary::ExcludedFromPartition { .. }) + ) + } } #[cfg(test)] diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 1a8ffffaca..3d93455778 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -1,4 +1,5 @@ use crate::compatibility_check::{Requirement, RequirementsChecker, create_version_parser}; +use crate::partition::Partition; use anyhow::Result; use camino::Utf8PathBuf; use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; @@ -25,6 +26,7 @@ mod clean; mod combine_configs; mod compatibility_check; mod new; +mod partition; mod profile_validation; pub mod run_tests; pub mod scarb; @@ -83,7 +85,7 @@ enum ForgeSubcommand { /// Run tests for a project in the current directory Test { #[command(flatten)] - args: TestArgs, + args: Box, }, /// Create a new Forge project at New { @@ -142,7 +144,7 @@ pub struct TestArgs { trace_args: TraceArgs, /// Use exact matches for `test_filter` - #[arg(short, long)] + #[arg(short, long, conflicts_with = "partition")] exact: bool, /// Skips any tests whose name contains the given SKIP string. @@ -203,6 +205,10 @@ pub struct TestArgs { #[arg(long, value_enum, default_value_t)] tracked_resource: ForgeTrackedResource, + /// If specified, divides tests into `total` partitions and runs only the partition with the given `index` (1-based). + #[arg(long, conflicts_with = "exact")] + partition: Option, + /// Additional arguments for cairo-coverage or cairo-profiler #[arg(last = true)] additional_args: Vec, @@ -297,7 +303,7 @@ pub fn main_execution(ui: Arc) -> Result { .enable_all() .build()?; - rt.block_on(run_for_workspace(args, ui)) + rt.block_on(run_for_workspace(*args, ui)) } ForgeSubcommand::CheckRequirements => { check_requirements(true, &ui)?; diff --git a/crates/forge/src/partition.rs b/crates/forge/src/partition.rs new file mode 100644 index 0000000000..d17d80b1b3 --- /dev/null +++ b/crates/forge/src/partition.rs @@ -0,0 +1,162 @@ +use crate::run_tests::package::RunForPackageArgs; +use cairo_lang_sierra::ids::FunctionId; +use forge_runner::package_tests::with_config_resolved::sanitize_test_case_name; +use serde::Serialize; +use std::{collections::HashMap, str::FromStr}; + +#[derive(Debug, Clone, Copy, Serialize)] +pub struct Partition { + index: usize, + total: usize, +} + +impl Partition { + #[must_use] + pub fn index_0_based(&self) -> usize { + self.index - 1 + } + + #[must_use] + pub fn index_1_based(&self) -> usize { + self.index + } + + #[must_use] + pub fn total(&self) -> usize { + self.total + } +} + +impl FromStr for Partition { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + let parts: Vec<&str> = s.split('/').collect(); + if parts.len() != 2 { + return Err("Partition must be in the format /".to_string()); + } + + let index = parts[0] + .parse::() + .map_err(|_| "INDEX must be a positive integer".to_string())?; + let total = parts[1] + .parse::() + .map_err(|_| "TOTAL must be a positive integer".to_string())?; + + if index == 0 || total == 0 || index > total { + return Err("Invalid partition values: ensure 1 <= INDEX <= TOTAL".to_string()); + } + + Ok(Partition { index, total }) + } +} + +#[derive(Serialize)] +pub struct TestsPartitionsMapping(HashMap); + +impl TestsPartitionsMapping { + pub fn get(&self, test_name: &str) -> Option<&usize> { + self.0.get(test_name) + } + + pub fn insert(&mut self, test_name: String, partition_index: usize) { + self.0.insert(test_name, partition_index); + } + + pub fn from_packages_args(packages_args: &[RunForPackageArgs], partition: Partition) -> Self { + let mut full_paths: Vec = packages_args + .iter() + .flat_map(|pkg| pkg.test_targets.iter()) + .flat_map(|tt| { + tt.sierra_program + .debug_info + .as_ref() + .and_then(|info| info.executables.get("snforge_internal_test_executable")) + .into_iter() + .flatten() + }) + .filter_map(|fid: &FunctionId| { + fid.debug_name + .as_ref() + .map(std::string::ToString::to_string) + }) + .collect(); + + full_paths.sort(); + + let total = partition.total(); + let mut mapping = HashMap::with_capacity(full_paths.len()); + + for (i, path) in full_paths.into_iter().enumerate() { + let partition_index_1_based = (i % total) + 1; + mapping.insert(sanitize_test_case_name(&path), partition_index_1_based); + } + + Self(mapping) + } +} + +#[derive(Serialize)] +pub struct PartitionConfig { + partition: Partition, + partitions_mapping: TestsPartitionsMapping, +} + +impl PartitionConfig { + pub fn new(partition: Partition, packages_args: &[RunForPackageArgs]) -> Self { + let partitions_mapping = + TestsPartitionsMapping::from_packages_args(packages_args, partition); + Self { + partition, + partitions_mapping, + } + } + + pub fn partition(&self) -> Partition { + self.partition + } + + pub fn partitions_mapping(&self) -> &TestsPartitionsMapping { + &self.partitions_mapping + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_happy_case() { + let partition = "2/5".parse::().unwrap(); + assert_eq!(partition.index_1_based(), 2); + assert_eq!(partition.index_0_based(), 1); + assert_eq!(partition.total(), 5); + } + + #[test] + fn test_invalid_format() { + let err = "2-5".parse::().unwrap_err(); + assert_eq!(err, "Partition must be in the format /"); + } + + #[test] + fn test_non_integer() { + let err = "a/5".parse::().unwrap_err(); + assert_eq!(err, "INDEX must be a positive integer"); + + let err = "2/b".parse::().unwrap_err(); + assert_eq!(err, "TOTAL must be a positive integer"); + } + + #[test] + fn test_out_of_bounds() { + let err = "0/5".parse::().unwrap_err(); + assert_eq!(err, "Invalid partition values: ensure 1 <= INDEX <= TOTAL"); + + let err = "6/5".parse::().unwrap_err(); + assert_eq!(err, "Invalid partition values: ensure 1 <= INDEX <= TOTAL"); + + let err = "2/0".parse::().unwrap_err(); + assert_eq!(err, "Invalid partition values: ensure 1 <= INDEX <= TOTAL"); + } +} diff --git a/crates/forge/src/run_tests/messages/mod.rs b/crates/forge/src/run_tests/messages/mod.rs index 9d28624e83..8919a4631f 100644 --- a/crates/forge/src/run_tests/messages/mod.rs +++ b/crates/forge/src/run_tests/messages/mod.rs @@ -1,6 +1,7 @@ pub mod collected_tests_count; pub mod latest_blocks_numbers; pub mod overall_summary; +pub mod partition; pub mod tests_failure_summary; pub mod tests_run; pub mod tests_summary; diff --git a/crates/forge/src/run_tests/messages/partition.rs b/crates/forge/src/run_tests/messages/partition.rs new file mode 100644 index 0000000000..227280a6d3 --- /dev/null +++ b/crates/forge/src/run_tests/messages/partition.rs @@ -0,0 +1,33 @@ +use foundry_ui::Message; +use serde::Serialize; +use serde_json::{Value, json}; + +use crate::partition::Partition; + +#[derive(Serialize)] +pub struct PartitionMessage { + partition: Partition, +} + +impl PartitionMessage { + #[must_use] + pub fn new(partition: Partition) -> Self { + Self { partition } + } +} + +impl Message for PartitionMessage { + fn text(&self) -> String { + format!( + "Finished partition run: {}/{}", + self.partition.index_1_based(), + self.partition.total() + ) + } + + fn json(&self) -> Value { + json!({ + "partition": format!("{}/{}", self.partition.index_1_based(), self.partition.total()) + }) + } +} diff --git a/crates/forge/src/run_tests/package.rs b/crates/forge/src/run_tests/package.rs index 519d5290dd..dda374995f 100644 --- a/crates/forge/src/run_tests/package.rs +++ b/crates/forge/src/run_tests/package.rs @@ -6,6 +6,7 @@ use crate::{ TestArgs, block_number_map::BlockNumberMap, combine_configs::combine_configs, + partition::PartitionConfig, run_tests::messages::{ collected_tests_count::CollectedTestsCountMessage, tests_run::TestsRunMessage, tests_summary::TestsSummaryMessage, @@ -28,7 +29,11 @@ use configuration::load_package_config; use console::Style; use forge_runner::{ forge_config::ForgeConfig, - package_tests::{raw::TestTargetRaw, with_config_resolved::TestTargetWithResolvedConfig}, + package_tests::{ + TestCase, TestTarget, + raw::TestTargetRaw, + with_config_resolved::{TestCaseResolvedConfig, TestTargetWithResolvedConfig}, + }, running::with_config::test_target_with_config, test_case_summary::AnyTestCaseSummary, test_target_summary::TestTargetSummary, @@ -158,8 +163,34 @@ async fn test_package_with_config_resolved( Ok(test_targets_with_resolved_config) } -fn sum_test_cases(test_targets: &[TestTargetWithResolvedConfig]) -> usize { - test_targets.iter().map(|tc| tc.test_cases.len()).sum() +fn sum_test_cases_from_package( + test_targets: &[TestTarget], + partition_config: Option<&PartitionConfig>, +) -> usize { + test_targets + .iter() + .map(|tt| sum_test_cases_from_test_target(tt.test_cases.clone(), partition_config)) + .sum() +} + +fn sum_test_cases_from_test_target( + test_cases: Vec>, + partition_config: Option<&PartitionConfig>, +) -> usize { + if let Some(partition_config) = partition_config { + test_cases + .into_iter() + .filter(|test_case| { + partition_config.partition().index_1_based() + == *partition_config + .partitions_mapping() + .get(&test_case.name) + .expect("Test case name not found in partitions mapping") + }) + .count() + } else { + test_cases.len() + } } #[tracing::instrument(skip_all, level = "debug")] @@ -172,6 +203,7 @@ pub async fn run_for_package( package_name, }: RunForPackageArgs, block_number_map: &mut BlockNumberMap, + partition_config: Option<&PartitionConfig>, ui: Arc, ) -> Result { let mut test_targets = test_package_with_config_resolved( @@ -182,7 +214,7 @@ pub async fn run_for_package( &tests_filter, ) .await?; - let all_tests = sum_test_cases(&test_targets); + let all_tests = sum_test_cases_from_package(&test_targets, partition_config); for test_target in &mut test_targets { tests_filter.filter_tests(&mut test_target.test_cases)?; @@ -191,7 +223,8 @@ pub async fn run_for_package( warn_if_available_gas_used_with_incompatible_scarb_version(&test_targets, &ui)?; warn_if_incompatible_rpc_version(&test_targets, ui.clone()).await?; - let not_filtered = sum_test_cases(&test_targets); + let not_filtered = sum_test_cases_from_package(&test_targets, partition_config); + ui.println(&CollectedTestsCountMessage { tests_num: not_filtered, package_name: package_name.clone(), @@ -203,11 +236,17 @@ pub async fn run_for_package( let ui = ui.clone(); ui.println(&TestsRunMessage::new( test_target.tests_location, - test_target.test_cases.len(), + sum_test_cases_from_test_target(test_target.test_cases.clone(), partition_config), )); - let summary = - run_for_test_target(test_target, forge_config.clone(), &tests_filter, ui).await?; + let summary = run_for_test_target( + test_target, + forge_config.clone(), + &tests_filter, + partition_config, + ui, + ) + .await?; match summary { TestTargetRunResult::Ok(summary) => { diff --git a/crates/forge/src/run_tests/test_target.rs b/crates/forge/src/run_tests/test_target.rs index e03e6d874e..8e052ce57b 100644 --- a/crates/forge/src/run_tests/test_target.rs +++ b/crates/forge/src/run_tests/test_target.rs @@ -1,3 +1,4 @@ +use crate::partition::PartitionConfig; use anyhow::Result; use forge_runner::messages::TestResultMessage; use forge_runner::{ @@ -25,6 +26,7 @@ pub async fn run_for_test_target( tests: TestTargetWithResolvedConfig, forge_config: Arc, tests_filter: &impl TestCaseFilter, + partition_config: Option<&PartitionConfig>, ui: Arc, ) -> Result { let casm_program = tests.casm_program.clone(); @@ -40,6 +42,26 @@ pub async fn run_for_test_target( for case in tests.test_cases { let case_name = case.name.clone(); + if let Some(partition_config) = &partition_config { + let test_partition = partition_config + .partitions_mapping() + .get(&case.name) + .unwrap_or_else(|| { + panic!("Test name '{}' not found in partitions mapping", case.name) + }); + let should_run_in_partition = + *test_partition == partition_config.partition().index_1_based(); + + if !should_run_in_partition { + tasks.push(tokio::task::spawn(async { + Ok(AnyTestCaseSummary::Single( + TestCaseSummary::ExcludedFromPartition {}, + )) + })); + continue; + } + } + if !tests_filter.should_be_run(&case) { tasks.push(tokio::task::spawn(async { // TODO TestCaseType should also be encoded in the test case definition @@ -68,7 +90,7 @@ pub async fn run_for_test_target( while let Some(task) = tasks.next().await { let result = task??; - if !result.is_interrupted() { + if !result.is_interrupted() && !result.is_excluded_from_partition() { let test_result_message = TestResultMessage::new( &result, forge_config.output_config.detailed_resources, diff --git a/crates/forge/src/run_tests/workspace.rs b/crates/forge/src/run_tests/workspace.rs index 2442deec0c..40c87dd5c9 100644 --- a/crates/forge/src/run_tests/workspace.rs +++ b/crates/forge/src/run_tests/workspace.rs @@ -1,7 +1,9 @@ use super::package::RunForPackageArgs; +use crate::partition::PartitionConfig; use crate::profile_validation::check_profile_compatibility; use crate::run_tests::messages::latest_blocks_numbers::LatestBlocksNumbersMessage; use crate::run_tests::messages::overall_summary::OverallSummaryMessage; +use crate::run_tests::messages::partition::PartitionMessage; use crate::run_tests::messages::tests_failure_summary::TestsFailureSummaryMessage; use crate::warn::{ error_if_snforge_std_deprecated_missing, error_if_snforge_std_deprecated_not_compatible, @@ -29,6 +31,7 @@ use std::env; use std::sync::Arc; #[tracing::instrument(skip_all, level = "debug")] +#[expect(clippy::too_many_lines)] pub async fn run_for_workspace(args: TestArgs, ui: Arc) -> Result { match args.color { // SAFETY: This runs in a single-threaded environment. @@ -94,31 +97,66 @@ pub async fn run_for_workspace(args: TestArgs, ui: Arc) -> Result>>()?; + + let partition_config = PartitionConfig::new(*partition, &packages_args); + + for (package, args) in packages.iter().zip(packages_args) { + env::set_current_dir(&package.root)?; + + let result = run_for_package( + args, + &mut block_number_map, + Some(&partition_config), + ui.clone(), + ) + .await?; + + let filtered = result.filtered(); + all_tests.extend(result.summaries()); + + total_filtered_count = calculate_total_filtered_count(total_filtered_count, filtered); + } + } else { + for package in packages { + env::set_current_dir(&package.root)?; - let args = RunForPackageArgs::build( - package, - &scarb_metadata, - &args, - &cache_dir, - &artifacts_dir_path, - &ui, - )?; + let args = RunForPackageArgs::build( + package, + &scarb_metadata, + &args, + &cache_dir, + &artifacts_dir_path, + &ui, + )?; - let result = run_for_package(args, &mut block_number_map, ui.clone()).await?; + let result = run_for_package(args, &mut block_number_map, None, ui.clone()).await?; - let filtered = result.filtered(); - all_tests.extend(result.summaries()); + let filtered = result.filtered(); + all_tests.extend(result.summaries()); - // Accumulate filtered test counts across packages. When using --exact flag, - // result.filtered_count is None, so total_filtered_count becomes None too. - total_filtered_count = total_filtered_count - .zip(filtered) - .map(|(total, filtered)| total + filtered); + total_filtered_count = calculate_total_filtered_count(total_filtered_count, filtered); + } } let overall_summary = OverallSummaryMessage::new(&all_tests, total_filtered_count); + let all_failed_tests: Vec = extract_failed_tests(all_tests).collect(); FailedTestsCache::new(&cache_dir).save_failed_tests(&all_failed_tests)?; @@ -138,6 +176,10 @@ pub async fn run_for_workspace(args: TestArgs, ui: Arc) -> Result) -> Result, + filtered: Option, +) -> Option { + // Accumulate filtered test counts across packages. When using `--exact` flag, + // `result.filtered_count` is None, so `total_filtered_count` becomes None too. + total_filtered_count + .zip(filtered) + .map(|(total, filtered)| total + filtered) +} + #[tracing::instrument(skip_all, level = "debug")] fn extract_failed_tests( tests_summaries: Vec, diff --git a/crates/forge/test_utils/src/running_tests.rs b/crates/forge/test_utils/src/running_tests.rs index 43ba00dfbb..c061f99df1 100644 --- a/crates/forge/test_utils/src/running_tests.rs +++ b/crates/forge/test_utils/src/running_tests.rs @@ -88,6 +88,7 @@ pub fn run_test_case( fork_targets: vec![], }, &mut BlockNumberMap::default(), + None, ui, )) .expect("Runner fail") diff --git a/crates/forge/tests/data/partitioning/Scarb.toml b/crates/forge/tests/data/partitioning/Scarb.toml new file mode 100644 index 0000000000..fba883415e --- /dev/null +++ b/crates/forge/tests/data/partitioning/Scarb.toml @@ -0,0 +1,39 @@ +[workspace] +members = [ + "crates/package_a", + "crates/package_b", +] + +[workspace.scripts] +test = "snforge" + +[workspace.tool.snforge] + +[workspace.dependencies] +starknet = "2.12.0" +snforge_std = { path = "../../../../../snforge_std" } + +[workspace.package] +version = "0.1.0" + +[package] +name = "partitioning" +version.workspace = true +edition = "2024_07" + +[scripts] +test.workspace = true + +[tool] +snforge.workspace = true + +[dependencies] +starknet.workspace = true +package_a = { path = "crates/package_a" } +package_b = { path = "crates/package_b" } + +[dev-dependencies] +snforge_std.workspace = true + +[[target.starknet-contract]] +sierra = true diff --git a/crates/forge/tests/data/partitioning/crates/package_a/Scarb.toml b/crates/forge/tests/data/partitioning/crates/package_a/Scarb.toml new file mode 100644 index 0000000000..a90f95ab8b --- /dev/null +++ b/crates/forge/tests/data/partitioning/crates/package_a/Scarb.toml @@ -0,0 +1,10 @@ +[package] +name = "package_a" +version.workspace = true +edition = "2024_07" + +[dependencies] +starknet.workspace = true + +[dev-dependencies] +snforge_std.workspace = true diff --git a/crates/forge/tests/data/partitioning/crates/package_a/src/lib.cairo b/crates/forge/tests/data/partitioning/crates/package_a/src/lib.cairo new file mode 100644 index 0000000000..2c8d367431 --- /dev/null +++ b/crates/forge/tests/data/partitioning/crates/package_a/src/lib.cairo @@ -0,0 +1,19 @@ +#[starknet::contract] +pub mod HelloStarknet { + #[storage] + struct Storage {} +} + +#[cfg(test)] +mod tests { + #[test] + fn test_a() { + assert!(1 + 1 == 2); + } + + #[test] + #[ignore] // Ignored on purpose + fn test_b() { + assert!(1 + 1 == 2); + } +} diff --git a/crates/forge/tests/data/partitioning/crates/package_a/tests/tests.cairo b/crates/forge/tests/data/partitioning/crates/package_a/tests/tests.cairo new file mode 100644 index 0000000000..8e7181b60b --- /dev/null +++ b/crates/forge/tests/data/partitioning/crates/package_a/tests/tests.cairo @@ -0,0 +1,9 @@ +#[test] +fn test_c() { + assert!(1 + 1 == 2); +} + +#[test] +fn test_d() { + assert!(1 + 1 == 2); +} diff --git a/crates/forge/tests/data/partitioning/crates/package_b/Scarb.toml b/crates/forge/tests/data/partitioning/crates/package_b/Scarb.toml new file mode 100644 index 0000000000..f09a0d86c8 --- /dev/null +++ b/crates/forge/tests/data/partitioning/crates/package_b/Scarb.toml @@ -0,0 +1,10 @@ +[package] +name = "package_b" +version.workspace = true +edition = "2024_07" + +[dependencies] +starknet.workspace = true + +[dev-dependencies] +snforge_std.workspace = true diff --git a/crates/forge/tests/data/partitioning/crates/package_b/src/lib.cairo b/crates/forge/tests/data/partitioning/crates/package_b/src/lib.cairo new file mode 100644 index 0000000000..d52c38c84a --- /dev/null +++ b/crates/forge/tests/data/partitioning/crates/package_b/src/lib.cairo @@ -0,0 +1,18 @@ +#[starknet::contract] +pub mod HelloStarknet { + #[storage] + struct Storage {} +} + +#[cfg(test)] +mod tests { + #[test] + fn test_e() { + assert!(1 + 1 == 2); + } + + #[test] + fn test_f() { + assert!(1 + 1 == 2); + } +} diff --git a/crates/forge/tests/data/partitioning/crates/package_b/tests/tests.cairo b/crates/forge/tests/data/partitioning/crates/package_b/tests/tests.cairo new file mode 100644 index 0000000000..5ed8490363 --- /dev/null +++ b/crates/forge/tests/data/partitioning/crates/package_b/tests/tests.cairo @@ -0,0 +1,9 @@ +#[test] +fn test_g() { + assert!(1 + 1 == 2); +} + +#[test] +fn test_h() { + assert!(1 + 1 == 3); // Failing on purpose +} diff --git a/crates/forge/tests/data/partitioning/src/lib.cairo b/crates/forge/tests/data/partitioning/src/lib.cairo new file mode 100644 index 0000000000..88efedd9b1 --- /dev/null +++ b/crates/forge/tests/data/partitioning/src/lib.cairo @@ -0,0 +1,18 @@ +#[starknet::contract] +pub mod HelloStarknet { + #[storage] + struct Storage {} +} + +#[cfg(test)] +mod tests { + #[test] + fn test_i() { + assert!(1 + 1 == 2); + } + + #[test] + fn test_j() { + assert!(1 + 1 == 2); + } +} diff --git a/crates/forge/tests/data/partitioning/tests/tests.cairo b/crates/forge/tests/data/partitioning/tests/tests.cairo new file mode 100644 index 0000000000..9489cc9359 --- /dev/null +++ b/crates/forge/tests/data/partitioning/tests/tests.cairo @@ -0,0 +1,15 @@ +#[test] +fn test_k() { + assert!(1 + 1 == 2); +} + +#[test] +fn test_l() { + assert!(1 + 1 == 2); +} + +#[test] +fn test_m() { + assert!(1 + 1 == 2); +} + diff --git a/crates/forge/tests/e2e/mod.rs b/crates/forge/tests/e2e/mod.rs index bc5f573b99..bef09cd2df 100644 --- a/crates/forge/tests/e2e/mod.rs +++ b/crates/forge/tests/e2e/mod.rs @@ -20,6 +20,7 @@ mod fuzzing; mod io_operations; mod new; mod package_warnings; +mod partitioning; mod plugin_diagnostics; mod plugin_versions; mod profiles; diff --git a/crates/forge/tests/e2e/partitioning.rs b/crates/forge/tests/e2e/partitioning.rs new file mode 100644 index 0000000000..a067fe0bff --- /dev/null +++ b/crates/forge/tests/e2e/partitioning.rs @@ -0,0 +1,292 @@ +use super::common::runner::{setup_package, test_runner}; +use indoc::indoc; +use shared::test_utils::output_assert::{assert_stderr_contains, assert_stdout_contains}; + +#[test] +fn test_whole_workspace_partition_1_2() { + let temp = setup_package("partitioning"); + let output = test_runner(&temp) + .args(["--partition", "1/2", "--workspace"]) + .assert() + .code(0); + + assert_stdout_contains( + output, + indoc! {r" + [..]Compiling[..] + [..]Finished[..] + + + Collected 2 test(s) from package_a package + Running 1 test(s) from tests/ + [PASS] package_a_integrationtest::tests::test_c ([..]) + Running 1 test(s) from src/ + [PASS] package_a::tests::test_a ([..]) + Tests: 2 passed, 0 failed, 0 ignored, 0 filtered out + + + Collected 2 test(s) from package_b package + Running 1 test(s) from src/ + [PASS] package_b::tests::test_e ([..]) + Running 1 test(s) from tests/ + [PASS] package_b_integrationtest::tests::test_g ([..]) + Tests: 2 passed, 0 failed, 0 ignored, 0 filtered out + + + Collected 3 test(s) from partitioning package + Running 2 test(s) from tests/ + [PASS] partitioning_integrationtest::tests::test_k ([..]) + [PASS] partitioning_integrationtest::tests::test_m ([..]) + Running 1 test(s) from src/ + [PASS] partitioning::tests::test_i ([..]) + Tests: 3 passed, 0 failed, 0 ignored, 0 filtered out + + + Tests summary: 7 passed, 0 failed, 0 ignored, 0 filtered out + Finished partition run: 1/2 + "}, + ); +} + +#[test] +fn test_whole_workspace_partition_2_2() { + let temp = setup_package("partitioning"); + let output = test_runner(&temp) + .args(["--partition", "2/2", "--workspace"]) + .assert() + .code(1); + + assert_stdout_contains( + output, + indoc! {r#" + [..]Compiling[..] + [..]Finished[..] + + + Collected 2 test(s) from package_a package + Running 1 test(s) from tests/ + [PASS] package_a_integrationtest::tests::test_d ([..]) + Running 1 test(s) from src/ + [IGNORE] package_a::tests::test_b + Tests: 1 passed, 0 failed, 1 ignored, 0 filtered out + + + Collected 2 test(s) from package_b package + Running 1 test(s) from src/ + [PASS] package_b::tests::test_f ([..]) + Running 1 test(s) from tests/ + [FAIL] package_b_integrationtest::tests::test_h + + Failure data: + "assertion failed: `1 + 1 == 3`." + + Tests: 1 passed, 1 failed, 0 ignored, 0 filtered out + + + Collected 2 test(s) from partitioning package + Running 1 test(s) from tests/ + [PASS] partitioning_integrationtest::tests::test_l ([..]) + Running 1 test(s) from src/ + [PASS] partitioning::tests::test_j ([..]) + Tests: 2 passed, 0 failed, 0 ignored, 0 filtered out + + Failures: + package_b_integrationtest::tests::test_h + + Tests summary: 4 passed, 1 failed, 1 ignored, 0 filtered out + Finished partition run: 2/2 + "#}, + ); +} + +#[test] +fn test_whole_workspace_partition_1_3() { + let temp = setup_package("partitioning"); + let output = test_runner(&temp) + .args(["--partition", "1/3", "--workspace"]) + .assert() + .code(0); + + assert_stdout_contains( + output, + indoc! {r" + [..]Compiling[..] + [..]Finished[..] + + + Collected 2 test(s) from package_a package + Running 1 test(s) from src/ + [PASS] package_a::tests::test_a ([..]) + Running 1 test(s) from tests/ + [PASS] package_a_integrationtest::tests::test_d ([..]) + Tests: 2 passed, 0 failed, 0 ignored, 0 filtered out + + + Collected 1 test(s) from package_b package + Running 0 test(s) from src/ + Running 1 test(s) from tests/ + [PASS] package_b_integrationtest::tests::test_g ([..]) + Tests: 1 passed, 0 failed, 0 ignored, 0 filtered out + + + Collected 2 test(s) from partitioning package + Running 1 test(s) from src/ + [PASS] partitioning::tests::test_j ([..]) + Running 1 test(s) from tests/ + [PASS] partitioning_integrationtest::tests::test_m ([..]) + Tests: 2 passed, 0 failed, 0 ignored, 0 filtered out + + + Tests summary: 5 passed, 0 failed, 0 ignored, 0 filtered out + Finished partition run: 1/3 + "}, + ); +} + +#[test] +fn test_whole_workspace_partition_2_3() { + let temp = setup_package("partitioning"); + let output = test_runner(&temp) + .args(["--partition", "2/3", "--workspace"]) + .assert() + .code(1); + + assert_stdout_contains( + output, + indoc! {r#" + [..]Compiling[..] + [..]Finished[..] + + + Collected 1 test(s) from package_a package + Running 0 test(s) from tests/ + Running 1 test(s) from src/ + [IGNORE] package_a::tests::test_b + Tests: 0 passed, 0 failed, 1 ignored, 0 filtered out + + + Collected 2 test(s) from package_b package + Running 1 test(s) from tests/ + [FAIL] package_b_integrationtest::tests::test_h + + Failure data: + "assertion failed: `1 + 1 == 3`." + + Running 1 test(s) from src/ + [PASS] package_b::tests::test_e ([..]) + Tests: 1 passed, 1 failed, 0 ignored, 0 filtered out + + + Collected 1 test(s) from partitioning package + Running 1 test(s) from tests/ + [PASS] partitioning_integrationtest::tests::test_k ([..]) + Running 0 test(s) from src/ + Tests: 1 passed, 0 failed, 0 ignored, 0 filtered out + + Failures: + package_b_integrationtest::tests::test_h + + Tests summary: 2 passed, 1 failed, 1 ignored, 0 filtered out + Finished partition run: 2/3 + "#}, + ); +} + +#[test] +fn test_whole_workspace_partition_3_3() { + let temp = setup_package("partitioning"); + let output = test_runner(&temp) + .args(["--partition", "3/3", "--workspace"]) + .assert() + .code(0); + + assert_stdout_contains( + output, + indoc! {r" + [..]Compiling[..] + [..]Finished[..] + + + Collected 1 test(s) from package_a package + Running 1 test(s) from tests/ + [PASS] package_a_integrationtest::tests::test_c ([..]) + Running 0 test(s) from src/ + Tests: 1 passed, 0 failed, 0 ignored, 0 filtered out + + + Collected 1 test(s) from package_b package + Running 1 test(s) from src/ + [PASS] package_b::tests::test_f ([..]) + Running 0 test(s) from tests/ + Tests: 1 passed, 0 failed, 0 ignored, 0 filtered out + + + Collected 2 test(s) from partitioning package + Running 1 test(s) from tests/ + [PASS] partitioning_integrationtest::tests::test_l ([..]) + Running 1 test(s) from src/ + [PASS] partitioning::tests::test_i ([..]) + Tests: 2 passed, 0 failed, 0 ignored, 0 filtered out + + + Tests summary: 4 passed, 0 failed, 0 ignored, 0 filtered out + Finished partition run: 3/3 + "}, + ); +} + +#[test] +fn test_does_not_work_with_exact_flag() { + let temp = setup_package("partitioning"); + let output = test_runner(&temp) + .args(["--partition", "3/3", "--workspace", "--exact"]) + .assert() + .code(2); + + assert_stderr_contains( + output, + indoc! {r" + error: the argument '--partition ' cannot be used with '--exact' + "}, + ); +} + +#[test] +fn test_works_with_name_filter() { + let temp = setup_package("partitioning"); + let output = test_runner(&temp) + .args(["--partition", "1/3", "--workspace", "test_a"]) + .assert() + .code(0); + + assert_stdout_contains( + output, + indoc! {r" + [..]Compiling[..] + [..]Finished[..] + + + Collected 1 test(s) from package_a package + Running 0 test(s) from tests/ + Running 1 test(s) from src/ + [PASS] package_a::tests::test_a ([..]) + Tests: 1 passed, 0 failed, 0 ignored, 1 filtered out + + + Collected 0 test(s) from package_b package + Running 0 test(s) from src/ + Running 0 test(s) from tests/ + Tests: 0 passed, 0 failed, 0 ignored, 1 filtered out + + + Collected 0 test(s) from partitioning package + Running 0 test(s) from tests/ + Running 0 test(s) from src/ + Tests: 0 passed, 0 failed, 0 ignored, 2 filtered out + + + Tests summary: 1 passed, 0 failed, 0 ignored, 4 filtered out + Finished partition run: 1/3 + "}, + ); +} diff --git a/crates/forge/tests/integration/setup_fork.rs b/crates/forge/tests/integration/setup_fork.rs index 6e3c95c742..8d87d9e565 100644 --- a/crates/forge/tests/integration/setup_fork.rs +++ b/crates/forge/tests/integration/setup_fork.rs @@ -174,6 +174,7 @@ fn fork_aliased_decorator() { }], }, &mut BlockNumberMap::default(), + None, ui, )) .expect("Runner fail") @@ -266,6 +267,7 @@ fn fork_aliased_decorator_overrding() { }], }, &mut BlockNumberMap::default(), + None, ui, )) .expect("Runner fail")