Skip to content
Merged
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
22 changes: 20 additions & 2 deletions crates/vite_task/src/session/event.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::time::Duration;
use std::{process::ExitStatus, time::Duration};

use bstr::BString;
// Re-export ExecutionItemDisplay from vite_task_plan since it's the canonical definition
Expand Down Expand Up @@ -44,6 +44,24 @@ pub enum CacheStatus {
Hit { replayed_duration: Duration },
}

/// Convert ExitStatus to an i32 exit code.
/// On Unix, if terminated by signal, returns 128 + signal_number.
pub fn exit_status_to_code(status: &ExitStatus) -> i32 {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status.code().unwrap_or_else(|| {
// Process was terminated by signal, use Unix convention: 128 + signal
status.signal().map(|sig| 128 + sig).unwrap_or(1)
})
}
#[cfg(not(unix))]
{
// Windows always has an exit code
status.code().unwrap_or(1)
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExecutionId(u32);

Expand All @@ -68,5 +86,5 @@ pub enum ExecutionEventKind {
Start { display: Option<ExecutionItemDisplay>, cache_status: CacheStatus },
Output { kind: OutputKind, content: BString },
Error { message: String },
Finish { status: Option<i32>, cache_update_status: CacheUpdateStatus },
Finish { status: Option<ExitStatus>, cache_update_status: CacheUpdateStatus },
}
6 changes: 3 additions & 3 deletions crates/vite_task/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ impl ExecutionContext<'_> {
self.event_handler.handle_event(ExecutionEvent {
execution_id,
kind: ExecutionEventKind::Finish {
status: Some(0),
status: None,
cache_update_status: CacheUpdateStatus::NotUpdated(
CacheNotUpdatedReason::CacheDisabled,
),
Expand Down Expand Up @@ -237,7 +237,7 @@ impl ExecutionContext<'_> {
self.event_handler.handle_event(ExecutionEvent {
execution_id,
kind: ExecutionEventKind::Finish {
status: Some(0),
status: None,
cache_update_status: CacheUpdateStatus::NotUpdated(
CacheNotUpdatedReason::CacheHit,
),
Expand Down Expand Up @@ -339,7 +339,7 @@ impl ExecutionContext<'_> {
self.event_handler.handle_event(ExecutionEvent {
execution_id,
kind: ExecutionEventKind::Finish {
status: result.exit_status.code(),
status: Some(result.exit_status),
cache_update_status,
},
});
Expand Down
38 changes: 23 additions & 15 deletions crates/vite_task/src/session/reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::{
collections::HashSet,
io::Write,
process::ExitStatus as StdExitStatus,
sync::{Arc, LazyLock},
time::Duration,
};
Expand All @@ -12,7 +13,10 @@ use vite_path::AbsolutePath;

use super::{
cache::{format_cache_status_inline, format_cache_status_summary},
event::{CacheStatus, ExecutionEvent, ExecutionEventKind, ExecutionId, ExecutionItemDisplay},
event::{
CacheStatus, ExecutionEvent, ExecutionEventKind, ExecutionId, ExecutionItemDisplay,
exit_status_to_code,
},
};

/// Wrap of `OwoColorize` that ignores style if `NO_COLOR` is set.
Expand Down Expand Up @@ -55,7 +59,8 @@ const CACHE_MISS_STYLE: Style = Style::new().purple();
struct ExecutionInfo {
display: Option<ExecutionItemDisplay>,
cache_status: CacheStatus, // Non-optional, determined at Start
exit_status: Option<i32>,
/// Exit status from the process. None means no process was spawned (cache hit or in-process).
exit_status: Option<StdExitStatus>,
error_message: Option<String>,
}

Expand Down Expand Up @@ -226,12 +231,11 @@ impl<W: Write> LabeledReporter<W> {
self.stats.failed += 1;
}

fn handle_finish(&mut self, execution_id: ExecutionId, status: Option<i32>) {
fn handle_finish(&mut self, execution_id: ExecutionId, status: Option<StdExitStatus>) {
// Update failure statistics
if let Some(s) = status {
if s != 0 {
self.stats.failed += 1;
}
// None means success (cache hit or in-process), Some checks the actual exit status
if status.is_some_and(|s| !s.success()) {
self.stats.failed += 1;
}

// Update execution info exit status
Expand Down Expand Up @@ -403,21 +407,23 @@ impl<W: Write> LabeledReporter<W> {
let _ = write!(self.writer, ": {}", command_display.style(COMMAND_STYLE));

// Execution result icon
match exec.exit_status {
Some(0) => {
// None means success (cache hit or in-process), Some checks actual status
match &exec.exit_status {
None => {
let _ = write!(self.writer, " {}", "✓".style(Style::new().green().bold()));
}
Some(status) if status.success() => {
let _ = write!(self.writer, " {}", "✓".style(Style::new().green().bold()));
}
Some(code) => {
Some(status) => {
let code = exit_status_to_code(status);
let _ = write!(
self.writer,
" {} {}",
"✗".style(Style::new().red().bold()),
format!("(exit code: {code})").style(Style::new().red())
);
}
None => {
let _ = write!(self.writer, " {}", "?".style(Style::new().bright_black()));
}
}
let _ = writeln!(self.writer);

Expand Down Expand Up @@ -536,11 +542,13 @@ impl<W: Write> Reporter for LabeledReporter<W> {
// 1. All tasks succeed → return Ok(())
// 2. Exactly one task failed → return Err with that task's exit code
// 3. More than one task failed → return Err(1)
// Note: None means success (cache hit or in-process)
let failed_exit_codes: Vec<i32> = self
.executions
.iter()
.filter_map(|exec| exec.exit_status)
.filter(|&status| status != 0)
.filter_map(|exec| exec.exit_status.as_ref())
.filter(|status| !status.success())
.map(exit_status_to_code)
.collect();

match failed_exit_codes.as_slice() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "signal-exit-test"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Tests exit code behavior for signal-terminated processes
# Unix-only: Windows doesn't have Unix signals, so exit codes differ

[[e2e]]
name = "signal terminated task returns non-zero exit code"
platform = "unix"
steps = [
"vite run abort # SIGABRT -> exit code 134",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/signal-exit
---
[134]> vite run abort # SIGABRT -> exit code 134
$ node -e "process.kill(process.pid, 6)"


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Vite+ Task Runner • Execution Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Statistics: 1 tasks • 0 cache hits • 1 cache misses • 1 failed
Performance: 0% cache hit rate

Task Details:
────────────────────────────────────────────────
[1] signal-exit-test#abort: $ node -e "process.kill(process.pid, 6)" ✗ (exit code: 134)
→ Cache miss: no previous cache entry found
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"tasks": {
"abort": {
"command": "node -e \"process.kill(process.pid, 6)\""
}
}
}
15 changes: 15 additions & 0 deletions crates/vite_task_bin/tests/e2e_snapshots/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ struct E2e {
#[serde(default)]
pub cwd: RelativePathBuf,
pub steps: Vec<Step>,
/// Optional platform filter: "unix" or "windows". If set, test only runs on that platform.
#[serde(default)]
pub platform: Option<Str>,
}

#[derive(serde::Deserialize, Default)]
Expand Down Expand Up @@ -165,6 +168,18 @@ async fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &Path, fixture_name

let mut e2e_count = 0u32;
for e2e in cases_file.e2e_cases {
// Skip test if platform doesn't match
if let Some(platform) = &e2e.platform {
let should_run = match platform.as_str() {
"unix" => cfg!(unix),
"windows" => cfg!(windows),
other => panic!("Unknown platform '{}' in test '{}'", other, e2e.name),
};
if !should_run {
continue;
}
}

let e2e_stage_path = tmpdir.join(format!("{}_e2e_stage_{}", fixture_name, e2e_count));
e2e_count += 1;
assert!(copy_dir(fixture_path, &e2e_stage_path).unwrap().is_empty());
Expand Down
Loading