From 4c159c0d61ad734a44fee741008b17f812ae5cc8 Mon Sep 17 00:00:00 2001 From: MatthewMckee4 Date: Sat, 7 Feb 2026 12:39:26 +0000 Subject: [PATCH 1/5] Add `-m` / `--match` flag for regex-based test name filtering Allow users to filter tests by name using regular expressions, enabling patterns like `karva test -m auth` to run only tests containing "auth". Multiple `-m` flags use OR semantics, consistent with `-t` tag filtering. Non-matching tests are skipped rather than excluded. Closes #427 --- Cargo.lock | 1 + crates/karva/src/lib.rs | 3 +- crates/karva/tests/it/main.rs | 1 + crates/karva/tests/it/name_filter.rs | 143 ++++++++++++++++++ crates/karva_cli/src/lib.rs | 10 ++ crates/karva_core/src/cli.rs | 8 +- .../karva_core/src/runner/package_runner.rs | 12 ++ crates/karva_metadata/Cargo.toml | 1 + crates/karva_metadata/src/filter.rs | 141 +++++++++++++++++ crates/karva_metadata/src/options.rs | 3 +- crates/karva_metadata/src/settings.rs | 7 +- crates/karva_runner/src/orchestration.rs | 6 + docs/cli.md | 3 + 13 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 crates/karva/tests/it/name_filter.rs diff --git a/Cargo.lock b/Cargo.lock index a5c4345d..87360f89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1356,6 +1356,7 @@ dependencies = [ "karva_combine", "karva_macros", "karva_system", + "regex", "ruff_db", "ruff_options_metadata", "ruff_python_ast", diff --git a/crates/karva/src/lib.rs b/crates/karva/src/lib.rs index 346b79ca..3a1747c1 100644 --- a/crates/karva/src/lib.rs +++ b/crates/karva/src/lib.rs @@ -11,7 +11,7 @@ use colored::Colorize; use karva_cache::AggregatedResults; use karva_cli::{Args, Command, OutputFormat, TestCommand}; use karva_logging::{Printer, set_colored_override, setup_tracing}; -use karva_metadata::filter::TagFilterSet; +use karva_metadata::filter::{NameFilterSet, TagFilterSet}; use karva_metadata::{ProjectMetadata, ProjectOptionsOverrides}; use karva_project::ProjectDatabase; use karva_python_semantic::current_python_version; @@ -119,6 +119,7 @@ pub(crate) fn test(args: TestCommand) -> Result { }; TagFilterSet::new(&sub_command.tag_expressions)?; + NameFilterSet::new(&sub_command.name_patterns)?; let config = karva_runner::ParallelTestConfig { num_workers, diff --git a/crates/karva/tests/it/main.rs b/crates/karva/tests/it/main.rs index 4b36ff3e..fe646336 100644 --- a/crates/karva/tests/it/main.rs +++ b/crates/karva/tests/it/main.rs @@ -4,3 +4,4 @@ mod basic; mod configuration; mod discovery; mod extensions; +mod name_filter; diff --git a/crates/karva/tests/it/name_filter.rs b/crates/karva/tests/it/name_filter.rs new file mode 100644 index 00000000..80d9892c --- /dev/null +++ b/crates/karva/tests/it/name_filter.rs @@ -0,0 +1,143 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::common::TestContext; + +const TWO_TESTS: &str = r" +def test_alpha(): + assert True + +def test_beta(): + assert True +"; + +#[test] +fn name_filter_substring_match() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("alpha"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_alpha ... ok + test test::test_beta ... skipped + + test result: ok. 1 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_anchored_regex() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("beta$"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_alpha ... skipped + test test::test_beta ... ok + + test result: ok. 1 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_multiple_flags_or_semantics() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("alpha").arg("-m").arg("beta"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_alpha ... ok + test test::test_beta ... ok + + test result: ok. 2 passed; 0 failed; 0 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_no_matches() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("nonexistent"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_alpha ... skipped + test test::test_beta ... skipped + + test result: ok. 0 passed; 0 failed; 2 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_invalid_regex() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("[invalid"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Karva failed + Cause: invalid regex pattern `[invalid`: regex parse error: + [invalid + ^ + error: unclosed character class + Cause: regex parse error: + [invalid + ^ + error: unclosed character class + "); +} + +#[test] +fn name_filter_parametrize() { + let context = TestContext::with_file( + "test.py", + r" +import karva + +@karva.tags.parametrize('x', [1, 2, 3]) +def test_param(x): + assert x > 0 + +def test_other(): + assert True + ", + ); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("test_param"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_param(x=1) ... ok + test test::test_param(x=2) ... ok + test test::test_param(x=3) ... ok + test test::test_other ... skipped + + test result: ok. 3 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_match_all() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg(".*"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_alpha ... ok + test test::test_beta ... ok + + test result: ok. 2 passed; 0 failed; 0 skipped; finished in [TIME] + + ----- stderr ----- + "); +} diff --git a/crates/karva_cli/src/lib.rs b/crates/karva_cli/src/lib.rs index 97786379..6955b890 100644 --- a/crates/karva_cli/src/lib.rs +++ b/crates/karva_cli/src/lib.rs @@ -146,6 +146,16 @@ pub struct SubTestCommand { #[clap(short = 't', long = "tag")] pub tag_expressions: Vec, + /// Filter tests by name using a regular expression. + /// + /// Only tests whose fully qualified name matches the pattern will run. + /// Uses partial matching (the pattern can match anywhere in the name). + /// When specified multiple times, a test runs if it matches any of the patterns. + /// + /// Examples: `-m auth`, `-m '^test::test_login'`, `-m 'slow|fast'`. + #[clap(short = 'm', long = "match")] + pub name_patterns: Vec, + #[clap(flatten)] pub verbosity: Verbosity, } diff --git a/crates/karva_core/src/cli.rs b/crates/karva_core/src/cli.rs index 01dc3e07..5900a8c2 100644 --- a/crates/karva_core/src/cli.rs +++ b/crates/karva_core/src/cli.rs @@ -10,7 +10,7 @@ use karva_cache::{Cache, RunHash}; use karva_cli::{SubTestCommand, Verbosity}; use karva_diagnostic::{DummyReporter, Reporter, TestCaseReporter}; use karva_logging::{Printer, set_colored_override, setup_tracing}; -use karva_metadata::filter::TagFilterSet; +use karva_metadata::filter::{NameFilterSet, TagFilterSet}; use karva_python_semantic::current_python_version; use karva_system::System; use karva_system::path::{TestPath, TestPathError}; @@ -143,11 +143,13 @@ fn run(f: impl FnOnce(Vec) -> Vec) -> anyhow::Result PackageRunner<'ctx, 'a> { } } + let name_filter = &self.context.settings().test().name_filter; + if !name_filter.is_empty() { + let display_name = QualifiedTestName::new(name.clone(), None).to_string(); + if !name_filter.matches(&display_name) { + return self.context.register_test_case_result( + &QualifiedTestName::new(name, None), + IndividualTestResultKind::Skipped { reason: None }, + std::time::Duration::ZERO, + ); + } + } + if let (true, reason) = tags.should_skip() { return self.context.register_test_case_result( &QualifiedTestName::new(name, None), diff --git a/crates/karva_metadata/Cargo.toml b/crates/karva_metadata/Cargo.toml index 90529f04..23324eeb 100644 --- a/crates/karva_metadata/Cargo.toml +++ b/crates/karva_metadata/Cargo.toml @@ -18,6 +18,7 @@ karva_macros = { workspace = true } karva_system = { workspace = true } camino = { workspace = true } +regex = { workspace = true } ruff_db = { workspace = true } ruff_options_metadata = { workspace = true } ruff_python_ast = { workspace = true } diff --git a/crates/karva_metadata/src/filter.rs b/crates/karva_metadata/src/filter.rs index cf0e2e3c..bef41eed 100644 --- a/crates/karva_metadata/src/filter.rs +++ b/crates/karva_metadata/src/filter.rs @@ -1,5 +1,78 @@ use std::fmt; +use regex::Regex; + +/// A name filter that matches test names using a regular expression. +#[derive(Debug, Clone)] +pub struct NameFilter { + regex: Regex, +} + +impl NameFilter { + pub fn new(pattern: &str) -> Result { + let regex = Regex::new(pattern).map_err(|err| NameFilterError::InvalidRegex { + pattern: pattern.to_string(), + source: err, + })?; + Ok(Self { regex }) + } + + pub fn matches(&self, name: &str) -> bool { + self.regex.is_match(name) + } +} + +/// A set of name filters. Any filter must match for the set to match (OR semantics across `-m` flags). +#[derive(Debug, Clone, Default)] +pub struct NameFilterSet { + filters: Vec, +} + +impl NameFilterSet { + pub fn new(patterns: &[String]) -> Result { + let mut filters = Vec::with_capacity(patterns.len()); + for pattern in patterns { + filters.push(NameFilter::new(pattern)?); + } + Ok(Self { filters }) + } + + pub fn is_empty(&self) -> bool { + self.filters.is_empty() + } + + pub fn matches(&self, name: &str) -> bool { + self.filters.is_empty() || self.filters.iter().any(|f| f.matches(name)) + } +} + +/// Error that occurs when parsing a name filter pattern. +#[derive(Debug)] +pub enum NameFilterError { + InvalidRegex { + pattern: String, + source: regex::Error, + }, +} + +impl fmt::Display for NameFilterError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidRegex { pattern, source } => { + write!(f, "invalid regex pattern `{pattern}`: {source}") + } + } + } +} + +impl std::error::Error for NameFilterError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidRegex { source, .. } => Some(source), + } + } +} + /// A parsed tag filter expression that can be matched against a set of tag names. #[derive(Debug, Clone)] pub struct TagFilter { @@ -668,4 +741,72 @@ mod tests { fn filter_set_rejects_invalid_expression() { assert!(TagFilterSet::new(&["slow".to_string(), "and".to_string()]).is_err()); } + + // ── NameFilter tests ───────────────────────────────────────────────── + + #[test] + fn name_filter_partial_match() { + let f = NameFilter::new("auth").expect("parse"); + assert!(f.matches("test::test_auth_login")); + assert!(f.matches("test::test_auth")); + assert!(!f.matches("test::test_login")); + } + + #[test] + fn name_filter_anchored_start() { + let f = NameFilter::new("^test::test_login").expect("parse"); + assert!(f.matches("test::test_login")); + assert!(f.matches("test::test_login_flow")); + assert!(!f.matches("other::test_login")); + } + + #[test] + fn name_filter_anchored_end() { + let f = NameFilter::new("login$").expect("parse"); + assert!(f.matches("test::test_login")); + assert!(!f.matches("test::test_login_flow")); + } + + #[test] + fn name_filter_alternation() { + let f = NameFilter::new("slow|fast").expect("parse"); + assert!(f.matches("test::test_slow")); + assert!(f.matches("test::test_fast")); + assert!(!f.matches("test::test_medium")); + } + + #[test] + fn name_filter_invalid_regex() { + assert!(matches!( + NameFilter::new("[invalid"), + Err(NameFilterError::InvalidRegex { .. }) + )); + } + + #[test] + fn name_filter_set_or_semantics() { + let set = NameFilterSet::new(&["test_a".to_string(), "test_b".to_string()]).expect("parse"); + assert!(set.matches("test::test_a")); + assert!(set.matches("test::test_b")); + assert!(!set.matches("test::test_c")); + } + + #[test] + fn name_filter_set_empty_always_matches() { + let set = NameFilterSet::new(&[]).expect("parse"); + assert!(set.is_empty()); + assert!(set.matches("anything")); + } + + #[test] + fn name_filter_set_rejects_invalid_pattern() { + assert!(NameFilterSet::new(&["valid".to_string(), "[invalid".to_string()]).is_err()); + } + + #[test] + fn name_filter_parametrized_name() { + let f = NameFilter::new(r"param=1").expect("parse"); + assert!(f.matches("test::test_add(param=1)")); + assert!(!f.matches("test::test_add(param=2)")); + } } diff --git a/crates/karva_metadata/src/options.rs b/crates/karva_metadata/src/options.rs index df4cbf0c..20d8551c 100644 --- a/crates/karva_metadata/src/options.rs +++ b/crates/karva_metadata/src/options.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::System; -use crate::filter::TagFilterSet; +use crate::filter::{NameFilterSet, TagFilterSet}; use crate::settings::{ProjectSettings, SrcSettings, TerminalSettings, TestSettings}; #[derive( @@ -219,6 +219,7 @@ impl TestOptions { try_import_fixtures: self.try_import_fixtures.unwrap_or_default(), retry: self.retry.unwrap_or_default(), tag_filter: TagFilterSet::default(), + name_filter: NameFilterSet::default(), } } } diff --git a/crates/karva_metadata/src/settings.rs b/crates/karva_metadata/src/settings.rs index 7eacf9f0..02a2d0bb 100644 --- a/crates/karva_metadata/src/settings.rs +++ b/crates/karva_metadata/src/settings.rs @@ -1,4 +1,4 @@ -use crate::filter::TagFilterSet; +use crate::filter::{NameFilterSet, TagFilterSet}; use crate::options::OutputFormat; #[derive(Default, Debug, Clone)] @@ -28,6 +28,10 @@ impl ProjectSettings { pub fn set_tag_filter(&mut self, tag_filter: TagFilterSet) { self.test.tag_filter = tag_filter; } + + pub fn set_name_filter(&mut self, name_filter: NameFilterSet) { + self.test.name_filter = name_filter; + } } #[derive(Default, Debug, Clone)] @@ -49,4 +53,5 @@ pub struct TestSettings { pub try_import_fixtures: bool, pub retry: u32, pub tag_filter: TagFilterSet, + pub name_filter: NameFilterSet, } diff --git a/crates/karva_runner/src/orchestration.rs b/crates/karva_runner/src/orchestration.rs index 8226ddc4..083d3c8c 100644 --- a/crates/karva_runner/src/orchestration.rs +++ b/crates/karva_runner/src/orchestration.rs @@ -319,6 +319,7 @@ fn inner_cli_args(settings: &ProjectSettings, args: &SubTestCommand) -> Vec = args.tag_expressions.clone(); + let name_patterns: Vec = args.name_patterns.clone(); cli_args .iter() @@ -328,5 +329,10 @@ fn inner_cli_args(settings: &ProjectSettings, args: &SubTestCommand) -> VecMay also be set with the KARVA_CONFIG_FILE environment variable.

--fail-fast

When set, the test will fail immediately if any test fails.

This only works when running tests in parallel.

--help, -h

Print help (see a summary with '-h')

+
--match, -m name-patterns

Filter tests by name using a regular expression.

+

Only tests whose fully qualified name matches the pattern will run. Uses partial matching (the pattern can match anywhere in the name). When specified multiple times, a test runs if it matches any of the patterns.

+

Examples: -m auth, -m '^test::test_login', -m 'slow|fast'.

--no-cache

Disable reading the karva cache for test duration history

--no-ignore

When set, .gitignore files will not be respected

--no-parallel

Disable parallel execution (equivalent to --num-workers 1)

From 4058fe649670985ee768b56a6924ee12dc2362a7 Mon Sep 17 00:00:00 2001 From: MatthewMckee4 Date: Sat, 7 Feb 2026 12:45:22 +0000 Subject: [PATCH 2/5] Add more regex integration tests and fix duplicate error output Add 11 new integration tests covering alternation, character classes, quantifiers, module-scoped filtering, tag+name filter combination, case sensitivity, inline flags, and additional invalid regex cases. Fix NameFilterError to not duplicate the regex error in the error chain. --- crates/karva/tests/it/name_filter.rs | 299 ++++++++++++++++++++++++++- crates/karva_metadata/src/filter.rs | 8 +- 2 files changed, 296 insertions(+), 11 deletions(-) diff --git a/crates/karva/tests/it/name_filter.rs b/crates/karva/tests/it/name_filter.rs index 80d9892c..3461fdd5 100644 --- a/crates/karva/tests/it/name_filter.rs +++ b/crates/karva/tests/it/name_filter.rs @@ -87,10 +87,6 @@ fn name_filter_invalid_regex() { Cause: invalid regex pattern `[invalid`: regex parse error: [invalid ^ - error: unclosed character class - Cause: regex parse error: - [invalid - ^ error: unclosed character class "); } @@ -141,3 +137,298 @@ fn name_filter_match_all() { ----- stderr ----- "); } + +#[test] +fn name_filter_alternation() { + let context = TestContext::with_file( + "test.py", + r" +def test_login(): + assert True + +def test_logout(): + assert True + +def test_signup(): + assert True + ", + ); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("login|signup"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_login ... ok + test test::test_logout ... skipped + test test::test_signup ... ok + + test result: ok. 2 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_character_class() { + let context = TestContext::with_file( + "test.py", + r" +def test_v1(): + assert True + +def test_v2(): + assert True + +def test_v10(): + assert True + ", + ); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg(r"test_v[12]$"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_v1 ... ok + test test::test_v2 ... ok + test test::test_v10 ... skipped + + test result: ok. 2 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_quantifier() { + let context = TestContext::with_file( + "test.py", + r" +def test_a(): + assert True + +def test_ab(): + assert True + +def test_abb(): + assert True + ", + ); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg(r"test_ab+$"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_a ... skipped + test test::test_ab ... ok + test test::test_abb ... ok + + test result: ok. 2 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_qualified_name_prefix() { + let context = TestContext::with_files([ + ( + "test_auth.py", + r" +def test_login(): + assert True + +def test_logout(): + assert True + ", + ), + ( + "test_users.py", + r" +def test_login(): + assert True + ", + ), + ]); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("^test_auth::"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test_users::test_login ... skipped + test test_auth::test_login ... ok + test test_auth::test_logout ... ok + + test result: ok. 2 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_combined_with_tag_filter() { + let context = TestContext::with_file( + "test.py", + r" +import karva + +@karva.tags.slow +def test_slow_alpha(): + assert True + +@karva.tags.slow +def test_slow_beta(): + assert True + +def test_fast_alpha(): + assert True + ", + ); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-t").arg("slow").arg("-m").arg("alpha"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_slow_alpha ... ok + test test::test_slow_beta ... skipped + test test::test_fast_alpha ... skipped + + test result: ok. 1 passed; 0 failed; 2 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_case_sensitive() { + let context = TestContext::with_file( + "test.py", + r" +def test_Alpha(): + assert True + +def test_alpha(): + assert True + ", + ); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("Alpha"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_Alpha ... ok + test test::test_alpha ... skipped + + test result: ok. 1 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_case_insensitive_regex() { + let context = TestContext::with_file( + "test.py", + r" +def test_Alpha(): + assert True + +def test_alpha(): + assert True + ", + ); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("(?i)alpha"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_Alpha ... ok + test test::test_alpha ... ok + + test result: ok. 2 passed; 0 failed; 0 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +#[test] +fn name_filter_dot_metacharacter() { + let context = TestContext::with_file( + "test.py", + r" +def test_a1(): + assert True + +def test_a2(): + assert True + +def test_ab(): + assert True + ", + ); + + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg(r"test_a\d"), @r" + success: true + exit_code: 0 + ----- stdout ----- + test test::test_a1 ... ok + test test::test_a2 ... ok + test test::test_ab ... skipped + + test result: ok. 2 passed; 0 failed; 1 skipped; finished in [TIME] + + ----- stderr ----- + "); +} + +// ── Invalid regex error cases ──────────────────────────────────────── + +#[test] +fn name_filter_invalid_regex_unclosed_group() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("(unclosed"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Karva failed + Cause: invalid regex pattern `(unclosed`: regex parse error: + (unclosed + ^ + error: unclosed group + "); +} + +#[test] +fn name_filter_invalid_regex_invalid_repetition() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("*invalid"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Karva failed + Cause: invalid regex pattern `*invalid`: regex parse error: + *invalid + ^ + error: repetition operator missing expression + "); +} + +#[test] +fn name_filter_invalid_regex_bad_escape() { + let context = TestContext::with_file("test.py", TWO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg(r"\p{Invalid}"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Karva failed + Cause: invalid regex pattern `\p{Invalid}`: regex parse error: + \p{Invalid} + ^^^^^^^^^^^ + error: Unicode property not found + "); +} diff --git a/crates/karva_metadata/src/filter.rs b/crates/karva_metadata/src/filter.rs index bef41eed..19166d8f 100644 --- a/crates/karva_metadata/src/filter.rs +++ b/crates/karva_metadata/src/filter.rs @@ -65,13 +65,7 @@ impl fmt::Display for NameFilterError { } } -impl std::error::Error for NameFilterError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::InvalidRegex { source, .. } => Some(source), - } - } -} +impl std::error::Error for NameFilterError {} /// A parsed tag filter expression that can be matched against a set of tag names. #[derive(Debug, Clone)] From 40a990e29c30609a7869148733190c63128e91d6 Mon Sep 17 00:00:00 2001 From: MatthewMckee4 Date: Sat, 7 Feb 2026 12:46:35 +0000 Subject: [PATCH 3/5] Remove section-separator comments from tests --- crates/karva/tests/it/name_filter.rs | 2 -- crates/karva_metadata/src/filter.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/crates/karva/tests/it/name_filter.rs b/crates/karva/tests/it/name_filter.rs index 3461fdd5..5f3fd76c 100644 --- a/crates/karva/tests/it/name_filter.rs +++ b/crates/karva/tests/it/name_filter.rs @@ -380,8 +380,6 @@ def test_ab(): "); } -// ── Invalid regex error cases ──────────────────────────────────────── - #[test] fn name_filter_invalid_regex_unclosed_group() { let context = TestContext::with_file("test.py", TWO_TESTS); diff --git a/crates/karva_metadata/src/filter.rs b/crates/karva_metadata/src/filter.rs index 19166d8f..7e6470ad 100644 --- a/crates/karva_metadata/src/filter.rs +++ b/crates/karva_metadata/src/filter.rs @@ -736,8 +736,6 @@ mod tests { assert!(TagFilterSet::new(&["slow".to_string(), "and".to_string()]).is_err()); } - // ── NameFilter tests ───────────────────────────────────────────────── - #[test] fn name_filter_partial_match() { let f = NameFilter::new("auth").expect("parse"); From b37415ca53e2824a232cac5bd819eb77b7bf0860 Mon Sep 17 00:00:00 2001 From: MatthewMckee4 Date: Sat, 7 Feb 2026 12:50:01 +0000 Subject: [PATCH 4/5] Simplify qualified name prefix test to use single file --- crates/karva/tests/it/name_filter.rs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/crates/karva/tests/it/name_filter.rs b/crates/karva/tests/it/name_filter.rs index 5f3fd76c..3dfe3fb0 100644 --- a/crates/karva/tests/it/name_filter.rs +++ b/crates/karva/tests/it/name_filter.rs @@ -230,33 +230,27 @@ def test_abb(): #[test] fn name_filter_qualified_name_prefix() { - let context = TestContext::with_files([ - ( - "test_auth.py", - r" + let context = TestContext::with_file( + "test.py", + r" def test_login(): assert True def test_logout(): assert True - ", - ), - ( - "test_users.py", - r" -def test_login(): + +def test_signup(): assert True - ", - ), - ]); + ", + ); - assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("^test_auth::"), @r" + assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg("^test::test_log"), @r" success: true exit_code: 0 ----- stdout ----- - test test_users::test_login ... skipped - test test_auth::test_login ... ok - test test_auth::test_logout ... ok + test test::test_login ... ok + test test::test_logout ... ok + test test::test_signup ... skipped test result: ok. 2 passed; 0 failed; 1 skipped; finished in [TIME] From 9e0f28ba17de8437a51d93f1792528e9d93eb702 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Sat, 7 Feb 2026 12:59:43 +0000 Subject: [PATCH 5/5] Apply suggestion from @MatthewMckee4 --- crates/karva/tests/it/name_filter.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/karva/tests/it/name_filter.rs b/crates/karva/tests/it/name_filter.rs index 3dfe3fb0..f997ec7f 100644 --- a/crates/karva/tests/it/name_filter.rs +++ b/crates/karva/tests/it/name_filter.rs @@ -123,6 +123,7 @@ def test_other(): } #[test] +#[cfg(unix)] fn name_filter_match_all() { let context = TestContext::with_file("test.py", TWO_TESTS); assert_cmd_snapshot!(context.command_no_parallel().arg("-m").arg(".*"), @r"