diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a035665e..bd17f436b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Added** `vp run --dry-run` shows whether each task would hit the local cache, or why it would miss, without running anything ([#792](https://github.com/voidzero-dev/vite-task/pull/792)). - **Changed** When a task isn't cached because it wrote a file it also read, `vp run --last-details` now says the task read and wrote the file, and shows the `cache: { input, output }` exclusions that let the task be cached ([#784](https://github.com/voidzero-dev/vite-task/pull/784)). - **Changed** The run summary now says a task that wrote a file it also read was `not cached because it modified its inputs`, and the statistics in `vp run --verbose` and `vp run --last-details` use the singular for a count of one, e.g. `1 task • 1 cache miss` ([#783](https://github.com/voidzero-dev/vite-task/pull/783)). - **Fixed** An invalid glob in `--filter` no longer shows its error message twice ([#763](https://github.com/voidzero-dev/vite-task/pull/763)). diff --git a/crates/vt/src/cli/mod.rs b/crates/vt/src/cli/mod.rs index 6e050f964..beec73f42 100644 --- a/crates/vt/src/cli/mod.rs +++ b/crates/vt/src/cli/mod.rs @@ -129,6 +129,10 @@ pub struct RunCommand { #[clap(long, exclusive = true)] pub(crate) last_details: bool, + /// Show whether each task would hit the local cache, without running anything. + #[clap(long)] + pub(crate) dry_run: bool, + #[clap( allow_hyphen_values = true, value_names = ["TASK_SPECIFIER", "ADDITIONAL_ARGS"], @@ -200,6 +204,9 @@ pub struct ResolvedRunCommand { pub flags: RunFlags, + /// Report predicted cache results instead of running tasks (`--dry-run`). + pub dry_run: bool, + /// Additional arguments to pass to the tasks. pub additional_args: Vec, } @@ -214,7 +221,12 @@ impl RunCommand { let mut iter = self.task_and_args.into_iter(); let task_specifier = iter.next(); let additional_args: Vec = iter.collect(); - ResolvedRunCommand { task_specifier, flags: self.flags, additional_args } + ResolvedRunCommand { + task_specifier, + flags: self.flags, + dry_run: self.dry_run, + additional_args, + } } } diff --git a/crates/vt/src/session/cache/mod.rs b/crates/vt/src/session/cache/mod.rs index c93d22235..699b3214a 100644 --- a/crates/vt/src/session/cache/mod.rs +++ b/crates/vt/src/session/cache/mod.rs @@ -13,7 +13,7 @@ pub use display::{ SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_input_change_str, format_spawn_change, }; -use rusqlite::{Connection, OptionalExtension as _}; +use rusqlite::{Connection, OpenFlags, OptionalExtension as _}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; @@ -306,6 +306,20 @@ impl ExecutionCache { Ok(Self { conn: Mutex::new(conn), remote_clients: RemoteClients::default() }) } + /// Open the cache database at `path` read-only, for `--dry-run`. Returns + /// `None` if there is no database yet. Nothing is created or written. + pub fn open_read_only(path: &AbsolutePath) -> anyhow::Result> { + let db_path = path.join("cache.db"); + if !db_path.as_path().exists() { + return Ok(None); + } + let conn = Connection::open_with_flags( + db_path.as_path(), + OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX, + )?; + Ok(Some(Self { conn: Mutex::new(conn), remote_clients: RemoteClients::default() })) + } + #[tracing::instrument] pub async fn save(self) -> anyhow::Result<()> { // do some cleanup in the future @@ -336,7 +350,13 @@ impl ExecutionCache { .try_hit_local(cache_metadata, &cache_key, globbed_inputs, workspace_root) .await? { - Ok(value) => return Ok(Ok(CacheHit { value, source: CacheHitSource::Local })), + Ok(value) => { + // Associate the execution key to the cache entry key if not already, + // so that next time we can find it and report what changed + self.upsert_task_fingerprint(&cache_metadata.execution_cache_key, &cache_key) + .await?; + return Ok(Ok(CacheHit { value, source: CacheHitSource::Local })); + } Err(miss) => miss, }; #[expect( @@ -371,6 +391,24 @@ impl ExecutionCache { })) } + /// Look up the task in the local cache without writing to it, for + /// `--dry-run`. Returns `Ok(Ok(_))` on a hit and `Ok(Err(miss))` otherwise. + /// The remote cache is never queried. + pub async fn peek_local( + &self, + cache_metadata: &CacheMetadata, + globbed_inputs: &BTreeMap, + workspace_root: &AbsolutePath, + ) -> anyhow::Result> { + let cache_key = CacheEntryKey::from_metadata(cache_metadata); + Ok(self + .try_hit_local(cache_metadata, &cache_key, globbed_inputs, workspace_root) + .await? + .map(drop)) + } + + /// Look up the task in the local cache. Read-only: on a hit, the caller + /// decides whether to record the execution key. async fn try_hit_local( &self, cache_metadata: &CacheMetadata, @@ -389,9 +427,6 @@ impl ExecutionCache { )? { return Ok(Err(CacheMiss::FingerprintMismatch(mismatch))); } - // Associate the execution key to the cache entry key if not already, - // so that next time we can find it and report what changed - self.upsert_task_fingerprint(execution_cache_key, cache_key).await?; return Ok(Ok(cache_value)); } diff --git a/crates/vt/src/session/execute/dry_run.rs b/crates/vt/src/session/execute/dry_run.rs new file mode 100644 index 000000000..061b97780 --- /dev/null +++ b/crates/vt/src/session/execute/dry_run.rs @@ -0,0 +1,148 @@ +//! `vp run --dry-run`: walk the planned graph in dependency order and predict +//! each task's cache result from the local cache, without running anything or +//! writing to the cache. + +use std::{io::Write as _, sync::Arc}; + +use rustc_hash::FxHashMap; +use vt_path::AbsolutePath; +use vt_plan::{ + ExecutionGraph, ExecutionItemDisplay, ExecutionItemKind, LeafExecutionKind, SpawnExecution, + execution_graph::ExecutionNodeIndex, +}; +use vt_str::Str; + +use super::glob::compute_globbed_inputs; +use crate::{ + Session, + session::{ + cache::ExecutionCache, + reporter::{ + dry_run::{DryRunPrediction, format_dry_run_line, format_remote_not_checked}, + summary::SavedCacheMissReason, + }, + }, +}; + +struct DryRun<'a> { + /// `None` when there is no local cache yet, so every lookup is a miss. + cache: Option<&'a ExecutionCache>, + workspace_root: &'a Arc, + out: Vec, + /// Set when a task has a remote cache configured. It isn't queried. + remote_cache_configured: bool, +} + +impl DryRun<'_> { + /// Predict every task in `graph`, dependencies first. `runs_after` names a + /// task outside `graph` that runs first and isn't a cache hit. + /// + /// Returns whether every task in `graph` is a predicted hit. + async fn walk_graph( + &mut self, + graph: &ExecutionGraph, + runs_after: Option<&Str>, + ) -> anyhow::Result { + // Tasks that aren't predicted hits, by node. Edge A→B means A depends + // on B, so reversing the topological order puts dependencies first. + let mut not_hit: FxHashMap = FxHashMap::default(); + for node_ix in graph.graph.compute_topological_order().into_iter().rev() { + let task = &graph.graph[node_ix]; + let mut blocker: Option = runs_after.cloned().or_else(|| { + graph.graph.neighbors(node_ix).find_map(|dep| not_hit.get(&dep).cloned()) + }); + let mut task_hit = true; + for item in &task.items { + let display = &item.execution_item_display; + let item_hit = match &item.kind { + ExecutionItemKind::Leaf(LeafExecutionKind::InProcess(_)) => { + // Built-ins don't change files, so they don't block later parts. + self.write(display, &DryRunPrediction::BuiltIn); + true + } + ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn)) => { + self.spawn_leaf(display, spawn, blocker.as_ref()).await? + } + ExecutionItemKind::Expanded(nested) => { + Box::pin(self.walk_graph(nested, blocker.as_ref())).await? + } + }; + if !item_hit { + task_hit = false; + // Later `&&` parts run after this one. + blocker.get_or_insert_with(|| display.command.clone()); + } + } + if !task_hit { + not_hit.insert(node_ix, vt_str::format!("{}", task.task_display)); + } + } + Ok(not_hit.is_empty()) + } + + /// Predict one spawned process and write its line. Returns whether it's a + /// predicted hit. + async fn spawn_leaf( + &mut self, + display: &ExecutionItemDisplay, + spawn: &SpawnExecution, + runs_after: Option<&Str>, + ) -> anyhow::Result { + let Some(metadata) = &spawn.cache_metadata else { + self.write(display, &DryRunPrediction::Disabled); + return Ok(false); + }; + self.remote_cache_configured |= metadata.remote_cache.is_some(); + + let prediction = if let Some(runs_after) = runs_after { + DryRunPrediction::Unknown { runs_after: runs_after.clone() } + } else { + let globbed_inputs = compute_globbed_inputs( + self.workspace_root, + &metadata.input_config.positive_globs, + &metadata.input_config.negative_globs, + )?; + let lookup = match self.cache { + Some(cache) => { + cache.peek_local(metadata, &globbed_inputs, self.workspace_root).await? + } + None => Err(crate::session::CacheMiss::NotFound), + }; + match lookup { + Ok(()) => DryRunPrediction::Hit, + Err(miss) => DryRunPrediction::Miss(SavedCacheMissReason::from_cache_miss(&miss)), + } + }; + let hit = matches!(prediction, DryRunPrediction::Hit); + self.write(display, &prediction); + Ok(hit) + } + + fn write(&mut self, display: &ExecutionItemDisplay, prediction: &DryRunPrediction) { + self.out.extend_from_slice( + format_dry_run_line(display, self.workspace_root, prediction).as_bytes(), + ); + } +} + +impl Session<'_> { + /// Report what running `graph` would do with the local cache, without + /// running any task. Nothing is written to the cache directory. + pub(crate) async fn dry_run(&self, graph: &ExecutionGraph) -> anyhow::Result<()> { + let cache = ExecutionCache::open_read_only(&self.cache_path)?; + let mut dry_run = DryRun { + cache: cache.as_ref(), + workspace_root: &self.workspace_path, + out: Vec::new(), + remote_cache_configured: false, + }; + dry_run.walk_graph(graph, None).await?; + if dry_run.remote_cache_configured { + dry_run.out.extend_from_slice(format_remote_not_checked().as_bytes()); + } + let mut stdout = std::io::stdout().lock(); + stdout.write_all(&dry_run.out)?; + stdout.flush()?; + Ok(()) + } +} diff --git a/crates/vt/src/session/execute/mod.rs b/crates/vt/src/session/execute/mod.rs index 0426f060f..8c008eea6 100644 --- a/crates/vt/src/session/execute/mod.rs +++ b/crates/vt/src/session/execute/mod.rs @@ -1,4 +1,5 @@ mod cache_update; +mod dry_run; pub mod fingerprint; pub mod glob; mod hash; diff --git a/crates/vt/src/session/mod.rs b/crates/vt/src/session/mod.rs index 6495080c7..f7cd51048 100644 --- a/crates/vt/src/session/mod.rs +++ b/crates/vt/src/session/mod.rs @@ -130,6 +130,13 @@ impl vt_plan::PlanRequestParser for PlanRequestParser<'_> { command.to_synthetic_plan_request(UserCacheConfig::disabled()), ))) } + ResolvedCommand::Run(run_command) if run_command.dry_run => { + // A nested `--dry-run` runs no tasks, so it isn't expanded into + // the parent's graph. + Ok(Some(PlanRequest::Synthetic( + command.to_synthetic_plan_request(UserCacheConfig::disabled()), + ))) + } ResolvedCommand::Run(run_command) => { match run_command.into_query_plan_request(&command.cwd) { Ok((query_plan_request, _)) => { @@ -336,6 +343,10 @@ impl<'a> Session<'a> { self.plan_from_query(qpr).await? }; + if run_command.dry_run { + return self.dry_run(&graph).await.map_err(SessionError::from); + } + let workspace_path = self.workspace_path(); let writer: Box = Box::new(std::io::stdout()); diff --git a/crates/vt/src/session/reporter/dry_run.rs b/crates/vt/src/session/reporter/dry_run.rs new file mode 100644 index 000000000..f0922bc42 --- /dev/null +++ b/crates/vt/src/session/reporter/dry_run.rs @@ -0,0 +1,67 @@ +//! Rendering for `vp run --dry-run`: one line per task with its predicted +//! cache result, in the style of the full run summary. + +use owo_colors::Style; +use vt_path::AbsolutePath; +use vt_plan::ExecutionItemDisplay; +use vt_str::Str; + +use super::{ + CACHE_MISS_STYLE, COMMAND_STYLE, ColorizeExt, format_command_display, + summary::{SavedCacheMissReason, format_hit}, +}; +use crate::session::cache::CacheHitSource; + +/// What a run would do with the cache for one task. +pub enum DryRunPrediction { + /// The local cache has a valid entry. + Hit, + /// The local cache has no valid entry. + Miss(SavedCacheMissReason), + /// Caching is off for the task. + Disabled, + /// A built-in command, which is never cached. + BuiltIn, + /// A task that runs before this one isn't a cache hit, and running it + /// could change this task's inputs. Holds that task's name or command. + Unknown { runs_after: Str }, +} + +/// Format one task's line, e.g. `build: $ tsc → Cache hit`. +pub fn format_dry_run_line( + display: &ExecutionItemDisplay, + workspace_path: &AbsolutePath, + prediction: &DryRunPrediction, +) -> Str { + let (detail, style) = match prediction { + DryRunPrediction::Hit => { + (vt_str::format!("→ {}", format_hit(CacheHitSource::Local)), Style::new().green()) + } + DryRunPrediction::Miss(reason) => { + (vt_str::format!("→ Cache miss: {reason}"), CACHE_MISS_STYLE) + } + DryRunPrediction::Disabled => (Str::from("→ Cache disabled"), Style::new().bright_black()), + DryRunPrediction::BuiltIn => { + (Str::from("→ Cache disabled for built-in command"), Style::new().bright_black()) + } + DryRunPrediction::Unknown { runs_after } => ( + vt_str::format!("→ Unknown: runs after '{runs_after}', which isn't a cache hit"), + Style::new().yellow(), + ), + }; + vt_str::format!( + "{}: {} {}\n", + vt_str::format!("{}", display.task_display).style(Style::new().bright_white().bold()), + format_command_display(display, workspace_path).style(COMMAND_STYLE), + detail.style(style), + ) +} + +/// The note printed once when a task has a remote cache configured. +pub fn format_remote_not_checked() -> Str { + vt_str::format!( + "{}\n", + "Remote cache not checked: --dry-run only reads the local cache" + .style(Style::new().bright_black()) + ) +} diff --git a/crates/vt/src/session/reporter/mod.rs b/crates/vt/src/session/reporter/mod.rs index 4a8522664..92f140dcd 100644 --- a/crates/vt/src/session/reporter/mod.rs +++ b/crates/vt/src/session/reporter/mod.rs @@ -23,6 +23,7 @@ //! Additionally, [`plain::PlainReporter`] is a standalone [`LeafExecutionReporter`] for //! single-leaf synthetic executions (e.g., `execute_synthetic`). +pub mod dry_run; mod grouped; mod interleaved; mod labeled; diff --git a/crates/vt/src/session/reporter/summary.rs b/crates/vt/src/session/reporter/summary.rs index 14936fc4f..227a261a6 100644 --- a/crates/vt/src/session/reporter/summary.rs +++ b/crates/vt/src/session/reporter/summary.rs @@ -279,7 +279,7 @@ impl SavedError { } impl SavedCacheMissReason { - fn from_cache_miss(cache_miss: &CacheMiss) -> Self { + pub fn from_cache_miss(cache_miss: &CacheMiss) -> Self { match cache_miss { CacheMiss::NotFound => Self::NotFound, CacheMiss::FingerprintMismatch(mismatch) => match mismatch { @@ -309,6 +309,31 @@ impl SavedCacheMissReason { } } +/// The reason shown after `Cache miss: ` in the full summary. +impl Display for SavedCacheMissReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotFound => f.write_str("no previous cache entry found"), + Self::SpawnFingerprintChanged(changes) => { + let formatted: Vec = changes.iter().map(format_spawn_change).collect(); + if formatted.is_empty() { + f.write_str("configuration changed") + } else { + f.write_str(&formatted.iter().map(Str::as_str).collect::>().join("; ")) + } + } + Self::ConfigChanged => f.write_str("input configuration changed"), + Self::InputChanged { kind, path } => { + f.write_str(&format_input_change_str(*kind, path.as_str())) + } + Self::TrackedEnvChanged(mismatch) | Self::TrackedEnvQueryChanged { mismatch, .. } => { + write!(f, "{mismatch}") + } + Self::RemoteReadFailed(error) => f.write_str(&error.message), + } + } +} + impl TaskResult { /// Build a [`TaskResult`] from live execution data. /// @@ -615,35 +640,10 @@ impl TaskResult { Self::InProcess => Str::from("→ Cache disabled for built-in command"), Self::Spawned { cache_status, .. } => match cache_status { SpawnedCacheStatus::Disabled => Str::from("→ Cache disabled in task configuration"), - SpawnedCacheStatus::Miss(reason) => match reason { - SavedCacheMissReason::NotFound => { - Str::from("→ Cache miss: no previous cache entry found") - } - SavedCacheMissReason::SpawnFingerprintChanged(changes) => { - let formatted: Vec = changes.iter().map(format_spawn_change).collect(); - if formatted.is_empty() { - Str::from("→ Cache miss: configuration changed") - } else { - let joined = - formatted.iter().map(Str::as_str).collect::>().join("; "); - vt_str::format!("→ Cache miss: {joined}") - } - } - SavedCacheMissReason::ConfigChanged => { - Str::from("→ Cache miss: input configuration changed") - } - SavedCacheMissReason::InputChanged { kind, path } => { - let desc = format_input_change_str(*kind, path.as_str()); - vt_str::format!("→ Cache miss: {desc}") - } - SavedCacheMissReason::TrackedEnvChanged(mismatch) - | SavedCacheMissReason::TrackedEnvQueryChanged { mismatch, .. } => { - vt_str::format!("→ Cache miss: {mismatch}") - } - SavedCacheMissReason::RemoteReadFailed(error) => { - return (vt_str::format!("→ Cache miss: {}", error.message), &error.causes); - } - }, + SpawnedCacheStatus::Miss(SavedCacheMissReason::RemoteReadFailed(error)) => { + return (vt_str::format!("→ Cache miss: {}", error.message), &error.causes); + } + SpawnedCacheStatus::Miss(reason) => vt_str::format!("→ Cache miss: {reason}"), }, }; (detail, &[]) @@ -707,7 +707,7 @@ impl TaskResult { } /// "Cache hit" or "Remote cache hit", for the full summary's detail line. -const fn format_hit(source: CacheHitSource) -> &'static str { +pub const fn format_hit(source: CacheHitSource) -> &'static str { match source { CacheHitSource::Local => "Cache hit", CacheHitSource::Remote => "Remote cache hit", diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/package.json new file mode 100644 index 000000000..099d8db7f --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/package.json @@ -0,0 +1,4 @@ +{ + "name": "dry-run", + "private": true +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots.toml new file mode 100644 index 000000000..67187846a --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots.toml @@ -0,0 +1,139 @@ +[[e2e]] +name = "predicts_hits_and_misses" +comment = """ +`--dry-run` reports each task's local cache result without running it. A task whose dependency isn't a hit is unknown. The dry run writes nothing, so it creates no cache and doesn't replace the last summary. +""" +steps = [ + { argv = [ + "vt", + "run", + "--dry-run", + "test", + ], comment = "no cache yet: build misses, test is unknown" }, + { argv = [ + "vtt", + "stat-file", + "node_modules", + ], comment = "the dry run created no cache" }, + { argv = [ + "vt", + "run", + "test", + ], comment = "populate the cache" }, + { argv = [ + "vt", + "run", + "--dry-run", + "test", + ], comment = "both hit" }, + { argv = [ + "vtt", + "replace-file-content", + "src.txt", + "source", + "changed", + ], comment = "modify build's input" }, + { argv = [ + "vt", + "run", + "--dry-run", + "test", + ], comment = "build misses with the reason, test is unknown" }, + { argv = [ + "vt", + "run", + "--dry-run", + "test", + ], comment = "unchanged: the dry run didn't update the cache" }, + { argv = [ + "vt", + "run", + "--last-details", + ], comment = "still the summary of the last real run" }, + { argv = [ + "vt", + "run", + "test", + ], comment = "the real run reports the same miss" }, + { argv = [ + "vt", + "run", + "--dry-run", + "test", + ], comment = "both hit again" }, +] + +[[e2e]] +name = "and_split_parts" +comment = """ +A later `&&` part of a task is unknown when an earlier part isn't a hit. +""" +steps = [ + [ + "vt", + "run", + "both", + ], + { argv = [ + "vtt", + "replace-file-content", + "src.txt", + "source", + "changed", + ], comment = "modify an input of both parts" }, + { argv = [ + "vt", + "run", + "--dry-run", + "both", + ], comment = "the first part misses, the second is unknown" }, +] + +[[e2e]] +name = "cache_disabled" +comment = """ +Tasks with caching off are reported as disabled, even when the cache has an entry. +""" +steps = [ + { argv = [ + "vt", + "run", + "--dry-run", + "uncached", + ], comment = "cache: false" }, + [ + "vt", + "run", + "test", + ], + { argv = [ + "vt", + "run", + "--dry-run", + "--no-cache", + "test", + ], comment = "--no-cache turns caching off for every task" }, +] + +[[e2e]] +name = "remote_cache_not_checked" +comment = """ +The dry run only reads the local cache. It says so once when a remote cache is configured. +""" +steps = [ + { argv = [ + "vt", + "run", + "--dry-run", + "build", + ], envs = [ + [ + "VP_REMOTE_CACHE_URL", + "cache.example/projects/test", + ], + ] }, +] + +[[e2e]] +name = "conflicts_with_last_details" +steps = [["vt", "run", "--dry-run", "--last-details"]] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/and_split_parts.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/and_split_parts.md new file mode 100644 index 000000000..eed030294 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/and_split_parts.md @@ -0,0 +1,32 @@ +# and_split_parts + +A later `&&` part of a task is unknown when an earlier part isn't a hit. + +## `vt run both` + +``` +$ vtt print-file src.txt +source + +$ vtt print-file test.txt +tests + +--- +vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details) +``` + +## `vtt replace-file-content src.txt source changed` + +modify an input of both parts + +``` +``` + +## `vt run --dry-run both` + +the first part misses, the second is unknown + +``` +dry-run#both: $ vtt print-file src.txt → Cache miss: 'src.txt' modified +dry-run#both: $ vtt print-file test.txt → Unknown: runs after 'vtt print-file src.txt', which isn't a cache hit +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/cache_disabled.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/cache_disabled.md new file mode 100644 index 000000000..45056b2ba --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/cache_disabled.md @@ -0,0 +1,33 @@ +# cache_disabled + +Tasks with caching off are reported as disabled, even when the cache has an entry. + +## `vt run --dry-run uncached` + +cache: false + +``` +dry-run#uncached: $ vtt print uncached → Cache disabled +``` + +## `vt run test` + +``` +$ vtt print-file src.txt +source + +$ vtt print-file test.txt +tests + +--- +vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details) +``` + +## `vt run --dry-run --no-cache test` + +--no-cache turns caching off for every task + +``` +dry-run#build: $ vtt print-file src.txt → Cache disabled +dry-run#test: $ vtt print-file test.txt → Cache disabled +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/conflicts_with_last_details.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/conflicts_with_last_details.md new file mode 100644 index 000000000..55791794a --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/conflicts_with_last_details.md @@ -0,0 +1,13 @@ +# conflicts_with_last_details + +## `vt run --dry-run --last-details` + +**Exit code:** 2 + +``` +error: the argument '--last-details' cannot be used with one or more of the other specified arguments + +Usage: vt run [OPTIONS] [TASK_SPECIFIER] [ADDITIONAL_ARGS]... + +For more information, try '--help'. +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/predicts_hits_and_misses.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/predicts_hits_and_misses.md new file mode 100644 index 000000000..64b61496c --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/predicts_hits_and_misses.md @@ -0,0 +1,116 @@ +# predicts_hits_and_misses + +`--dry-run` reports each task's local cache result without running it. A task whose dependency isn't a hit is unknown. The dry run writes nothing, so it creates no cache and doesn't replace the last summary. + +## `vt run --dry-run test` + +no cache yet: build misses, test is unknown + +``` +dry-run#build: $ vtt print-file src.txt → Cache miss: no previous cache entry found +dry-run#test: $ vtt print-file test.txt → Unknown: runs after 'dry-run#build', which isn't a cache hit +``` + +## `vtt stat-file node_modules` + +the dry run created no cache + +``` +node_modules: missing +``` + +## `vt run test` + +populate the cache + +``` +$ vtt print-file src.txt +source + +$ vtt print-file test.txt +tests + +--- +vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details) +``` + +## `vt run --dry-run test` + +both hit + +``` +dry-run#build: $ vtt print-file src.txt → Cache hit +dry-run#test: $ vtt print-file test.txt → Cache hit +``` + +## `vtt replace-file-content src.txt source changed` + +modify build's input + +``` +``` + +## `vt run --dry-run test` + +build misses with the reason, test is unknown + +``` +dry-run#build: $ vtt print-file src.txt → Cache miss: 'src.txt' modified +dry-run#test: $ vtt print-file test.txt → Unknown: runs after 'dry-run#build', which isn't a cache hit +``` + +## `vt run --dry-run test` + +unchanged: the dry run didn't update the cache + +``` +dry-run#build: $ vtt print-file src.txt → Cache miss: 'src.txt' modified +dry-run#test: $ vtt print-file test.txt → Unknown: runs after 'dry-run#build', which isn't a cache hit +``` + +## `vt run --last-details` + +still the summary of the last real run + +``` + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 2 tasks • 0 cache hits • 2 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] dry-run#build: $ vtt print-file src.txt ✓ + → Cache miss: no previous cache entry found + ······················································· + [2] dry-run#test: $ vtt print-file test.txt ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## `vt run test` + +the real run reports the same miss + +``` +$ vtt print-file src.txt ○ cache miss: 'src.txt' modified, executing +changed + +$ vtt print-file test.txt ◉ cache hit, replaying +tests + +--- +vt run: 1/2 cache hit (50%). (Run `vt run --last-details` for full details) +``` + +## `vt run --dry-run test` + +both hit again + +``` +dry-run#build: $ vtt print-file src.txt → Cache hit +dry-run#test: $ vtt print-file test.txt → Cache hit +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/remote_cache_not_checked.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/remote_cache_not_checked.md new file mode 100644 index 000000000..f87f08ec4 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/snapshots/remote_cache_not_checked.md @@ -0,0 +1,10 @@ +# remote_cache_not_checked + +The dry run only reads the local cache. It says so once when a remote cache is configured. + +## `VP_REMOTE_CACHE_URL=cache.example/projects/test vt run --dry-run build` + +``` +dry-run#build: $ vtt print-file src.txt → Cache miss: no previous cache entry found +Remote cache not checked: --dry-run only reads the local cache +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/src.txt b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/src.txt new file mode 100644 index 000000000..5a18cd2fb --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/src.txt @@ -0,0 +1 @@ +source diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/test.txt b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/test.txt new file mode 100644 index 000000000..2b29f2764 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/test.txt @@ -0,0 +1 @@ +tests diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/vite-task.json new file mode 100644 index 000000000..2ecd69124 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/dry_run/vite-task.json @@ -0,0 +1,21 @@ +{ + "tasks": { + "build": { + "command": "vtt print-file src.txt", + "cache": { "input": ["src.txt"] } + }, + "test": { + "command": "vtt print-file test.txt", + "dependsOn": ["build"], + "cache": { "input": ["test.txt"] } + }, + "both": { + "command": "vtt print-file src.txt && vtt print-file test.txt", + "cache": { "input": ["*.txt"] } + }, + "uncached": { + "command": "vtt print uncached", + "cache": false + } + } +}