diff --git a/crates/vite_task/src/session/cache/display.rs b/crates/vite_task/src/session/cache/display.rs index c194f934..4e6a910d 100644 --- a/crates/vite_task/src/session/cache/display.rs +++ b/crates/vite_task/src/session/cache/display.rs @@ -8,7 +8,9 @@ use std::collections::HashSet; use vite_task_plan::cache_metadata::SpawnFingerprint; use super::{CacheMiss, FingerprintMismatch}; -use crate::session::event::{CacheDisabledReason, CacheStatus}; +use crate::session::event::{ + CacheDisabledReason, CacheNotUpdatedReason, CacheStatus, CacheUpdateStatus, +}; /// Describes a single atomic change between two spawn fingerprints enum SpawnFingerprintChange { @@ -283,3 +285,32 @@ pub fn format_cache_status_summary(cache_status: &CacheStatus) -> String { } } } + +/// Format cache update status for summary display (post-execution). +/// +/// Returns Some(formatted_string) only when the reason is not already clear from CacheStatus. +/// - Updated: No message needed (success is implied) +/// - CacheHit: No message needed (already shown in CacheStatus::Hit) +/// - CacheDisabled: No message needed (already shown in CacheStatus::Disabled) +/// - BuiltInCommand: No message needed (already shown in CacheStatus::Disabled(InProcessExecution)) +/// - NonZeroExitStatus: Shows message that cache wasn't updated due to failure +/// +/// Note: Returns plain text without styling. The reporter applies colors. +pub fn format_cache_update_status(status: &CacheUpdateStatus) -> Option { + match status { + CacheUpdateStatus::Updated => None, + CacheUpdateStatus::NotUpdated(reason) => match reason { + // These are already clear from CacheStatus in the Start event + CacheNotUpdatedReason::CacheHit => None, + CacheNotUpdatedReason::CacheDisabled => None, + CacheNotUpdatedReason::BuiltInCommand => None, + // These need to be shown - they explain why cache wasn't updated + CacheNotUpdatedReason::NonZeroExitStatus => { + Some("→ Cache not updated: task failed".to_string()) + } + CacheNotUpdatedReason::StdinDataExists => { + Some("→ Cache not updated: stdin had data".to_string()) + } + }, + } +} diff --git a/crates/vite_task/src/session/cache/mod.rs b/crates/vite_task/src/session/cache/mod.rs index d43a01db..ed9d3915 100644 --- a/crates/vite_task/src/session/cache/mod.rs +++ b/crates/vite_task/src/session/cache/mod.rs @@ -6,7 +6,9 @@ use std::{fmt::Display, fs::File, io::Write, sync::Arc, time::Duration}; use bincode::{Decode, Encode, decode_from_slice, encode_to_vec}; // Re-export display functions for convenience -pub use display::{format_cache_status_inline, format_cache_status_summary}; +pub use display::{ + format_cache_status_inline, format_cache_status_summary, format_cache_update_status, +}; use rusqlite::{Connection, OptionalExtension as _, config::DbConfig}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; diff --git a/crates/vite_task/src/session/event.rs b/crates/vite_task/src/session/event.rs index 85d07584..0f8b581e 100644 --- a/crates/vite_task/src/session/event.rs +++ b/crates/vite_task/src/session/event.rs @@ -27,6 +27,10 @@ pub enum CacheNotUpdatedReason { CacheDisabled, /// Execution exited with non-zero status NonZeroExitStatus, + /// Built-in command doesn't support caching + BuiltInCommand, + /// Stdin had data - output may depend on input, unsafe to cache + StdinDataExists, } #[derive(Debug)] diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs index b62c01e4..354f1541 100644 --- a/crates/vite_task/src/session/execute/mod.rs +++ b/crates/vite_task/src/session/execute/mod.rs @@ -153,13 +153,13 @@ impl ExecutionContext<'_> { }, }); - // Emit Finish with CacheDisabled status (in-process executions don't cache) + // Emit Finish with BuiltInCommand status (built-in commands don't cache) self.event_handler.handle_event(ExecutionEvent { execution_id, kind: ExecutionEventKind::Finish { status: Some(0), cache_update_status: CacheUpdateStatus::NotUpdated( - CacheNotUpdatedReason::CacheDisabled, + CacheNotUpdatedReason::BuiltInCommand, ), }, }); @@ -287,11 +287,18 @@ impl ExecutionContext<'_> { }; // 5. Update cache if successful and determine cache update status + // Priority: NonZeroExitStatus > StdinDataExists > Success let cache_update_status = if let Some((track_result, cache_metadata)) = track_result_with_cache_metadata { - if result.exit_status.success() { - // Execution succeeded, attempt cache update + if !result.exit_status.success() { + // Priority 1: Non-zero exit status - don't update cache + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::NonZeroExitStatus) + } else if result.stdin_had_data { + // Priority 2: Stdin had data - output may depend on input, unsafe to cache + CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::StdinDataExists) + } else { + // Success: attempt cache update let fingerprint_ignores = cache_metadata.spawn_fingerprint.fingerprint_ignores().map(|v| v.as_slice()); match PostRunFingerprint::create( @@ -326,9 +333,6 @@ impl ExecutionContext<'_> { return Err(ExecutionAborted); } } - } else { - // Execution failed with non-zero exit status, don't update cache - CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::NonZeroExitStatus) } } else { // Caching was disabled for this task diff --git a/crates/vite_task/src/session/execute/spawn.rs b/crates/vite_task/src/session/execute/spawn.rs index 6cf24019..61876d19 100644 --- a/crates/vite_task/src/session/execute/spawn.rs +++ b/crates/vite_task/src/session/execute/spawn.rs @@ -10,7 +10,7 @@ use bincode::{Decode, Encode}; use bstr::BString; use fspy::AccessMode; use serde::Serialize; -use tokio::io::AsyncReadExt as _; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; use vite_path::{AbsolutePath, RelativePathBuf}; use vite_task_plan::SpawnCommand; @@ -45,6 +45,8 @@ pub struct StdOutput { pub struct SpawnResult { pub exit_status: ExitStatus, pub duration: Duration, + /// Whether stdin had data forwarded to the child + pub stdin_had_data: bool, } /// Tracking result from a spawned process for caching @@ -65,8 +67,35 @@ pub struct SpawnTrackResult { /// Returns the execution result including captured outputs, exit status, /// and tracked file accesses. /// -/// - `on_output` is called in real-time as stdout/stderr data arrives. -/// - `track_result` if provided, will be populated with captured outputs and path accesses for caching. If `None`, tracking is disabled. +/// # Arguments +/// - `spawn_command`: The command to spawn with its arguments, environment, and working directory. +/// - `workspace_root`: Base path for converting absolute paths to relative paths in tracking. +/// - `on_output`: Callback invoked in real-time as stdout/stderr data arrives. +/// - `track_result`: If provided, will be populated with captured outputs and path accesses +/// for caching. If `None`, tracking is disabled and the command runs without fspy overhead. +/// +/// # Concurrent I/O Architecture +/// +/// This function manages three concurrent I/O operations: +/// 1. **drain_outputs**: Reads stdout and stderr until both reach EOF +/// 2. **forward_stdin**: Forwards parent's stdin to child until EOF +/// 3. **wait_for_exit**: Waits for child process to terminate +/// +/// Each operation is a separate future with its own internal loop. This design avoids +/// a single large `select!` loop with many condition flags, making the code easier to +/// understand and maintain. +/// +/// # Deadlock Avoidance +/// +/// All three operations run concurrently. This is critical for commands that depend on +/// stdin, like `node -e "process.stdin.pipe(process.stdout)"`. If we waited for stdout +/// to close before forwarding stdin, we would deadlock because stdout won't close until +/// stdin closes. +/// +/// # Cancellation +/// +/// When the child exits, `forward_stdin` is implicitly cancelled by breaking out of +/// the coordination loop. This is safe because stdin data after child exit is meaningless. pub async fn spawn_with_tracking( spawn_command: &SpawnCommand, workspace_root: &AbsolutePath, @@ -80,126 +109,240 @@ where cmd.args(spawn_command.args.iter().map(|arg| arg.as_str())); cmd.envs(spawn_command.all_envs.iter()); cmd.current_dir(&*spawn_command.cwd); - cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()); - /// The tracking state of the spawned process - enum TrackingState<'a> { - /// Tacking is enabled, with the tracked child and result reference - Enabled(fspy::TrackedChild, &'a mut SpawnTrackResult), - - /// Tracking is disabled, with the tokio child process - Disabled(tokio::process::Child), + // Spawn with or without tracking based on track_result. + // + // WaitHandle is separated from track_result to avoid borrow checker issues: + // - track_result needs to be borrowed mutably after the spawn + // - wait_handle needs to be moved into a future + // By keeping them separate, we can move wait_handle while retaining track_result. + enum WaitHandle { + /// Tracked spawn via fspy - returns termination info with file access data + Tracked(futures_util::future::BoxFuture<'static, std::io::Result>), + /// Untracked spawn via tokio - just returns exit status + Untracked(tokio::process::Child), } - let mut tracking_state = if let Some(track_result) = track_result { - // track_result is Some. Spawn with tracking enabled - TrackingState::Enabled(cmd.spawn().await?, track_result) + let ( + mut child_stdout, + mut child_stderr, + child_stdin, + mut wait_handle, + track_result, + track_enabled, + ) = if let Some(track_result) = track_result { + let mut tracked = cmd.spawn().await?; + let stdout = tracked.stdout.take().unwrap(); + let stderr = tracked.stderr.take().unwrap(); + let stdin = tracked.stdin.take(); + (stdout, stderr, stdin, WaitHandle::Tracked(tracked.wait_handle), Some(track_result), true) } else { - // Spawn without tracking - TrackingState::Disabled(cmd.into_tokio_command().spawn()?) + let mut child = cmd.into_tokio_command().spawn()?; + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + let stdin = child.stdin.take(); + (stdout, stderr, stdin, WaitHandle::Untracked(child), None, false) }; - let mut child_stdout = match &mut tracking_state { - TrackingState::Enabled(tracked_child, _) => tracked_child.stdout.take().unwrap(), - TrackingState::Disabled(tokio_child) => tokio_child.stdout.take().unwrap(), - }; - let mut child_stderr = match &mut tracking_state { - TrackingState::Enabled(tracked_child, _) => tracked_child.stderr.take().unwrap(), - TrackingState::Disabled(tokio_child) => tokio_child.stderr.take().unwrap(), - }; + // Local buffer for captured outputs. This is separate from track_result to allow + // the drain_outputs future to own a mutable reference without conflicting with + // track_result's lifetime. + let mut std_outputs: Vec = Vec::new(); + let start = Instant::now(); + + // Future that drains both stdout and stderr until EOF. + // + // Uses an internal select! loop to read from whichever stream has data available. + // Consecutive outputs of the same kind are merged into a single StdOutput entry + // to reduce storage overhead when caching. + let drain_outputs = async { + let mut stdout_buf = [0u8; 8192]; + let mut stderr_buf = [0u8; 8192]; + let mut stdout_done = false; + let mut stderr_done = false; - let mut outputs = match &mut tracking_state { - TrackingState::Enabled(_, track_result) => Some(&mut track_result.std_outputs), - TrackingState::Disabled(_) => None, + while !stdout_done || !stderr_done { + tokio::select! { + result = child_stdout.read(&mut stdout_buf), if !stdout_done => { + match result? { + 0 => stdout_done = true, + n => { + let content = stdout_buf[..n].to_vec(); + on_output(OutputKind::StdOut, content.clone().into()); + if track_enabled { + if let Some(last) = std_outputs.last_mut() + && last.kind == OutputKind::StdOut + { + last.content.extend(&content); + } else { + std_outputs.push(StdOutput { kind: OutputKind::StdOut, content }); + } + } + } + } + } + result = child_stderr.read(&mut stderr_buf), if !stderr_done => { + match result? { + 0 => stderr_done = true, + n => { + let content = stderr_buf[..n].to_vec(); + on_output(OutputKind::StdErr, content.clone().into()); + if track_enabled { + if let Some(last) = std_outputs.last_mut() + && last.kind == OutputKind::StdErr + { + last.content.extend(&content); + } else { + std_outputs.push(StdOutput { kind: OutputKind::StdErr, content }); + } + } + } + } + } + } + } + Ok::<_, std::io::Error>(()) }; - let mut stdout_buf = [0u8; 8192]; - let mut stderr_buf = [0u8; 8192]; - let mut stdout_done = false; - let mut stderr_done = false; - let start = Instant::now(); + // Future that forwards stdin from parent to child until EOF. + // + // This runs concurrently with drain_outputs to avoid deadlock: some commands + // (like `cat` or `node -e "process.stdin.pipe(process.stdout)"`) won't produce + // output until they receive input, so we must forward stdin while draining outputs. + // + // When parent stdin reaches EOF, we drop the child stdin handle to signal EOF + // to the child process. + // + // Returns whether any data was forwarded - this is used to disable caching since + // the output may depend on the input data which we don't fingerprint. + let forward_stdin = async { + let mut buf = [0u8; 8192]; + let mut stdin_had_data = false; + let mut parent_stdin = tokio::io::stdin(); + let mut child_stdin = child_stdin; - // Helper closure to process output chunks - let mut process_output = |kind: OutputKind, content: Vec| { - // Emit event immediately - on_output(kind, content.clone().into()); - - // Store outputs for caching - if let Some(outputs) = &mut outputs { - // Merge consecutive outputs of the same kind for caching - if let Some(last) = outputs.last_mut() - && last.kind == kind - { - last.content.extend(&content); - } else { - outputs.push(StdOutput { kind, content }); + loop { + match parent_stdin.read(&mut buf).await? { + 0 => { + // EOF on parent stdin - close child stdin to signal EOF + drop(child_stdin.take()); + break Ok::<_, std::io::Error>(stdin_had_data); + } + n => { + stdin_had_data = true; + if let Some(ref mut stdin) = child_stdin { + stdin.write_all(&buf[..n]).await?; + } + } } } }; - // Read from both stdout and stderr concurrently using select! + // Future that waits for child to exit. + // Returns Some(ChildTermination) for tracked spawns, None for untracked. + let wait_for_exit = async { + match &mut wait_handle { + WaitHandle::Tracked(wait) => wait.await.map(Some), + WaitHandle::Untracked(child) => child.wait().await.map(|_| None), + } + }; + + // Pin all futures for use in the coordination loop. + // We can't use join!/select! macros directly because: + // 1. drain_outputs must complete before wait_for_exit (child won't exit until we drain pipes) + // 2. forward_stdin should be cancelled when child exits (not waited on) + // 3. We need to track intermediate state (stdin_result) + futures_util::pin_mut!(drain_outputs, forward_stdin, wait_for_exit); + + // State flags for the coordination loop + let mut drain_done = false; + let mut stdin_result: Option> = None; + let termination: Option; + + // Coordination loop: orchestrates the three concurrent operations. + // + // The select! conditions ensure correct ordering: + // - drain_outputs runs unconditionally until done + // - forward_stdin runs until it completes or child exits (whichever first) + // - wait_for_exit only runs after drain_outputs is done (we must drain pipes first) + // + // When wait_for_exit completes, we break out of the loop. This implicitly cancels + // any pending stdin read, which is safe and intentional. loop { tokio::select! { - result = child_stdout.read(&mut stdout_buf), if !stdout_done => { - match result? { - 0 => stdout_done = true, - n => process_output(OutputKind::StdOut, stdout_buf[..n].to_vec()), - } + // Drain stdout/stderr - must complete before we can wait for exit + result = &mut drain_outputs, if !drain_done => { + result?; + drain_done = true; } - result = child_stderr.read(&mut stderr_buf), if !stderr_done => { - match result? { - 0 => stderr_done = true, - n => process_output(OutputKind::StdErr, stderr_buf[..n].to_vec()), - } + // Forward stdin - record result but don't block child exit + result = &mut forward_stdin, if stdin_result.is_none() => { + stdin_result = Some(result); + } + // Wait for child exit - only after drain is done + result = &mut wait_for_exit, if drain_done => { + termination = result?; + break; } - else => break, } } - let (termination, track_result) = match tracking_state { - TrackingState::Enabled(tracked_child, track_result) => { - (tracked_child.wait_handle.await?, track_result) - } - TrackingState::Disabled(mut tokio_child) => { - return Ok(SpawnResult { - exit_status: tokio_child.wait().await?, - duration: start.elapsed(), - }); - } - }; let duration = start.elapsed(); - // Process path accesses + // Extract stdin_had_data from the result. If stdin read was cancelled (None) or + // errored, we conservatively assume no data was forwarded. + let stdin_had_data = stdin_result.map(|r| r.unwrap_or(false)).unwrap_or(false); + + // Get exit status from termination info + let exit_status = match &termination { + Some(term) => term.status, + None => ExitStatus::default(), // Untracked path - status already consumed + }; + + // If tracking was disabled, return early without processing file accesses + let Some(track_result) = track_result else { + return Ok(SpawnResult { exit_status, duration, stdin_had_data }); + }; + + // Copy captured outputs from local buffer to track_result for caching + track_result.std_outputs = std_outputs; + + // Process path accesses from fspy tracking. + // These are used to build the post-run fingerprint for cache invalidation. + let termination = termination.expect("termination should be Some when tracking is enabled"); let path_reads = &mut track_result.path_reads; let path_writes = &mut track_result.path_writes; for access in termination.path_accesses.iter() { + // Convert absolute paths to workspace-relative paths. + // Paths outside the workspace are ignored (e.g., system libraries). let relative_path = access.path.strip_path_prefix(workspace_root, |strip_result| { let Ok(stripped_path) = strip_result else { return None; }; - // On Windows, paths are possible to be still absolute after stripping the workspace root. - // For example: c:\workspace\subdir\c:\workspace\subdir - // Just ignore those accesses. RelativePathBuf::new(stripped_path).ok() }); let Some(relative_path) = relative_path else { - // Ignore accesses outside the workspace continue; }; - // Skip .git directory accesses (workaround for tools like oxlint) + // Skip .git directory - these are internal git operations that shouldn't + // affect cache fingerprinting (e.g., reading HEAD, refs). if relative_path.as_path().strip_prefix(".git").is_ok() { continue; } + // Track read accesses for fingerprinting input files if access.mode.contains(AccessMode::READ) { path_reads.entry(relative_path.clone()).or_insert(PathRead { read_dir_entries: false }); } + // Track write accesses (for future use - output fingerprinting) if access.mode.contains(AccessMode::WRITE) { path_writes.insert(relative_path.clone(), PathWrite); } + // Track directory reads (e.g., readdir) which may affect cache validity if access.mode.contains(AccessMode::READ_DIR) { match path_reads.entry(relative_path) { Entry::Occupied(mut occupied) => occupied.get_mut().read_dir_entries = true, @@ -214,8 +357,8 @@ where "spawn finished, path_reads: {}, path_writes: {}, exit_status: {}", path_reads.len(), path_writes.len(), - termination.status, + exit_status, ); - Ok(SpawnResult { exit_status: termination.status, duration }) + Ok(SpawnResult { exit_status, duration, stdin_had_data }) } diff --git a/crates/vite_task/src/session/reporter.rs b/crates/vite_task/src/session/reporter.rs index ef8be7d7..408c311f 100644 --- a/crates/vite_task/src/session/reporter.rs +++ b/crates/vite_task/src/session/reporter.rs @@ -11,8 +11,11 @@ use owo_colors::{Style, Styled}; use vite_path::AbsolutePath; use super::{ - cache::{format_cache_status_inline, format_cache_status_summary}, - event::{CacheStatus, ExecutionEvent, ExecutionEventKind, ExecutionId, ExecutionItemDisplay}, + cache::{format_cache_status_inline, format_cache_status_summary, format_cache_update_status}, + event::{ + CacheStatus, CacheUpdateStatus, ExecutionEvent, ExecutionEventKind, ExecutionId, + ExecutionItemDisplay, + }, }; /// Wrap of `OwoColorize` that ignores style if `NO_COLOR` is set. @@ -55,6 +58,7 @@ const CACHE_MISS_STYLE: Style = Style::new().purple(); struct ExecutionInfo { display: Option, cache_status: CacheStatus, // Non-optional, determined at Start + cache_update_status: Option, // Set at Finish exit_status: Option, error_message: Option, } @@ -157,6 +161,7 @@ impl LabeledReporter { self.executions.push(ExecutionInfo { display: None, cache_status, + cache_update_status: None, exit_status: None, error_message: None, }); @@ -199,6 +204,7 @@ impl LabeledReporter { self.executions.push(ExecutionInfo { display: Some(display), cache_status, + cache_update_status: None, exit_status: None, error_message: None, }); @@ -226,7 +232,12 @@ impl LabeledReporter { self.stats.failed += 1; } - fn handle_finish(&mut self, execution_id: ExecutionId, status: Option) { + fn handle_finish( + &mut self, + execution_id: ExecutionId, + status: Option, + cache_update_status: CacheUpdateStatus, + ) { // Update failure statistics if let Some(s) = status { if s != 0 { @@ -234,9 +245,10 @@ impl LabeledReporter { } } - // Update execution info exit status + // Update execution info if let Some(exec) = self.executions.last_mut() { exec.exit_status = status; + exec.cache_update_status = Some(cache_update_status); } // For direct synthetic execution with cache hit, print message at the bottom @@ -430,6 +442,14 @@ impl LabeledReporter { }; let _ = writeln!(self.writer, " {}", styled_summary); + // Cache update status (only shown for NonZeroExitStatus) + if let Some(ref cache_update_status) = exec.cache_update_status { + if let Some(update_msg) = format_cache_update_status(cache_update_status) { + let _ = + writeln!(self.writer, " {}", update_msg.style(Style::new().yellow())); + } + } + // Error message if present if let Some(ref error_msg) = exec.error_message { let _ = writeln!( @@ -486,8 +506,8 @@ impl Reporter for LabeledReporter { ExecutionEventKind::Error { message } => { self.handle_error(event.execution_id, message); } - ExecutionEventKind::Finish { status, cache_update_status: _ } => { - self.handle_finish(event.execution_id, status); + ExecutionEventKind::Finish { status, cache_update_status } => { + self.handle_finish(event.execution_id, status, cache_update_status); } } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/package.json new file mode 100644 index 00000000..8bfea287 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/package.json @@ -0,0 +1,6 @@ +{ + "name": "cache-non-zero-exit-test", + "scripts": { + "test": "node -e \"process.exit(1)\"" + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/snapshots.toml new file mode 100644 index 00000000..844b5823 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/snapshots.toml @@ -0,0 +1,6 @@ +[[e2e]] +name = "failed task does not update cache" +steps = [ + "vite run test", + "vite run test", +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/snapshots/failed task does not update cache.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/snapshots/failed task does not update cache.snap new file mode 100644 index 00000000..4243e3cc --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit/snapshots/failed task does not update cache.snap @@ -0,0 +1,40 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-non-zero-exit +--- +[1]> vite run test +$ node -e "process.exit(1)" + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses • 1 failed +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] cache-non-zero-exit-test#test: $ node -e "process.exit(1)" ✗ (exit code: 1) + → Cache miss: no previous cache entry found + → Cache not updated: task failed +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +[1]> vite run test +$ node -e "process.exit(1)" + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses • 1 failed +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] cache-non-zero-exit-test#test: $ node -e "process.exit(1)" ✗ (exit code: 1) + → Cache miss: no previous cache entry found + → Cache not updated: task failed +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exit-codes/snapshots/multiple task failures returns exit code 1.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exit-codes/snapshots/multiple task failures returns exit code 1.snap index 27126371..d111a2c6 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exit-codes/snapshots/multiple task failures returns exit code 1.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exit-codes/snapshots/multiple task failures returns exit code 1.snap @@ -21,7 +21,9 @@ Task Details: ──────────────────────────────────────────────── [1] pkg-b#fail: ~/packages/pkg-b$ node -e "process.exit(7)" ✗ (exit code: 7) → Cache miss: no previous cache entry found + → Cache not updated: task failed ······················································· [2] pkg-a#fail: ~/packages/pkg-a$ node -e "process.exit(42)" ✗ (exit code: 42) → Cache miss: no previous cache entry found + → Cache not updated: task failed ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exit-codes/snapshots/single task failure returns task exit code.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exit-codes/snapshots/single task failure returns task exit code.snap index 97dde56a..cfd5239e 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exit-codes/snapshots/single task failure returns task exit code.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exit-codes/snapshots/single task failure returns task exit code.snap @@ -19,4 +19,5 @@ Task Details: ──────────────────────────────────────────────── [1] pkg-a#fail: ~/packages/pkg-a$ node -e "process.exit(42)" ✗ (exit code: 42) → Cache miss: no previous cache entry found + → Cache not updated: task failed ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/stdin-passthrough/snapshots/stdin passthrough to single task.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/stdin-passthrough/snapshots/stdin passthrough to single task.snap index e7355cdb..628ab1c2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/stdin-passthrough/snapshots/stdin passthrough to single task.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/stdin-passthrough/snapshots/stdin passthrough to single task.snap @@ -19,4 +19,5 @@ Task Details: ──────────────────────────────────────────────── [1] stdin-passthrough#echo-stdin: $ node -e "process.stdin.pipe(process.stdout)" ✓ → Cache miss: no previous cache entry found + → Cache not updated: stdin had data ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━