From ac5f5a57af39c6a12679bf4bdf1459fb7b5b96fe Mon Sep 17 00:00:00 2001 From: omchillure Date: Fri, 17 Apr 2026 23:12:14 +0530 Subject: [PATCH 1/6] Fix: Configurable behavior when no tests match --- .karva_cache/last-failed.json | 1 + .../run-1776445309804/worker-0/durations.json | 10 ++++ .../run-1776445309804/worker-0/stats.json | 3 + .../run-1776445492867/worker-0/durations.json | 10 ++++ .../run-1776445492867/worker-0/stats.json | 3 + crates/karva/src/commands/test/mod.rs | 28 +++++++++- crates/karva/tests/it/filterset.rs | 56 ++++++++++++++++--- crates/karva_cli/src/lib.rs | 42 +++++++++++++- crates/karva_metadata/src/lib.rs | 2 +- crates/karva_metadata/src/options.rs | 16 +++++- crates/karva_metadata/src/settings.rs | 23 ++++++++ docs/cli.md | 8 ++- docs/configuration.md | 19 +++++++ 13 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 .karva_cache/last-failed.json create mode 100644 .karva_cache/run-1776445309804/worker-0/durations.json create mode 100644 .karva_cache/run-1776445309804/worker-0/stats.json create mode 100644 .karva_cache/run-1776445492867/worker-0/durations.json create mode 100644 .karva_cache/run-1776445492867/worker-0/stats.json diff --git a/.karva_cache/last-failed.json b/.karva_cache/last-failed.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/.karva_cache/last-failed.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.karva_cache/run-1776445309804/worker-0/durations.json b/.karva_cache/run-1776445309804/worker-0/durations.json new file mode 100644 index 00000000..f16de801 --- /dev/null +++ b/.karva_cache/run-1776445309804/worker-0/durations.json @@ -0,0 +1,10 @@ +{ + "test.test_no_tests::test_fast": { + "secs": 0, + "nanos": 66580 + }, + "test.test_no_tests::test_untagged": { + "secs": 0, + "nanos": 8926 + } +} \ No newline at end of file diff --git a/.karva_cache/run-1776445309804/worker-0/stats.json b/.karva_cache/run-1776445309804/worker-0/stats.json new file mode 100644 index 00000000..76c0d3a8 --- /dev/null +++ b/.karva_cache/run-1776445309804/worker-0/stats.json @@ -0,0 +1,3 @@ +{ + "passed": 2 +} \ No newline at end of file diff --git a/.karva_cache/run-1776445492867/worker-0/durations.json b/.karva_cache/run-1776445492867/worker-0/durations.json new file mode 100644 index 00000000..6e148a4f --- /dev/null +++ b/.karva_cache/run-1776445492867/worker-0/durations.json @@ -0,0 +1,10 @@ +{ + "test.test_no_tests::test_fast": { + "secs": 0, + "nanos": 83744 + }, + "test.test_no_tests::test_untagged": { + "secs": 0, + "nanos": 9898 + } +} \ No newline at end of file diff --git a/.karva_cache/run-1776445492867/worker-0/stats.json b/.karva_cache/run-1776445492867/worker-0/stats.json new file mode 100644 index 00000000..76c0d3a8 --- /dev/null +++ b/.karva_cache/run-1776445492867/worker-0/stats.json @@ -0,0 +1,3 @@ +{ + "passed": 2 +} \ No newline at end of file diff --git a/crates/karva/src/commands/test/mod.rs b/crates/karva/src/commands/test/mod.rs index 28f58a06..968c657d 100644 --- a/crates/karva/src/commands/test/mod.rs +++ b/crates/karva/src/commands/test/mod.rs @@ -9,7 +9,7 @@ use karva_cache::AggregatedResults; use karva_cli::{OutputFormat, TestCommand}; use karva_logging::{Printer, Stdout, set_colored_override, setup_tracing}; use karva_metadata::filter::FiltersetSet; -use karva_metadata::{ProjectMetadata, ProjectOptionsOverrides}; +use karva_metadata::{NoTestsMode, ProjectMetadata, ProjectOptionsOverrides}; use karva_project::Project; use karva_project::path::absolute; use karva_python_semantic::current_python_version; @@ -87,6 +87,25 @@ pub fn test(args: TestCommand) -> Result { durations, )?; + if no_tests_matched_filters(&result) { + match project.settings().test().no_tests { + NoTestsMode::Pass => return Ok(ExitStatus::Success), + NoTestsMode::Warn => { + let mut stdout = printer.stream_for_requested_summary().lock(); + writeln!(stdout, "warning: no tests matched the provided filters")?; + return Ok(ExitStatus::Success); + } + NoTestsMode::Fail => { + let mut stdout = printer.stream_for_failure_summary().lock(); + writeln!( + stdout, + "error: no tests matched the provided filters (use --no-tests=pass or --no-tests=warn)" + )?; + return Ok(ExitStatus::Failure); + } + } + } + if result.stats.is_success() && result.discovery_diagnostics.is_empty() { Ok(ExitStatus::Success) } else { @@ -94,6 +113,13 @@ pub fn test(args: TestCommand) -> Result { } } +fn no_tests_matched_filters(result: &AggregatedResults) -> bool { + result.stats.passed() == 0 + && result.stats.failed() == 0 + && result.discovery_diagnostics.is_empty() + && result.diagnostics.is_empty() +} + /// Print test output: diagnostics, durations, and result summary. pub fn print_test_output( printer: Printer, diff --git a/crates/karva/tests/it/filterset.rs b/crates/karva/tests/it/filterset.rs index 30a7b323..54a96cbe 100644 --- a/crates/karva/tests/it/filterset.rs +++ b/crates/karva/tests/it/filterset.rs @@ -10,6 +10,11 @@ def test_beta(): assert True "; +const NO_TESTS: &str = r" +def helper(): + pass +"; + #[test] fn filterset_test_substring() { let context = TestContext::with_file("test.py", TWO_TESTS); @@ -74,20 +79,52 @@ fn filterset_test_multiple_flags_or_semantics() { #[test] fn filterset_test_no_matches() { - let context = TestContext::with_file("test.py", TWO_TESTS); - assert_cmd_snapshot!(context.command_no_parallel().arg("-E").arg("test(~nonexistent)"), @" + let context = TestContext::with_file("test.py", NO_TESTS); + assert_cmd_snapshot!(context.command_no_parallel(), @" + success: false + exit_code: 1 + ----- stdout ----- + ──────────── + Summary [TIME] 0 tests run: 0 passed, 0 skipped + error: no tests matched the provided filters (use --no-tests=pass or --no-tests=warn) + + ----- stderr ----- + "); +} + +#[test] +fn filterset_test_no_matches_pass() { + let context = TestContext::with_file("test.py", NO_TESTS); + assert_cmd_snapshot!( + context.command_no_parallel().arg("--no-tests").arg("pass"), + @" success: true exit_code: 0 ----- stdout ----- - Starting 2 tests across 1 worker - SKIP [TIME] test::test_alpha - SKIP [TIME] test::test_beta + ──────────── + Summary [TIME] 0 tests run: 0 passed, 0 skipped + ----- stderr ----- + " + ); +} + +#[test] +fn filterset_test_no_matches_warn() { + let context = TestContext::with_file("test.py", NO_TESTS); + assert_cmd_snapshot!( + context.command_no_parallel().arg("--no-tests").arg("warn"), + @" + success: true + exit_code: 0 + ----- stdout ----- ──────────── - Summary [TIME] 2 tests run: 0 passed, 2 skipped + Summary [TIME] 0 tests run: 0 passed, 0 skipped + warning: no tests matched the provided filters ----- stderr ----- - "); + " + ); } #[test] @@ -618,8 +655,8 @@ def test_fast(): ); assert_cmd_snapshot!(context.command_no_parallel().arg("-E").arg("tag(slow)"), @" - success: true - exit_code: 0 + success: false + exit_code: 1 ----- stdout ----- Starting 2 tests across 1 worker SKIP [TIME] test::test_untagged @@ -627,6 +664,7 @@ def test_fast(): ──────────── Summary [TIME] 2 tests run: 0 passed, 2 skipped + error: no tests matched the provided filters (use --no-tests=pass or --no-tests=warn) ----- stderr ----- "); diff --git a/crates/karva_cli/src/lib.rs b/crates/karva_cli/src/lib.rs index 0cedb6f8..8db14979 100644 --- a/crates/karva_cli/src/lib.rs +++ b/crates/karva_cli/src/lib.rs @@ -5,7 +5,9 @@ use clap::Parser; use clap::builder::Styles; use clap::builder::styling::{AnsiColor, Effects}; use karva_logging::{TerminalColor, VerbosityLevel}; -use karva_metadata::{MaxFail, Options, RunIgnoredMode, SrcOptions, TerminalOptions, TestOptions}; +use karva_metadata::{ + MaxFail, NoTestsMode, Options, RunIgnoredMode, SrcOptions, TerminalOptions, TestOptions, +}; use ruff_db::diagnostic::DiagnosticFormat; const STYLES: Styles = Styles::styled() @@ -207,6 +209,10 @@ pub struct SubTestCommand { #[clap(short = 'E', long = "filter", help_heading = "Filter options")] pub filter_expressions: Vec, + /// Configure behavior when filters match no runnable tests. + #[arg(long, value_name = "WHEN", help_heading = "Filter options")] + pub no_tests: Option, + /// Run ignored tests. #[arg(long, help_heading = "Filter options")] pub run_ignored: Option, @@ -370,6 +376,7 @@ impl SubTestCommand { max_fail, try_import_fixtures: self.try_import_fixtures, retry: self.retry, + no_tests: self.no_tests.map(Into::into), }), } } @@ -408,3 +415,36 @@ impl From for RunIgnoredMode { } } } + +/// Behavior when no tests match filters. +#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, clap::ValueEnum)] +pub enum NoTests { + /// Exit successfully without extra output. + Pass, + + /// Exit successfully and print a warning. + Warn, + + /// Exit with failure status. + Fail, +} + +impl NoTests { + pub fn as_str(self) -> &'static str { + match self { + Self::Pass => "pass", + Self::Warn => "warn", + Self::Fail => "fail", + } + } +} + +impl From for NoTestsMode { + fn from(value: NoTests) -> Self { + match value { + NoTests::Pass => Self::Pass, + NoTests::Warn => Self::Warn, + NoTests::Fail => Self::Fail, + } + } +} diff --git a/crates/karva_metadata/src/lib.rs b/crates/karva_metadata/src/lib.rs index 16b34f5a..fd706fba 100644 --- a/crates/karva_metadata/src/lib.rs +++ b/crates/karva_metadata/src/lib.rs @@ -14,7 +14,7 @@ pub use options::{ Options, OutputFormat, ProjectOptionsOverrides, SrcOptions, TerminalOptions, TestOptions, }; pub use pyproject::{PyProject, PyProjectError}; -pub use settings::{ProjectSettings, RunIgnoredMode}; +pub use settings::{NoTestsMode, ProjectSettings, RunIgnoredMode}; use crate::options::KarvaTomlError; diff --git a/crates/karva_metadata/src/options.rs b/crates/karva_metadata/src/options.rs index bec623a9..f422d6c8 100644 --- a/crates/karva_metadata/src/options.rs +++ b/crates/karva_metadata/src/options.rs @@ -8,7 +8,7 @@ use thiserror::Error; use crate::filter::FiltersetSet; use crate::max_fail::MaxFail; use crate::settings::{ - ProjectSettings, RunIgnoredMode, SrcSettings, TerminalSettings, TestSettings, + NoTestsMode, ProjectSettings, RunIgnoredMode, SrcSettings, TerminalSettings, TestSettings, }; #[derive( @@ -215,6 +215,19 @@ pub struct TestOptions { "# )] pub retry: Option, + + /// Configures behavior when filters match no runnable tests. + /// + /// Defaults to `fail`. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"fail"#, + value_type = "pass | warn | fail", + example = r#" + no-tests = "warn" + "# + )] + pub no_tests: Option, } impl TestOptions { @@ -234,6 +247,7 @@ impl TestOptions { retry: self.retry.unwrap_or_default(), filter: FiltersetSet::default(), run_ignored: RunIgnoredMode::default(), + no_tests: self.no_tests.unwrap_or_default(), } } } diff --git a/crates/karva_metadata/src/settings.rs b/crates/karva_metadata/src/settings.rs index d6c726d8..0ad84fff 100644 --- a/crates/karva_metadata/src/settings.rs +++ b/crates/karva_metadata/src/settings.rs @@ -1,3 +1,6 @@ +use karva_combine::Combine; +use serde::{Deserialize, Serialize}; + use crate::filter::FiltersetSet; use crate::max_fail::MaxFail; use crate::options::OutputFormat; @@ -10,6 +13,25 @@ pub enum RunIgnoredMode { All, } +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub enum NoTestsMode { + Pass, + Warn, + #[default] + Fail, +} + +impl Combine for NoTestsMode { + #[inline(always)] + fn combine_with(&mut self, _other: Self) {} + + #[inline] + fn combine(self, _other: Self) -> Self { + self + } +} + #[derive(Default, Debug, Clone)] pub struct ProjectSettings { pub(crate) terminal: TerminalSettings, @@ -63,4 +85,5 @@ pub struct TestSettings { pub retry: u32, pub filter: FiltersetSet, pub run_ignored: RunIgnoredMode, + pub no_tests: NoTestsMode, } diff --git a/docs/cli.md b/docs/cli.md index 0c5e00ad..ef1ebfcf 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -65,7 +65,13 @@ karva test [OPTIONS] [PATH]...
--no-ignore

When set, .gitignore files will not be respected

--no-parallel

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

--no-progress

When set, we will not show individual test case results during execution

-
--num-workers, -n num-workers

Number of parallel workers (default: number of CPU cores)

+
--no-tests when

Configure behavior when filters match no runnable tests

+

Possible values:

+
    +
  • pass: Exit successfully without extra output
  • +
  • warn: Exit successfully and print a warning
  • +
  • fail: Exit with failure status
  • +
--num-workers, -n num-workers

Number of parallel workers (default: number of CPU cores)

--output-format output-format

The format to use for printing diagnostic messages

Possible values:

    diff --git a/docs/configuration.md b/docs/configuration.md index 8ac92acc..e640eca6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -133,6 +133,25 @@ max-fail = 3 --- +### `no-tests` + +Configures behavior when filters match no runnable tests. + +Defaults to `fail`. + +**Default value**: `fail` + +**Type**: `pass | warn | fail` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.karva.test] +no-tests = "warn" +``` + +--- + ### `retry` When set, we will retry failed tests up to this number of times. From 245971cddcf3324139b70df52de001952e3ecb85 Mon Sep 17 00:00:00 2001 From: omchillure Date: Fri, 17 Apr 2026 23:56:00 +0530 Subject: [PATCH 2/6] Fix regression --- .../run-1776445309804/worker-0/durations.json | 2 +- .../run-1776445309804/worker-0/stats.json | 2 +- .../run-1776445492867/worker-0/durations.json | 2 +- .../run-1776445492867/worker-0/stats.json | 2 +- crates/karva/src/commands/test/mod.rs | 3 +-- crates/karva/tests/it/basic.rs | 15 +++++++++------ crates/karva/tests/it/filterset.rs | 5 ++--- crates/karva_metadata/src/options.rs | 5 ++++- 8 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.karva_cache/run-1776445309804/worker-0/durations.json b/.karva_cache/run-1776445309804/worker-0/durations.json index f16de801..678ba5b7 100644 --- a/.karva_cache/run-1776445309804/worker-0/durations.json +++ b/.karva_cache/run-1776445309804/worker-0/durations.json @@ -7,4 +7,4 @@ "secs": 0, "nanos": 8926 } -} \ No newline at end of file +} diff --git a/.karva_cache/run-1776445309804/worker-0/stats.json b/.karva_cache/run-1776445309804/worker-0/stats.json index 76c0d3a8..539321fe 100644 --- a/.karva_cache/run-1776445309804/worker-0/stats.json +++ b/.karva_cache/run-1776445309804/worker-0/stats.json @@ -1,3 +1,3 @@ { "passed": 2 -} \ No newline at end of file +} diff --git a/.karva_cache/run-1776445492867/worker-0/durations.json b/.karva_cache/run-1776445492867/worker-0/durations.json index 6e148a4f..4fd88bec 100644 --- a/.karva_cache/run-1776445492867/worker-0/durations.json +++ b/.karva_cache/run-1776445492867/worker-0/durations.json @@ -7,4 +7,4 @@ "secs": 0, "nanos": 9898 } -} \ No newline at end of file +} diff --git a/.karva_cache/run-1776445492867/worker-0/stats.json b/.karva_cache/run-1776445492867/worker-0/stats.json index 76c0d3a8..539321fe 100644 --- a/.karva_cache/run-1776445492867/worker-0/stats.json +++ b/.karva_cache/run-1776445492867/worker-0/stats.json @@ -1,3 +1,3 @@ { "passed": 2 -} \ No newline at end of file +} diff --git a/crates/karva/src/commands/test/mod.rs b/crates/karva/src/commands/test/mod.rs index 968c657d..036dda50 100644 --- a/crates/karva/src/commands/test/mod.rs +++ b/crates/karva/src/commands/test/mod.rs @@ -114,8 +114,7 @@ pub fn test(args: TestCommand) -> Result { } fn no_tests_matched_filters(result: &AggregatedResults) -> bool { - result.stats.passed() == 0 - && result.stats.failed() == 0 + result.stats.total() == 0 && result.discovery_diagnostics.is_empty() && result.diagnostics.is_empty() } diff --git a/crates/karva/tests/it/basic.rs b/crates/karva/tests/it/basic.rs index 2193647e..275b7587 100644 --- a/crates/karva/tests/it/basic.rs +++ b/crates/karva/tests/it/basic.rs @@ -39,11 +39,12 @@ fn test_empty_file() { let context = TestContext::with_file("test.py", ""); assert_cmd_snapshot!(context.command(), @" - success: true - exit_code: 0 + success: false + exit_code: 1 ----- stdout ----- ──────────── Summary [TIME] 0 tests run: 0 passed, 0 skipped + error: no tests matched the provided filters (use --no-tests=pass or --no-tests=warn) ----- stderr ----- "); @@ -54,11 +55,12 @@ fn test_empty_directory() { let context = TestContext::new(); assert_cmd_snapshot!(context.command(), @" - success: true - exit_code: 0 + success: false + exit_code: 1 ----- stdout ----- ──────────── Summary [TIME] 0 tests run: 0 passed, 0 skipped + error: no tests matched the provided filters (use --no-tests=pass or --no-tests=warn) ----- stderr ----- "); @@ -140,11 +142,12 @@ fn test_no_tests_found() { let context = TestContext::with_file("test_no_tests.py", r""); assert_cmd_snapshot!(context.command(), @" - success: true - exit_code: 0 + success: false + exit_code: 1 ----- stdout ----- ──────────── Summary [TIME] 0 tests run: 0 passed, 0 skipped + error: no tests matched the provided filters (use --no-tests=pass or --no-tests=warn) ----- stderr ----- "); diff --git a/crates/karva/tests/it/filterset.rs b/crates/karva/tests/it/filterset.rs index 54a96cbe..2537aabd 100644 --- a/crates/karva/tests/it/filterset.rs +++ b/crates/karva/tests/it/filterset.rs @@ -655,8 +655,8 @@ def test_fast(): ); assert_cmd_snapshot!(context.command_no_parallel().arg("-E").arg("tag(slow)"), @" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- Starting 2 tests across 1 worker SKIP [TIME] test::test_untagged @@ -664,7 +664,6 @@ def test_fast(): ──────────── Summary [TIME] 2 tests run: 0 passed, 2 skipped - error: no tests matched the provided filters (use --no-tests=pass or --no-tests=warn) ----- stderr ----- "); diff --git a/crates/karva_metadata/src/options.rs b/crates/karva_metadata/src/options.rs index f422d6c8..22b7a20f 100644 --- a/crates/karva_metadata/src/options.rs +++ b/crates/karva_metadata/src/options.rs @@ -378,7 +378,7 @@ nonsense = 42 | 4 | nonsense = 42 | ^^^^^^^^ - unknown field `nonsense`, expected one of `test-function-prefix`, `fail-fast`, `max-fail`, `try-import-fixtures`, `retry` + unknown field `nonsense`, expected one of `test-function-prefix`, `fail-fast`, `max-fail`, `try-import-fixtures`, `retry`, `no-tests` " ); } @@ -458,6 +458,7 @@ max-fail = 0 retry: Some( 5, ), + no_tests: None, } "#); } @@ -484,6 +485,7 @@ max-fail = 0 retry: Some( 3, ), + no_tests: None, } "#); } @@ -545,6 +547,7 @@ max-fail = 0 retry: Some( 2, ), + no_tests: None, }, ) "#); From f89a7c25c821ee2c0278690355f437441cea7f2e Mon Sep 17 00:00:00 2001 From: omchillure Date: Sat, 18 Apr 2026 00:04:35 +0530 Subject: [PATCH 3/6] remove .karva cache to fix ci check --- .karva_cache/last-failed.json | 1 - .karva_cache/run-1776445309804/worker-0/durations.json | 10 ---------- .karva_cache/run-1776445309804/worker-0/stats.json | 3 --- .karva_cache/run-1776445492867/worker-0/durations.json | 10 ---------- .karva_cache/run-1776445492867/worker-0/stats.json | 3 --- 5 files changed, 27 deletions(-) delete mode 100644 .karva_cache/last-failed.json delete mode 100644 .karva_cache/run-1776445309804/worker-0/durations.json delete mode 100644 .karva_cache/run-1776445309804/worker-0/stats.json delete mode 100644 .karva_cache/run-1776445492867/worker-0/durations.json delete mode 100644 .karva_cache/run-1776445492867/worker-0/stats.json diff --git a/.karva_cache/last-failed.json b/.karva_cache/last-failed.json deleted file mode 100644 index 0637a088..00000000 --- a/.karva_cache/last-failed.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/.karva_cache/run-1776445309804/worker-0/durations.json b/.karva_cache/run-1776445309804/worker-0/durations.json deleted file mode 100644 index 678ba5b7..00000000 --- a/.karva_cache/run-1776445309804/worker-0/durations.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "test.test_no_tests::test_fast": { - "secs": 0, - "nanos": 66580 - }, - "test.test_no_tests::test_untagged": { - "secs": 0, - "nanos": 8926 - } -} diff --git a/.karva_cache/run-1776445309804/worker-0/stats.json b/.karva_cache/run-1776445309804/worker-0/stats.json deleted file mode 100644 index 539321fe..00000000 --- a/.karva_cache/run-1776445309804/worker-0/stats.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "passed": 2 -} diff --git a/.karva_cache/run-1776445492867/worker-0/durations.json b/.karva_cache/run-1776445492867/worker-0/durations.json deleted file mode 100644 index 4fd88bec..00000000 --- a/.karva_cache/run-1776445492867/worker-0/durations.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "test.test_no_tests::test_fast": { - "secs": 0, - "nanos": 83744 - }, - "test.test_no_tests::test_untagged": { - "secs": 0, - "nanos": 9898 - } -} diff --git a/.karva_cache/run-1776445492867/worker-0/stats.json b/.karva_cache/run-1776445492867/worker-0/stats.json deleted file mode 100644 index 539321fe..00000000 --- a/.karva_cache/run-1776445492867/worker-0/stats.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "passed": 2 -} From 2f9087874127165a8002024212cad694f408fa8d Mon Sep 17 00:00:00 2001 From: omchillure Date: Sat, 18 Apr 2026 09:28:13 +0530 Subject: [PATCH 4/6] fix --- crates/karva/src/commands/test/mod.rs | 12 ++++++++++++ crates/karva/tests/it/filterset.rs | 17 +++++++++++++++++ crates/karva_cli/src/lib.rs | 20 +++++++++++++++----- crates/karva_metadata/src/options.rs | 10 ++++++---- crates/karva_metadata/src/settings.rs | 3 ++- docs/cli.md | 12 +++++++----- docs/configuration.md | 10 ++++++---- 7 files changed, 65 insertions(+), 19 deletions(-) diff --git a/crates/karva/src/commands/test/mod.rs b/crates/karva/src/commands/test/mod.rs index 036dda50..5c492674 100644 --- a/crates/karva/src/commands/test/mod.rs +++ b/crates/karva/src/commands/test/mod.rs @@ -88,7 +88,19 @@ pub fn test(args: TestCommand) -> Result { )?; if no_tests_matched_filters(&result) { + let has_filters = !sub_command.filter_expressions.is_empty(); match project.settings().test().no_tests { + NoTestsMode::Auto => { + if has_filters { + return Ok(ExitStatus::Success); + } + let mut stdout = printer.stream_for_failure_summary().lock(); + writeln!( + stdout, + "error: no tests matched the provided filters (use --no-tests=pass or --no-tests=warn)" + )?; + return Ok(ExitStatus::Failure); + } NoTestsMode::Pass => return Ok(ExitStatus::Success), NoTestsMode::Warn => { let mut stdout = printer.stream_for_requested_summary().lock(); diff --git a/crates/karva/tests/it/filterset.rs b/crates/karva/tests/it/filterset.rs index 2537aabd..4d534d76 100644 --- a/crates/karva/tests/it/filterset.rs +++ b/crates/karva/tests/it/filterset.rs @@ -92,6 +92,23 @@ fn filterset_test_no_matches() { "); } +#[test] +fn filterset_test_no_matches_auto_with_filter() { + let context = TestContext::with_file("test.py", NO_TESTS); + assert_cmd_snapshot!( + context.command_no_parallel().arg("-E").arg("test(~helper)"), + @" + success: true + exit_code: 0 + ----- stdout ----- + ──────────── + Summary [TIME] 0 tests run: 0 passed, 0 skipped + + ----- stderr ----- + " + ); +} + #[test] fn filterset_test_no_matches_pass() { let context = TestContext::with_file("test.py", NO_TESTS); diff --git a/crates/karva_cli/src/lib.rs b/crates/karva_cli/src/lib.rs index 8db14979..98618d63 100644 --- a/crates/karva_cli/src/lib.rs +++ b/crates/karva_cli/src/lib.rs @@ -209,8 +209,12 @@ pub struct SubTestCommand { #[clap(short = 'E', long = "filter", help_heading = "Filter options")] pub filter_expressions: Vec, - /// Configure behavior when filters match no runnable tests. - #[arg(long, value_name = "WHEN", help_heading = "Filter options")] + /// Behavior when no tests are found to run [default: auto] + /// + /// `auto` fails if no filter expressions were given, and passes silently + /// if filters were given (the filter may legitimately match nothing on + /// some platforms or configurations). + #[arg(long, value_name = "ACTION", env = "KARVA_NO_TESTS", help_heading = "Filter options")] pub no_tests: Option, /// Run ignored tests. @@ -419,19 +423,24 @@ impl From for RunIgnoredMode { /// Behavior when no tests match filters. #[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, clap::ValueEnum)] pub enum NoTests { - /// Exit successfully without extra output. + /// Automatically determine behavior: fail if no filter expressions were + /// given, pass silently if filters were given. + Auto, + + /// Silently exit with code 0. Pass, - /// Exit successfully and print a warning. + /// Produce a warning and exit with code 0. Warn, - /// Exit with failure status. + /// Produce an error message and exit with a non-zero code. Fail, } impl NoTests { pub fn as_str(self) -> &'static str { match self { + Self::Auto => "auto", Self::Pass => "pass", Self::Warn => "warn", Self::Fail => "fail", @@ -442,6 +451,7 @@ impl NoTests { impl From for NoTestsMode { fn from(value: NoTests) -> Self { match value { + NoTests::Auto => Self::Auto, NoTests::Pass => Self::Pass, NoTests::Warn => Self::Warn, NoTests::Fail => Self::Fail, diff --git a/crates/karva_metadata/src/options.rs b/crates/karva_metadata/src/options.rs index 22b7a20f..e941fc53 100644 --- a/crates/karva_metadata/src/options.rs +++ b/crates/karva_metadata/src/options.rs @@ -216,13 +216,15 @@ pub struct TestOptions { )] pub retry: Option, - /// Configures behavior when filters match no runnable tests. + /// Configures behavior when no tests are found to run. /// - /// Defaults to `fail`. + /// `auto` (the default) fails when no filter expressions were given, and + /// passes silently when filters were given. Use `fail` to always fail, + /// `warn` to always warn, or `pass` to always succeed silently. #[serde(skip_serializing_if = "Option::is_none")] #[option( - default = r#"fail"#, - value_type = "pass | warn | fail", + default = r#"auto"#, + value_type = "auto | pass | warn | fail", example = r#" no-tests = "warn" "# diff --git a/crates/karva_metadata/src/settings.rs b/crates/karva_metadata/src/settings.rs index 0ad84fff..ae4b2cb0 100644 --- a/crates/karva_metadata/src/settings.rs +++ b/crates/karva_metadata/src/settings.rs @@ -16,9 +16,10 @@ pub enum RunIgnoredMode { #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum NoTestsMode { + #[default] + Auto, Pass, Warn, - #[default] Fail, } diff --git a/docs/cli.md b/docs/cli.md index ef1ebfcf..894dfdd7 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -65,12 +65,14 @@ karva test [OPTIONS] [PATH]...
--no-ignore

When set, .gitignore files will not be respected

--no-parallel

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

--no-progress

When set, we will not show individual test case results during execution

-
--no-tests when

Configure behavior when filters match no runnable tests

-

Possible values:

+
--no-tests action

Behavior when no tests are found to run.

+

auto (the default) fails if no filter expressions were given, and passes silently if filters were given (the filter may legitimately match nothing on some platforms or configurations).

+

May also be set with the KARVA_NO_TESTS environment variable.

Possible values:

    -
  • pass: Exit successfully without extra output
  • -
  • warn: Exit successfully and print a warning
  • -
  • fail: Exit with failure status
  • +
  • auto: Automatically determine behavior: fail if no filter expressions were given, pass silently if filters were given
  • +
  • pass: Silently exit with code 0
  • +
  • warn: Produce a warning and exit with code 0
  • +
  • fail: Produce an error message and exit with a non-zero code
--num-workers, -n num-workers

Number of parallel workers (default: number of CPU cores)

--output-format output-format

The format to use for printing diagnostic messages

Possible values:

diff --git a/docs/configuration.md b/docs/configuration.md index e640eca6..5f91d4ff 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -135,13 +135,15 @@ max-fail = 3 ### `no-tests` -Configures behavior when filters match no runnable tests. +Configures behavior when no tests are found to run. -Defaults to `fail`. +`auto` (the default) fails when no filter expressions were given, and +passes silently when filters were given. Use `fail` to always fail, +`warn` to always warn, or `pass` to always succeed silently. -**Default value**: `fail` +**Default value**: `auto` -**Type**: `pass | warn | fail` +**Type**: `auto | pass | warn | fail` **Example usage** (`pyproject.toml`): From 82c96ad5f3435a0e81b019fd7a0d4add49eac794 Mon Sep 17 00:00:00 2001 From: omchillure Date: Sat, 18 Apr 2026 09:34:30 +0530 Subject: [PATCH 5/6] fix cli --- docs/cli.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 894dfdd7..59c6f03c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -65,8 +65,8 @@ karva test [OPTIONS] [PATH]...
--no-ignore

When set, .gitignore files will not be respected

--no-parallel

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

--no-progress

When set, we will not show individual test case results during execution

-
--no-tests action

Behavior when no tests are found to run.

-

auto (the default) fails if no filter expressions were given, and passes silently if filters were given (the filter may legitimately match nothing on some platforms or configurations).

+
--no-tests action

Behavior when no tests are found to run [default: auto]

+

auto fails if no filter expressions were given, and passes silently if filters were given (the filter may legitimately match nothing on some platforms or configurations).

May also be set with the KARVA_NO_TESTS environment variable.

Possible values:

  • auto: Automatically determine behavior: fail if no filter expressions were given, pass silently if filters were given
  • From 3e62a94bffd51a13e6e068f13cb90caa6749928d Mon Sep 17 00:00:00 2001 From: omchillure Date: Sat, 18 Apr 2026 15:11:40 +0530 Subject: [PATCH 6/6] format fix --- crates/karva_cli/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/karva_cli/src/lib.rs b/crates/karva_cli/src/lib.rs index 98618d63..b045fc53 100644 --- a/crates/karva_cli/src/lib.rs +++ b/crates/karva_cli/src/lib.rs @@ -214,7 +214,12 @@ pub struct SubTestCommand { /// `auto` fails if no filter expressions were given, and passes silently /// if filters were given (the filter may legitimately match nothing on /// some platforms or configurations). - #[arg(long, value_name = "ACTION", env = "KARVA_NO_TESTS", help_heading = "Filter options")] + #[arg( + long, + value_name = "ACTION", + env = "KARVA_NO_TESTS", + help_heading = "Filter options" + )] pub no_tests: Option, /// Run ignored tests.