Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
255d523
Skip config resolution for non-matching targets in `--exact` runs
franciszekjob Apr 21, 2026
6ce1dbb
Remove `exact_filter_does_not_resolve_config_for_filtered_out_tests`
franciszekjob Apr 21, 2026
33f62e1
Remove test package
franciszekjob Apr 21, 2026
0d4a06a
Make self review
franciszekjob Apr 21, 2026
fb53920
Add `exact_filter_does_not_resolve_config_for_filtered_out_tests` test
franciszekjob Apr 21, 2026
40240e8
Fix lint
franciszekjob Apr 21, 2026
283a874
Small improvements
franciszekjob Apr 21, 2026
148824e
Fix typo
franciszekjob Apr 21, 2026
ca488e4
Merge branch 'master' into franciszekjob/3899-optimize-configs-resolu…
franciszekjob Apr 21, 2026
da14311
Format comment
franciszekjob Apr 21, 2026
2213b0a
Merge branch 'franciszekjob/3899-optimize-configs-resolution-with-exa…
franciszekjob Apr 21, 2026
fa6719f
Extend logic for work with match too
franciszekjob Apr 22, 2026
f367291
Revert "Extend logic for work with match too"
franciszekjob Apr 22, 2026
513ac01
Reorder `prepare_test_target` body
franciszekjob Apr 22, 2026
8086a48
Rename variables
franciszekjob Apr 22, 2026
72cb08b
Update comment
franciszekjob Apr 22, 2026
3c50e06
Rename `TestSelectionMode` -> `TestNameSelection`
franciszekjob Apr 22, 2026
82b8198
Apply review suggestion
franciszekjob Apr 29, 2026
0660f41
Refactor name filter
franciszekjob May 1, 2026
1bcb710
Return `Option<TestTargetWithConfig>` from `prepare_test_target` on n…
franciszekjob May 4, 2026
40c77f8
Guard filtered-out test against silent pass
franciszekjob May 4, 2026
1d1b340
Simplify test case selection by using `matches` for all filter types
franciszekjob May 4, 2026
c445e98
Fix filtered out count regression for `Match` filter
franciszekjob May 5, 2026
ad4d143
Skip CASM compilation for targets with no matching tests
franciszekjob May 5, 2026
dc93e70
Revert non-exact match filtering optimization
franciszekjob May 5, 2026
8cbc7ff
Fix missing "Running 0 test(s)" message for exact-match filtered-out …
franciszekjob May 5, 2026
7dab4d4
Simplify target ordering
franciszekjob May 5, 2026
4c4335a
Fix lint
franciszekjob May 5, 2026
e1a7c8c
Use ordered target tuples instead of `PreparedTarget`
franciszekjob May 5, 2026
42dabac
Drop `NameFilter::exact_match` helper
franciszekjob May 5, 2026
6152fc8
Avoid cloning targets for RPC warning checks
franciszekjob May 5, 2026
61f8526
Trigger CI
franciszekjob May 6, 2026
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
54 changes: 54 additions & 0 deletions crates/forge-runner/src/filtering.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
use crate::package_tests::TestCase;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NameFilter {
All,
Match(String),
ExactMatch(String),
}

impl NameFilter {
#[must_use]
pub fn from_flags(test_name_filter: Option<String>, exact_match: bool) -> Self {
if exact_match {
Self::ExactMatch(
test_name_filter
.expect("Argument test_name_filter cannot be None with exact_match"),
)
} else if let Some(name) = test_name_filter {
Self::Match(name)
} else {
Self::All
}
}

#[must_use]
pub fn matches(&self, sanitized_name: &str) -> bool {
match self {
Self::All => true,
Self::Match(filter) => sanitized_name.contains(filter),
Self::ExactMatch(name) => sanitized_name == name,
}
}
}

/// Result of filtering a test case.
#[derive(Debug)]
pub enum FilterResult {
Expand Down Expand Up @@ -27,3 +59,25 @@ pub trait TestCaseFilter {
pub trait TestCaseIsIgnored {
fn is_ignored(&self) -> bool;
}

#[cfg(test)]
mod tests {
use super::NameFilter;

#[test]
fn name_filter_all_matches_everything() {
assert!(NameFilter::All.matches("any::test"));
}

#[test]
fn name_filter_match_uses_substring_matching() {
assert!(NameFilter::Match("selected".to_string()).matches("pkg::selected_test"));
assert!(!NameFilter::Match("other".to_string()).matches("pkg::selected_test"));
}

#[test]
fn name_filter_exact_match_uses_equality() {
assert!(NameFilter::ExactMatch("pkg::test".to_string()).matches("pkg::test"));
assert!(!NameFilter::ExactMatch("pkg::test".to_string()).matches("pkg::test_case"));
}
}
120 changes: 90 additions & 30 deletions crates/forge-runner/src/running/target.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::{
filtering::NameFilter,
forge_config::ForgeTrackedResource,
package_tests::{
TestDetails,
TestDetails, TestTargetLocation,
raw::TestTargetRaw,
with_config::{TestCaseWithConfig, TestTargetWithConfig},
with_config_resolved::sanitize_test_case_name,
},
running::config_run::run_config_pass,
};
Expand All @@ -12,16 +14,48 @@ use cairo_lang_sierra::{
ids::ConcreteTypeId,
program::{GenFunction, StatementIdx, TypeDeclaration},
};
use rayon::iter::IntoParallelIterator;
use rayon::iter::IntoParallelRefIterator;
use rayon::iter::ParallelIterator;
use std::{collections::HashMap, sync::Arc};
use universal_sierra_compiler_api::compile_raw_sierra_at_path;
use universal_sierra_compiler_api::representation::RawCasmProgram;

#[tracing::instrument(skip_all, level = "debug")]
pub fn prepare_test_target(
test_target_raw: TestTargetRaw,
tracked_resource: &ForgeTrackedResource,
) -> Result<TestTargetWithConfig> {
name_filter: &NameFilter,
) -> Result<(Option<TestTargetWithConfig>, TestTargetLocation)> {
let tests_location = test_target_raw.tests_location;
let default_executables = vec![];
let executables = test_target_raw
.sierra_program
.debug_info
.as_ref()
.and_then(|info| info.executables.get("snforge_internal_test_executable"))
.unwrap_or(&default_executables);

let exact_matches = match name_filter {
NameFilter::ExactMatch(exact_match) => {
let matches = executables
.iter()
.filter_map(|case| {
let raw_name: String = case.debug_name.clone()?.into();
let sanitized_name = sanitize_test_case_name(&raw_name);
(sanitized_name == *exact_match).then_some((&case.id, raw_name))
})
.collect::<Vec<_>>();

if matches.is_empty() {
return Ok((None, tests_location));
}

Some(matches)
}
NameFilter::All | NameFilter::Match(_) => None,
};

macro_rules! by_id {
($field:ident) => {{
let temp: HashMap<_, _> = test_target_raw
Expand All @@ -42,37 +76,63 @@ pub fn prepare_test_target(
test_target_raw.sierra_program_path.as_std_path(),
)?);

let default_executables = vec![];
let executables = test_target_raw
.sierra_program
.debug_info
.as_ref()
.and_then(|info| info.executables.get("snforge_internal_test_executable"))
.unwrap_or(&default_executables);

let test_cases = executables
.par_iter()
.map(|case| -> Result<TestCaseWithConfig> {
let func = funcs[&case.id];

let test_details = build_test_details(func, &type_declarations);
let test_cases = if let Some(matches) = exact_matches {
matches
.into_par_iter()
.map(|(id, name)| {
build_test_case_with_config(
funcs[id],
name,
&type_declarations,
&casm_program,
*tracked_resource,
)
})
.collect::<Result<_>>()?
} else {
executables
.par_iter()
.map(|case| {
build_test_case_with_config(
funcs[&case.id],
case.debug_name
.clone()
.expect("Failed to get test case name")
.into(),
&type_declarations,
&casm_program,
*tracked_resource,
)
})
.collect::<Result<_>>()?
};

let raw_config = run_config_pass(&test_details, &casm_program, tracked_resource)?;
Ok((
Some(TestTargetWithConfig {
tests_location,
test_cases,
sierra_program: test_target_raw.sierra_program,
sierra_program_path: test_target_raw.sierra_program_path.into(),
casm_program,
}),
tests_location,
))
}

Ok(TestCaseWithConfig {
config: raw_config.into(),
name: case.debug_name.clone().unwrap().into(),
test_details,
})
})
.collect::<Result<_>>()?;
fn build_test_case_with_config(
func: &GenFunction<StatementIdx>,
name: String,
type_declarations: &HashMap<u64, &TypeDeclaration>,
casm_program: &Arc<RawCasmProgram>,
tracked_resource: ForgeTrackedResource,
) -> Result<TestCaseWithConfig> {
let test_details = build_test_details(func, type_declarations);
let raw_config = run_config_pass(&test_details, casm_program, &tracked_resource)?;

Ok(TestTargetWithConfig {
tests_location: test_target_raw.tests_location,
test_cases,
sierra_program: test_target_raw.sierra_program,
sierra_program_path: test_target_raw.sierra_program_path.into(),
casm_program,
Ok(TestCaseWithConfig {
config: raw_config.into(),
name,
test_details,
})
}

Expand Down
49 changes: 38 additions & 11 deletions crates/forge/src/run_tests/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,23 @@ use crate::{
tests_summary::TestsSummaryMessage,
},
shared_cache::FailedTestsCache,
test_filter::{NameFilter, TestsFilter},
test_filter::TestsFilter,
warn::warn_if_incompatible_rpc_version,
};
use anyhow::Result;
use camino::{Utf8Path, Utf8PathBuf};
use cheatnet::runtime_extensions::forge_runtime_extension::contracts_data::ContractsData;
use console::Style;
use forge_runner::{
filtering::NameFilter,
forge_config::{ForgeConfig, ForgeTrackedResource},
package_tests::{
TestTargetLocation,
raw::TestTargetRaw,
with_config::TestTargetWithConfig,
with_config_resolved::{TestCaseWithResolvedConfig, sanitize_test_case_name},
with_config_resolved::{
TestCaseWithResolvedConfig, TestTargetWithResolvedConfig, sanitize_test_case_name,
},
},
partition::PartitionConfig,
running::target::prepare_test_target,
Expand All @@ -41,6 +45,8 @@ use scarb_metadata::{Metadata, PackageMetadata};
use std::sync::Arc;
use tokio::task::JoinHandle;

type PrepareTargetHandle = JoinHandle<Result<(Option<TestTargetWithConfig>, TestTargetLocation)>>;

pub struct PackageTestResult {
summaries: Vec<TestTargetSummary>,
filtered: Option<usize>,
Expand All @@ -67,7 +73,7 @@ impl PackageTestResult {
}

pub struct RunForPackageArgs {
pub target_handles: Vec<JoinHandle<Result<TestTargetWithConfig>>>,
pub target_handles: Vec<PrepareTargetHandle>,
pub tests_filter: TestsFilter,
pub forge_config: Arc<ForgeConfig>,
pub fork_targets: Vec<ForkTarget>,
Expand Down Expand Up @@ -125,10 +131,11 @@ impl RunForPackageArgs {
}

let tracked_resource = forge_config.test_runner_config.tracked_resource;
let name_filter = tests_filter.name_filter.clone();

let target_handles = raw_test_targets
.into_iter()
.map(|t| spawn_prepare_test_target(t, tracked_resource))
.map(|t| spawn_prepare_test_target(t, tracked_resource, name_filter.clone()))
.collect();

Ok(RunForPackageArgs {
Expand All @@ -145,8 +152,11 @@ impl RunForPackageArgs {
fn spawn_prepare_test_target(
target: TestTargetRaw,
tracked_resource: ForgeTrackedResource,
) -> JoinHandle<Result<TestTargetWithConfig>> {
tokio::task::spawn_blocking(move || prepare_test_target(target, &tracked_resource))
name_filter: NameFilter,
) -> PrepareTargetHandle {
tokio::task::spawn_blocking(move || {
prepare_test_target(target, &tracked_resource, &name_filter)
})
}

fn sum_test_cases_from_test_target(
Expand Down Expand Up @@ -185,12 +195,18 @@ pub async fn run_for_package(
exit_first_channel: &mut ExitFirstChannel,
) -> Result<PackageTestResult> {
// Resolve all targets first so the collected count includes #[ignore] filtering.
let mut resolved_targets = vec![];
let mut resolved_targets: Vec<(TestTargetLocation, Option<TestTargetWithResolvedConfig>)> =
vec![];
let mut all_tests = 0;
let mut not_filtered_total = 0;

for handle in target_handles {
let target_with_config = handle.await??;
let (maybe_target, tests_location) = handle.await??;

let Some(target_with_config) = maybe_target else {
resolved_targets.push((tests_location, None));
continue;
};

let mut resolved = resolve_config(
target_with_config,
Expand All @@ -212,10 +228,16 @@ pub async fn run_for_package(
all_tests += all;
not_filtered_total += not_filtered;

resolved_targets.push(resolved);
resolved_targets.push((tests_location, Some(resolved)));
}

warn_if_incompatible_rpc_version(&resolved_targets, ui.clone()).await?;
warn_if_incompatible_rpc_version(
resolved_targets
.iter()
.filter_map(|(_, resolved)| resolved.as_ref()),
ui.clone(),
)
.await?;

ui.println(&CollectedTestsCountMessage {
tests_num: not_filtered_total,
Expand All @@ -224,7 +246,12 @@ pub async fn run_for_package(

let mut summaries = vec![];

for resolved in resolved_targets {
for (location, resolved) in resolved_targets {
let Some(resolved) = resolved else {
ui.println(&TestsRunMessage::new(location, 0));
continue;
};

ui.println(&TestsRunMessage::new(
resolved.tests_location,
sum_test_cases_from_test_target(
Expand Down
Loading
Loading