alba_engine/event.rs
1//! What a run tells the outside world: the [`RunEvent`] stream a run emits
2//! while it happens, and the [`RunSummary`] it ends with.
3//!
4//! This is the only contract between execution and display: the headless
5//! CLI renderer today, and a TUI later, are two consumers of the same
6//! stream. Nothing here knows how a
7//! beam runs — [`crate::scheduler`] produces these values, and the channel
8//! they travel on is unbounded, so a slow consumer never stalls the run.
9
10use std::time::Duration;
11
12use alba_core::BeamId;
13use alba_executors::OutputLine;
14
15/// One thing that happened during a run.
16///
17/// Per beam, the order is `BeamStarted`, then every `BeamOutput`, then
18/// `BeamFinished` — a beam that runs several commands still emits exactly
19/// one started/finished pair, since the individual commands are an
20/// implementation detail of the beam. A cache hit follows the same shape
21/// with `BeamCached` in place of `BeamStarted`: the same replayed
22/// `BeamOutput` lines, then `BeamFinished` with [`BeamStatus::Cached`]. A
23/// beam that never ran (cancelled before it acquired a slot, or skipped
24/// because a dependency failed) emits only `BeamFinished` with
25/// [`BeamStatus::Cancelled`]. Events from different beams interleave
26/// freely; `RunStarted` is first *per run*, before any
27/// `BeamStarted`/`BeamCached`; `RunFinished` is last *per run* — in a watch
28/// session, it is followed by `WatchWaiting` and, once a change is seen,
29/// `WatchTriggered` before the next run's events begin.
30#[derive(Debug, Clone)]
31pub enum RunEvent {
32 /// A run is beginning. First event of every run, before any
33 /// `BeamStarted`/`BeamCached`, carrying the snapshot of what this run
34 /// will schedule: the targets, the union of their subgraphs in plan
35 /// order, and the dependency edges between members of that subgraph. A
36 /// snapshot rather than something the consumer looks up, because a
37 /// watch session reloads the Beamfile mid-session — a run's graph is
38 /// not known once and for all.
39 RunStarted {
40 /// The beams the run was asked for: one for `alba run <beam>`,
41 /// any number for an affected run, none for an affected run that
42 /// found nothing to do.
43 targets: Vec<BeamId>,
44 /// The git reference an affected run was computed against, when
45 /// it was one.
46 affected_by: Option<String>,
47 beams: Vec<BeamId>,
48 /// `(beam, dependency)`: the first needs the second. Both ends are
49 /// always members of `beams` — the subgraph is transitively closed.
50 edges: Vec<(BeamId, BeamId)>,
51 },
52 BeamStarted {
53 id: BeamId,
54 },
55 /// A cache hit: emitted instead of `BeamStarted` for a beam that is
56 /// skipped, followed by its replayed `BeamOutput` lines and a
57 /// `BeamFinished` with [`BeamStatus::Cached`] carrying the original
58 /// run's duration.
59 BeamCached {
60 id: BeamId,
61 },
62 BeamOutput {
63 id: BeamId,
64 line: OutputLine,
65 /// True for a line replayed from the cache rather than produced
66 /// by a live command — CI tooling parsing the JSON stream needs
67 /// to tell the two apart.
68 replayed: bool,
69 },
70 BeamFinished {
71 id: BeamId,
72 status: BeamStatus,
73 duration: Duration,
74 },
75 RunFinished {
76 summary: RunSummary,
77 },
78 /// Watch mode only: the session is waiting, having finished a run or
79 /// given up on starting one. `files` is how many files the watched
80 /// `inputs` currently resolve to — the status line's number, and `0`
81 /// for a session parked on a project that will not load, which
82 /// resolves nothing. The first one a session emits also announces the
83 /// session itself.
84 WatchWaiting {
85 files: usize,
86 },
87 /// Watch mode only: relevant changes started a new run. `paths` are
88 /// project-root-relative display strings; empty means the watcher
89 /// overflowed and could not say what changed (rescan). Emitted after
90 /// the previous run's `RunFinished` — when that summary is full of
91 /// cancelled beams, this event is what marks the run as interrupted
92 /// by the watch rather than abandoned by the user.
93 WatchTriggered {
94 paths: Vec<String>,
95 },
96 /// The session cannot work from its project: the Beamfile stopped
97 /// loading, or a run could not be carried out (unknown target after a
98 /// rename, an unschedulable beam). Carries the *rendered* diagnostic —
99 /// caret, spans, help — because rendering lives above this crate and is
100 /// done by the session's `render_error` callback; the event exists so a
101 /// stream consumer that never sees stderr still learns why nothing runs.
102 ProjectBroken {
103 diagnostic: String,
104 },
105}
106
107/// How a beam ended.
108///
109/// `Failed` and `FailedAllowed` are the same event seen through the beam's
110/// `allow_failure` flag: a `FailedAllowed` beam counts as satisfied for its
111/// dependents, never triggers fail-fast, and does not affect the run's exit
112/// code. `Cancelled` covers both reasons a beam can be skipped: a
113/// dependency that failed or was itself cancelled, and a run-wide stop
114/// (fail-fast, or the caller's cancellation token).
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum BeamStatus {
117 Succeeded,
118 /// A cache hit: counts as satisfied for this beam's dependents and as
119 /// success for the run's exit code, exactly like `Succeeded`.
120 Cached,
121 Failed {
122 exit_code: i32,
123 },
124 FailedAllowed {
125 exit_code: i32,
126 },
127 Cancelled,
128}
129
130/// What a whole run amounts to: every beam of the target's subgraph sorted
131/// into the bucket it ended in, plus how long the run took.
132///
133/// The five vectors are in the project's declaration order, never in
134/// completion order — a run is a concurrent thing, and ordering the summary
135/// by whichever task happened to finish first would make it differ from run
136/// to run for the same project.
137#[derive(Debug, Clone, Default)]
138pub struct RunSummary {
139 pub succeeded: Vec<BeamId>,
140 pub cached: Vec<BeamId>,
141 pub failed: Vec<BeamId>,
142 pub failed_allowed: Vec<BeamId>,
143 pub cancelled: Vec<BeamId>,
144 pub duration: Duration,
145}
146
147impl RunSummary {
148 /// The process exit code this run should produce: 0 unless a beam
149 /// failed outright. An allowed failure and a cancellation are both
150 /// deliberately 0 here; Alba's own errors (a Beamfile that does not
151 /// load, an unschedulable run) exit 2 and never reach a summary.
152 pub fn exit_code(&self) -> i32 {
153 if self.failed.is_empty() { 0 } else { 1 }
154 }
155
156 /// Files `id` under `status`. The single place a status becomes a
157 /// summary bucket, so the mapping cannot drift between call sites.
158 pub(crate) fn record(&mut self, id: BeamId, status: &BeamStatus) {
159 match status {
160 BeamStatus::Succeeded => self.succeeded.push(id),
161 BeamStatus::Cached => self.cached.push(id),
162 BeamStatus::Failed { .. } => self.failed.push(id),
163 BeamStatus::FailedAllowed { .. } => self.failed_allowed.push(id),
164 BeamStatus::Cancelled => self.cancelled.push(id),
165 }
166 }
167}