diff --git a/.vscode/settings.json b/.vscode/settings.json index e39ca7f..e7ed8d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "debouncer", "filetime", "indexmap", + "inotify", "itertools", "mapref", "npath", diff --git a/src/args.rs b/src/args.rs index fbde674..e0be593 100644 --- a/src/args.rs +++ b/src/args.rs @@ -43,6 +43,17 @@ pub struct Args { #[clap(long = "ignore-missing", short = 'i')] pub ignore_missing: bool, + /// Keeps building files even if an error has occurred. + /// + /// Normally, whenever an error occurs, further rules are forbidden + /// to execute, although currently executing rules continue running. + /// + /// This makes it so that whenever an error occurs, + /// we continue searching and executing rules until there is nothing + /// else we can do + #[clap(long = "keep-going")] + pub keep_going: bool, + /// Watch for file changes and rebuild any necessary targets. /// /// WARNING: If the log file is situated in the same directory as any watched @@ -71,6 +82,7 @@ impl Default for Args { zbuild_path: None, jobs: None, ignore_missing: false, + keep_going: false, watch: false, watcher_debouncer_timeout_ms: None, log_file: None, diff --git a/src/lib.rs b/src/lib.rs index 4203e06..6ffd02e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -144,8 +144,9 @@ pub async fn run(args: Args) -> Result<(), AppError> { ); // Create the builder - // Note: We should stop builds on the first error if we're *not* watching. - let builder = Builder::new(jobs, rules, expander, !args.watch) + // Note: We should stop builds on the first error if we're *not* watching and the + // user doesn't want to keep going. + let builder = Builder::new(jobs, rules, expander, !args.watch && !args.keep_going) .context("Unable to create builder") .map_err(AppError::Other)?; let builder = Arc::new(builder); diff --git a/tests/keep_going.rs b/tests/keep_going.rs new file mode 100644 index 0000000..171d6fc --- /dev/null +++ b/tests/keep_going.rs @@ -0,0 +1,125 @@ +// Features +#![feature(must_not_suspend, strict_provenance)] +// Lints +#![expect(clippy::tests_outside_test_module, reason = "We're an integration test")] + +// Modules +mod util; + +// Imports +use { + anyhow::Context, + std::fs, + tempdir::TempDir, + zbuild::{Args, ExitResult}, +}; + +/// Test for `--keep-going` +#[tokio::test] +#[tracing_test::traced_test] +async fn keep_going() -> ExitResult { + self::inner(true).await.context("Unable to test with `--keep-going`")?; + self::inner(false) + .await + .context("Unable to test without `--keep-going`")?; + + ExitResult::Ok +} + +/// Inner function to test +/// +/// This works by having the following tree: +/// +/// ```no_compile +/// A -> B +/// \-> C1 -> C2 +/// ``` +/// +/// Where `B` is always going to fail, after 200ms, to allow all other targets +/// to start running. +/// +/// We make `C2` take a long time, to ensure `B` is executed (and fails) +/// before it can return. +/// +/// When testing with `keep_going = true`, we ensure that `C1` is still built, +/// despite `C2` only finishing *after* `B` errors out. +/// +/// When testing with `keep_going = false`, we ensure that `C1` is not built, +/// since `C2` exits after `B` errors, so nothing else should be built. +async fn inner(keep_going: bool) -> Result<(), anyhow::Error> { + let temp_dir = TempDir::new("zbuild").context("Unable to create temporary directory")?; + let zbuild_yaml = temp_dir.path().join("zbuild.yaml"); + + // TODO: Instead of sleeping, use `inotify` to wait for other + // actions to happen? + fs::write( + &zbuild_yaml, + r#"--- +rules: + a: + out: [a] + deps: [b, c1] + exec: + - [touch, a] + b: + out: [b] + exec: + - [sleep, "0.1"] + - ["false"] + - [touch, b] + c1: + out: [c1] + deps: [c2] + exec: + - [touch, c1] + c2: + out: [c2] + exec: + - [sleep, "0.2"] + - [touch, c2] +"#, + ) + .context("Unable to write zbuild manifest")?; + + let args = Args { + targets: ["a".to_owned()].into(), + zbuild_path: Some(zbuild_yaml), + keep_going, + ..Args::default() + }; + tracing::info!(?args, "Arguments"); + let res = zbuild::run(args).await; + anyhow::ensure!(res.is_err(), "Expected zbuild error"); + + + let a = temp_dir.path().join("a"); + let b = temp_dir.path().join("b"); + let c1 = temp_dir.path().join("c1"); + let c2 = temp_dir.path().join("c2"); + anyhow::ensure!( + !a.try_exists().context("Unable to check if output file exists")?, + "Output file {a:?} was created" + ); + anyhow::ensure!( + !b.try_exists().context("Unable to check if output file exists")?, + "Output file {b:?} was created" + ); + + match keep_going { + true => anyhow::ensure!( + c1.try_exists().context("Unable to check if output file exists")?, + "Output file {c1:?} was missing" + ), + false => anyhow::ensure!( + !c1.try_exists().context("Unable to check if output file exists")?, + "Output file {c1:?} was created" + ), + } + + anyhow::ensure!( + c2.try_exists().context("Unable to check if output file exists")?, + "Output file {c2:?} was missing" + ); + + Ok(()) +} diff --git a/tests/util/mod.rs b/tests/util/mod.rs index 89413f5..3509663 100644 --- a/tests/util/mod.rs +++ b/tests/util/mod.rs @@ -1,5 +1,11 @@ //! Utilities for all integration tests +// Lints +#![allow( + dead_code, + reason = "This module is used from many tests, which might not use everything" +)] + // Imports use {anyhow::Context, std::fs, tempdir::TempDir, zbuild::Args};