Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 5 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ config = { version = "0.15.13", default-features = false, features = [
"toml",
"preserve_order",
] }
chrono = "0.4.41"
chrono = { version = "0.4.41", features = ["serde"] }
clap = { version = "4.5.41", features = ["derive", "unstable-markdown"] }
console-subscriber = "0.4.1"
cp_r = "0.5.2"
Expand Down
1 change: 1 addition & 0 deletions cargo-nextest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ rust-version.workspace = true
[dependencies]
camino.workspace = true
cfg-if.workspace = true
chrono.workspace = true
clap = { workspace = true, features = ["derive", "env", "unicode", "wrap_help"] }
color-eyre.workspace = true
dialoguer.workspace = true
Expand Down
189 changes: 182 additions & 7 deletions cargo-nextest/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
redact::Redactor,
reporter::{
FinalStatusLevel, ReporterBuilder, StatusLevel, TestOutputDisplay, TestOutputErrorSlice,
events::{FinalRunStats, RunStatsFailureKind},
events::{FinalRunStats, RunStatsFailureKind, TestEventKind},
highlight_end, structured,
},
reuse_build::{ArchiveReporter, PathMapper, ReuseBuildInfo, archive_to_file},
Expand All @@ -54,7 +54,7 @@
env::VarError,
fmt,
io::{Cursor, Write},
sync::{Arc, OnceLock},
sync::{Arc, Mutex, OnceLock},
};
use swrite::{SWrite, swrite};
use tracing::{Level, debug, info, warn};
Expand Down Expand Up @@ -587,6 +587,18 @@
#[arg(long)]
ignore_default_filter: bool,

/// Only run tests that failed in the last run
#[arg(long, visible_alias = "lf", conflicts_with_all = ["failed_last", "clear_failed"])]
last_failed: bool,

/// Run failed tests first, then other tests
#[arg(long, visible_alias = "fl", conflicts_with_all = ["last_failed", "clear_failed"])]
failed_last: bool,

/// Clear the list of failed tests without running tests
#[arg(long, conflicts_with_all = ["last_failed", "failed_last"])]
clear_failed: bool,

/// Test name filters.
#[arg(help_heading = None, name = "FILTERS")]
pre_double_dash_filters: Vec<String>,
Expand Down Expand Up @@ -648,12 +660,83 @@
.map_err(|err| ExpectedError::CreateTestListError { err })
}

fn make_test_filter_builder(&self, filter_exprs: Vec<Filterset>) -> Result<TestFilterBuilder> {
fn make_test_filter_builder(
&self,
filter_exprs: Vec<Filterset>,
profile_name: &str,
profile: &EarlyProfile<'_>,
) -> Result<TestFilterBuilder> {
// Merge the test binary args into the patterns.
let mut run_ignored = self.run_ignored.map(Into::into);
let mut patterns = TestFilterPatterns::new(self.pre_double_dash_filters.clone());
self.merge_test_binary_args(&mut run_ignored, &mut patterns)?;

// Handle --last-failed and --failed-last options
if self.last_failed || self.failed_last {
use nextest_runner::reporter::last_failed::FailedTestStore;

let store = FailedTestStore::new(profile.store_dir(), profile_name);
match store.load() {
Ok(Some(snapshot)) => {
if snapshot.failed_tests.is_empty() {
eprintln!(
"No failed tests found from previous run for profile '{}'",

Check warning on line 683 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L682-L683

Added lines #L682 - L683 were not covered by tests
profile_name
);
if self.last_failed {
// For --last-failed with no failed tests, we should run no tests
// Create a pattern that matches nothing
patterns = TestFilterPatterns::default();
patterns.add_exact_pattern(
"__nextest_internal_no_tests_to_run__".to_string(),
);
}

Check warning on line 693 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L686-L693

Added lines #L686 - L693 were not covered by tests
// For --failed-last, we continue with the normal filtering
} else {
if self.last_failed {
eprintln!("Running only tests that failed in the last run");
} else {
eprintln!(
"Found {} failed test(s) from previous run",
snapshot.failed_tests.len()
);
}

if self.last_failed {
// Only run failed tests - replace all patterns
patterns = TestFilterPatterns::default();
for failed_test in &snapshot.failed_tests {
// Add exact pattern for each failed test
patterns.add_exact_pattern(failed_test.test_name.clone());
}
} else {
// --failed-last: prioritize failed tests
// This will be handled in the test runner by sorting tests
// For now, we pass the failed tests information through some mechanism
// TODO: Add a way to pass failed test info to the runner for prioritization
}
}
}
Ok(None) => {
if self.last_failed {
eprintln!("No failed tests found from previous run");
} else {
eprintln!("No previous test run found for profile '{}'", profile_name);
}

Check warning on line 725 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L724-L725

Added lines #L724 - L725 were not covered by tests
if self.last_failed {
// For --last-failed with no history, run no tests
patterns = TestFilterPatterns::default();
patterns
.add_exact_pattern("__nextest_internal_no_tests_to_run__".to_string());
}
}
Err(err) => {
eprintln!("Warning: Failed to load test history: {}", err);
// Continue with normal filtering on error
}

Check warning on line 736 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L733-L736

Added lines #L733 - L736 were not covered by tests
}
}

Ok(TestFilterBuilder::new(
run_ignored.unwrap_or_default(),
self.partition.clone(),
Expand Down Expand Up @@ -1649,8 +1732,17 @@

let (version_only_config, config) = self.base.load_config(&pcx)?;
let profile = self.base.load_profile(&config)?;
let profile_name = self.base.config_opts.profile.as_deref().unwrap_or_else(|| {
if std::env::var_os("MIRI_SYSROOT").is_some() {
NextestConfig::DEFAULT_MIRI_PROFILE

Check warning on line 1737 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L1737

Added line #L1737 was not covered by tests
} else {
NextestConfig::DEFAULT_PROFILE
}
});
let filter_exprs = self.build_filtering_expressions(&pcx)?;
let test_filter_builder = self.build_filter.make_test_filter_builder(filter_exprs)?;
let test_filter_builder =
self.build_filter
.make_test_filter_builder(filter_exprs, profile_name, &profile)?;

let binary_list = self.base.build_binary_list()?;

Expand Down Expand Up @@ -1710,6 +1802,13 @@
let pcx = ParseContext::new(self.base.graph());
let (_, config) = self.base.load_config(&pcx)?;
let profile = self.base.load_profile(&config)?;
let profile_name = self.base.config_opts.profile.as_deref().unwrap_or_else(|| {
if std::env::var_os("MIRI_SYSROOT").is_some() {
NextestConfig::DEFAULT_MIRI_PROFILE

Check warning on line 1807 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L1807

Added line #L1807 was not covered by tests
} else {
NextestConfig::DEFAULT_PROFILE
}
});

// Validate test groups before doing any other work.
let mode = if groups.is_empty() {
Expand All @@ -1721,7 +1820,9 @@
let settings = ShowTestGroupSettings { mode, show_default };

let filter_exprs = self.build_filtering_expressions(&pcx)?;
let test_filter_builder = self.build_filter.make_test_filter_builder(filter_exprs)?;
let test_filter_builder =
self.build_filter
.make_test_filter_builder(filter_exprs, profile_name, &profile)?;

let binary_list = self.base.build_binary_list()?;
let build_platforms = binary_list.rust_build_meta.build_platforms.clone();
Expand Down Expand Up @@ -1765,6 +1866,26 @@
let pcx = ParseContext::new(self.base.graph());
let (version_only_config, config) = self.base.load_config(&pcx)?;
let profile = self.base.load_profile(&config)?;
let profile_name = self.base.config_opts.profile.as_deref().unwrap_or_else(|| {
if std::env::var_os("MIRI_SYSROOT").is_some() {
NextestConfig::DEFAULT_MIRI_PROFILE

Check warning on line 1871 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L1871

Added line #L1871 was not covered by tests
} else {
NextestConfig::DEFAULT_PROFILE
}
});

// Handle clearing failed tests early if requested
if self.build_filter.clear_failed {
use nextest_runner::reporter::last_failed::FailedTestStore;
let store = FailedTestStore::new(profile.store_dir(), profile_name);
store
.clear()
.map_err(|err| ExpectedError::ClearFailedTestsError {
error: err.to_string(),
})?;

Check warning on line 1885 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L1884-L1885

Added lines #L1884 - L1885 were not covered by tests
eprintln!("Cleared failed test history");
return Ok(0);
}

// Construct this here so that errors are reported before the build step.
let mut structured_reporter = structured::StructuredReporter::new();
Expand Down Expand Up @@ -1818,7 +1939,9 @@
reporter_builder.set_verbose(self.base.output.verbose);

let filter_exprs = self.build_filtering_expressions(&pcx)?;
let test_filter_builder = self.build_filter.make_test_filter_builder(filter_exprs)?;
let test_filter_builder =
self.build_filter
.make_test_filter_builder(filter_exprs, profile_name, &profile)?;

let binary_list = self.base.build_binary_list()?;
let build_platforms = &binary_list.rust_build_meta.build_platforms.clone();
Expand Down Expand Up @@ -1870,11 +1993,51 @@
);

configure_handle_inheritance(no_capture)?;

// Track failed tests during the run
use nextest_runner::reporter::last_failed::{
FailedTest, FailedTestStore, FailedTestsSnapshot,
};
let failed_tests = Arc::new(Mutex::new(Vec::<FailedTest>::new()));
let failed_tests_for_callback = Arc::clone(&failed_tests);

let run_stats = runner.try_execute(|event| {
// Track failed tests for persistence
if let TestEventKind::TestFinished {
test_instance,
run_statuses,
..
} = &event.kind
{
if !run_statuses.last_status().result.is_success() {
let mut failed = failed_tests_for_callback.lock().unwrap();
failed.push(FailedTest::from_test_instance_id(test_instance.id()));
}
}

// Write and flush the event.
reporter.report_event(event)
})?;
reporter.finish();

// After the run completes, persist failed tests if we're not in no-run mode
if !runner_opts.no_run {
let store = FailedTestStore::new(profile.store_dir(), profile_name);

let failed = failed_tests.lock().unwrap();
let snapshot = FailedTestsSnapshot {
version: 1,
created_at: chrono::Utc::now(),
profile_name: profile_name.to_owned(),
failed_tests: failed.iter().cloned().collect(),
};

if let Err(err) = store.save(&snapshot) {
eprintln!("Warning: Failed to save failed test history: {}", err);
// Don't fail the entire test run if we can't save the history

Check warning on line 2037 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L2036-L2037

Added lines #L2036 - L2037 were not covered by tests
}
}

Check warning on line 2039 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L2039

Added line #L2039 was not covered by tests

self.base
.check_version_config_final(version_only_config.nextest_version())?;

Expand Down Expand Up @@ -2734,7 +2897,19 @@
fn get_test_filter_builder(cmd: &str) -> Result<TestFilterBuilder> {
let app = TestCli::try_parse_from(shell_words::split(cmd).expect("valid command line"))
.unwrap_or_else(|_| panic!("{cmd} should have successfully parsed"));
app.build_filter.make_test_filter_builder(vec![])
// For tests, skip the failed test loading functionality
let mut run_ignored = app.build_filter.run_ignored.map(Into::into);
let mut patterns =
TestFilterPatterns::new(app.build_filter.pre_double_dash_filters.clone());
app.build_filter
.merge_test_binary_args(&mut run_ignored, &mut patterns)?;

Ok(TestFilterBuilder::new(
run_ignored.unwrap_or_default(),
app.build_filter.partition.clone(),
patterns,
vec![],
)?)

Check warning on line 2912 in cargo-nextest/src/dispatch.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/dispatch.rs#L2912

Added line #L2912 was not covered by tests
}

let valid = &[
Expand Down
9 changes: 8 additions & 1 deletion cargo-nextest/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@
#[source]
err: std::io::Error,
},
#[error("failed to clear failed test history")]
ClearFailedTestsError { error: String },
}

impl ExpectedError {
Expand Down Expand Up @@ -433,7 +435,8 @@
| Self::SignalHandlerSetupError { .. }
| Self::ShowTestGroupsError { .. }
| Self::InvalidMessageFormatVersion { .. }
| Self::DebugExtractReadError { .. } => NextestExitCode::SETUP_ERROR,
| Self::DebugExtractReadError { .. }
| Self::ClearFailedTestsError { .. } => NextestExitCode::SETUP_ERROR,
Self::ConfigParseError { err } => {
// Experimental features not being enabled are their own error.
match err.kind() {
Expand Down Expand Up @@ -985,6 +988,10 @@
error!("error writing {format} output");
Some(err as &dyn Error)
}
Self::ClearFailedTestsError { error } => {
error!("failed to clear failed test history: {}", error);
None

Check warning on line 993 in cargo-nextest/src/errors.rs

View check run for this annotation

Codecov / codecov/patch

cargo-nextest/src/errors.rs#L991-L993

Added lines #L991 - L993 were not covered by tests
}
};

while let Some(err) = next_error {
Expand Down
Loading
Loading