-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathmod.rs
More file actions
337 lines (304 loc) · 15.3 KB
/
mod.rs
File metadata and controls
337 lines (304 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
//! Reporter traits and implementations for rendering execution events.
//!
//! This module provides a typestate-based reporter system with three phases:
//!
//! 1. [`GraphExecutionReporterBuilder`] — created before execution begins.
//! Transitions to [`GraphExecutionReporter`] when `build()` is called.
//!
//! 2. [`GraphExecutionReporter`] — creates [`LeafExecutionReporter`]
//! instances for individual leaf executions via `new_leaf_execution()`. Finalized with `finish()`.
//!
//! 3. [`LeafExecutionReporter`] — handles events for a single leaf execution (output streaming,
//! cache status, errors). Finalized with `finish()`.
//!
//! Two concrete implementations are provided in child modules:
//!
//! - [`plain::PlainReporter`] — a standalone [`LeafExecutionReporter`] for single-leaf execution
//! (e.g., `execute_synthetic`). Self-contained, no shared state, no summary.
//!
//! - [`labeled::LabeledReporterBuilder`] / [`labeled::LabeledGraphReporter`] /
//! `LabeledLeafReporter` — a full graph-aware reporter family. Tracks stats across multiple
//! leaf executions, prints command lines with cache status, and renders a summary at the end.
mod labeled;
mod plain;
pub mod summary;
// Re-export the concrete implementations so callers can use `reporter::PlainReporter`
// and `reporter::LabeledReporterBuilder` without navigating into child modules.
use std::{process::ExitStatus as StdExitStatus, sync::LazyLock};
pub use labeled::LabeledReporterBuilder;
use owo_colors::{Style, Styled};
pub use plain::PlainReporter;
use tokio::io::AsyncWrite;
use vite_path::AbsolutePath;
use vite_str::Str;
use vite_task_plan::{ExecutionItemDisplay, LeafExecutionKind};
use super::{
cache::format_cache_status_inline,
event::{CacheStatus, CacheUpdateStatus, ExecutionError},
};
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Exit status type
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// Exit status code for task execution.
///
/// Wraps a `u8` exit code. `0` means success, non-zero means failure.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExitStatus(pub u8);
impl ExitStatus {
pub const FAILURE: Self = Self(1);
pub const SUCCESS: Self = Self(0);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Stdio suggestion and configuration
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// Suggestion from the reporter about what stdio mode to use for a spawned process.
///
/// The actual stdio mode is determined by [`execute_spawn`](super::execute::execute_spawn)
/// based on this suggestion AND whether caching is enabled for the task:
/// - `Inherited` is only honoured when caching is disabled (`cache_metadata` is `None`).
/// With caching enabled, the execution engine overrides to `Piped` so that output can
/// be captured for the cache.
/// - `Piped` is always respected as-is.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StdioSuggestion {
/// stdin is `/dev/null`, stdout and stderr are piped into the reporter's
/// [`AsyncWrite`] streams. Used when multiple tasks run concurrently and
/// stdio should not be shared.
Piped,
/// All three file descriptors (stdin, stdout, stderr) are inherited from the
/// parent process, allowing interactive input and direct terminal output.
/// Only effective when caching is disabled for the task.
Inherited,
}
/// Stdio configuration returned by [`LeafExecutionReporter::start`].
///
/// Contains the reporter's suggestion for the stdio mode together with two
/// async writers that receive the child process's stdout and stderr when the
/// execution engine decides to use piped mode. The writers are always provided
/// because the engine may override the suggestion (e.g. when caching forces
/// piped mode even though the reporter suggested inherited).
pub struct StdioConfig {
/// The reporter's preferred stdio mode.
pub suggestion: StdioSuggestion,
/// Async writer for the child process's stdout (used in piped mode and cache replay).
pub stdout_writer: Box<dyn AsyncWrite + Unpin>,
/// Async writer for the child process's stderr (used in piped mode and cache replay).
pub stderr_writer: Box<dyn AsyncWrite + Unpin>,
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Typestate traits
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// Builder for creating a [`GraphExecutionReporter`].
///
/// This is the initial state before the execution graph is known. The `build` method
/// transitions to the [`GraphExecutionReporter`] phase.
pub trait GraphExecutionReporterBuilder {
/// Create a [`GraphExecutionReporter`].
fn build(self: Box<Self>) -> Box<dyn GraphExecutionReporter>;
}
/// Reporter for an entire graph execution session.
///
/// Creates [`LeafExecutionReporter`] instances for individual leaf executions
/// and finalizes the session with `finish()`.
#[async_trait::async_trait(?Send)]
pub trait GraphExecutionReporter {
/// Create a new leaf execution reporter for the given leaf.
///
/// `all_ancestors_single_node` is `true` when every execution graph in
/// the ancestry chain (root + all nested `Expanded` parents) contains
/// exactly one node. The reporter may use this to decide stdio mode
/// (e.g. suggesting inherited stdio for a single spawned process).
fn new_leaf_execution(
&mut self,
display: &ExecutionItemDisplay,
leaf_kind: &LeafExecutionKind,
all_ancestors_single_node: bool,
) -> Box<dyn LeafExecutionReporter>;
/// Finalize the graph execution session.
///
/// Leaf-level errors are already tracked internally by the reporter via the
/// leaf reporter's `finish()` method. Graph-level errors (cycle detection) are
/// now caught at plan time and never reach the reporter.
///
/// Returns `Ok(())` if all tasks succeeded, or `Err(ExitStatus)` to indicate the
/// process should exit with the given status code.
async fn finish(self: Box<Self>) -> Result<(), ExitStatus>;
}
/// Reporter for a single leaf execution (one spawned process or in-process command).
///
/// Lifecycle: `start()` → `finish()`.
///
/// `start()` may not be called before `finish()` if an error occurs before the cache
/// status is determined (e.g., cache lookup failure).
#[async_trait::async_trait(?Send)]
pub trait LeafExecutionReporter {
/// Report that execution is starting with the given cache status.
///
/// Called after the cache lookup completes, before any output is produced.
/// Returns a [`StdioConfig`] containing:
/// - The reporter's stdio mode suggestion (inherited or piped).
/// - Two [`AsyncWrite`] streams for receiving the child's stdout and stderr
/// (used when the execution engine decides on piped mode, or for cache replay).
///
/// The execution engine decides the actual stdio mode based on the suggestion
/// AND whether caching is enabled — inherited stdio is only used when the
/// suggestion is [`StdioSuggestion::Inherited`] AND the task has no cache
/// metadata (caching disabled).
async fn start(&mut self, cache_status: CacheStatus) -> StdioConfig;
/// Finalize this leaf execution.
///
/// - `status`: The process exit status, or `None` for cache hits and in-process commands.
/// - `cache_update_status`: Whether the cache was updated after execution.
/// - `error`: If `Some`, an error occurred during this leaf's execution (cache lookup
/// failure, spawn failure, fingerprint creation failure, cache update failure).
///
/// This method consumes the reporter — no further calls are possible after `finish()`.
async fn finish(
self: Box<Self>,
status: Option<StdExitStatus>,
cache_update_status: CacheUpdateStatus,
error: Option<ExecutionError>,
);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Shared display helpers
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// Wrap of `OwoColorize` that ignores style if `NO_COLOR` is set.
trait ColorizeExt {
fn style(&self, style: Style) -> Styled<&Self>;
}
impl<T: owo_colors::OwoColorize> ColorizeExt for T {
fn style(&self, style: Style) -> Styled<&Self> {
static NO_COLOR: LazyLock<bool> =
LazyLock::new(|| std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()));
owo_colors::OwoColorize::style(self, if *NO_COLOR { Style::new() } else { style })
}
}
const COMMAND_STYLE: Style = Style::new().blue();
const CACHE_MISS_STYLE: Style = Style::new().purple();
/// Format the display's cwd as a string relative to the workspace root.
/// Returns an empty string if the cwd equals the workspace root.
fn format_cwd_relative(display: &ExecutionItemDisplay, workspace_path: &AbsolutePath) -> Str {
let cwd_relative = if let Ok(Some(rel)) = display.cwd.strip_prefix(workspace_path) {
Str::from(rel.as_str())
} else {
Str::default()
};
if cwd_relative.is_empty() { Str::default() } else { vite_str::format!("~/{cwd_relative}") }
}
/// Format the command string with cwd prefix for display (e.g., `~/packages/lib$ vitest run`).
fn format_command_display(display: &ExecutionItemDisplay, workspace_path: &AbsolutePath) -> Str {
let cwd_str = format_cwd_relative(display, workspace_path);
vite_str::format!("{cwd_str}$ {}", display.command)
}
/// Format the command line with optional inline cache status.
///
/// This is called during `start()` to show the user what command is being executed
/// and its cache status. The caller writes the returned string to the async writer.
fn format_command_with_cache_status(
display: &ExecutionItemDisplay,
workspace_path: &AbsolutePath,
cache_status: &CacheStatus,
) -> Str {
let command_str = format_command_display(display, workspace_path);
format_cache_status_inline(cache_status).map_or_else(
|| vite_str::format!("{}\n", command_str.style(COMMAND_STYLE)),
|inline_status| {
// Apply styling based on cache status type
let styled_status = match cache_status {
CacheStatus::Hit { .. } => inline_status.style(Style::new().green().dimmed()),
CacheStatus::Miss(_) => inline_status.style(CACHE_MISS_STYLE.dimmed()),
CacheStatus::Disabled(_) => inline_status.style(Style::new().bright_black()),
};
vite_str::format!("{} {}\n", command_str.style(COMMAND_STYLE), styled_status)
},
)
}
/// Format an error message in red with an error icon.
fn format_error_message(message: &str) -> Str {
vite_str::format!(
"{} {}\n",
"✗".style(Style::new().red().bold()),
message.style(Style::new().red())
)
}
/// Format the "cache hit, logs replayed" message for synthetic executions without display info.
fn format_cache_hit_message() -> Str {
vite_str::format!("{}\n", "✓ cache hit, logs replayed".style(Style::new().green().dimmed()))
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Test fixtures (shared by child module tests)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#[cfg(test)]
pub mod test_fixtures {
use std::{collections::BTreeMap, sync::Arc};
use vite_path::AbsolutePath;
use vite_task_graph::display::TaskDisplay;
use vite_task_plan::{
ExecutionItem, ExecutionItemDisplay, ExecutionItemKind, InProcessExecution,
LeafExecutionKind, SpawnCommand, SpawnExecution, TaskExecution,
};
/// Create a dummy `AbsolutePath` for test fixtures.
pub fn test_path() -> Arc<AbsolutePath> {
#[cfg(unix)]
{
Arc::from(AbsolutePath::new("/test").unwrap())
}
#[cfg(windows)]
{
Arc::from(AbsolutePath::new("C:\\test").unwrap())
}
}
/// Create a dummy `TaskDisplay` for test fixtures.
pub fn test_task_display(name: &str) -> TaskDisplay {
TaskDisplay {
package_name: "pkg".into(),
task_name: name.into(),
package_path: test_path(),
}
}
/// Create a dummy `ExecutionItemDisplay` for test fixtures.
pub fn test_display(name: &str) -> ExecutionItemDisplay {
ExecutionItemDisplay {
task_display: test_task_display(name),
command: name.into(),
and_item_index: None,
cwd: test_path(),
}
}
/// Create a `TaskExecution` with a single spawn leaf.
pub fn spawn_task(name: &str) -> TaskExecution {
TaskExecution {
task_display: test_task_display(name),
items: vec![ExecutionItem {
execution_item_display: test_display(name),
kind: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(SpawnExecution {
cache_metadata: None,
spawn_command: SpawnCommand {
program_path: test_path(),
args: Arc::from([]),
all_envs: Arc::new(BTreeMap::new()),
cwd: test_path(),
},
})),
}],
}
}
/// Create a `TaskExecution` with a single in-process leaf (echo).
pub fn in_process_task(name: &str) -> TaskExecution {
TaskExecution {
task_display: test_task_display(name),
items: vec![ExecutionItem {
execution_item_display: test_display(name),
kind: ExecutionItemKind::Leaf(LeafExecutionKind::InProcess(
InProcessExecution::get_builtin_execution(
"echo",
["hello"].iter(),
&test_path(),
)
.unwrap(),
)),
}],
}
}
}