Skip to content

Commit 1b08a61

Browse files
refactor(hm-exec): Replace raw-string-literal job-state match with a typed classifier (#124)
1 parent 3ae2129 commit 1b08a61

1 file changed

Lines changed: 51 additions & 13 deletions

File tree

crates/hm-exec/src/cloud/watch.rs

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use harmont_cloud::{
1717
HarmontClient, HarmontError,
1818
logs::{LogEvent, StreamKind},
1919
models::{build_is_terminal, job_is_terminal},
20+
types::JobState,
2021
};
2122
use hm_plugin_protocol::events::{BuildEvent, PlanSummary, StdStream};
2223
use uuid::Uuid;
@@ -56,6 +57,27 @@ fn duration_ms(start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> u64
5657
}
5758
}
5859

60+
/// Whether a job has reached a state where its logs exist (running or already
61+
/// terminal), and so a log stream should be started for it.
62+
///
63+
/// Matching the typed [`JobState`] enum (rather than `to_string()`/`as_str()`
64+
/// against string literals) makes the set of states exhaustive: when the cloud
65+
/// adds a new `JobState` variant the compiler forces this decision to be
66+
/// revisited, and a misspelled state can no longer silently drop a job's logs.
67+
const fn job_logs_available(state: JobState) -> bool {
68+
match state {
69+
JobState::Running
70+
| JobState::Passed
71+
| JobState::Failed
72+
| JobState::TimedOut
73+
| JobState::Canceling
74+
| JobState::Canceled
75+
| JobState::TimingOut => true,
76+
// No logs yet (not started) or never produced (skipped).
77+
JobState::Pending | JobState::Scheduled | JobState::Assigned | JobState::Skipped => false,
78+
}
79+
}
80+
5981
/// Map a terminal build state to the process exit code the renderer and the
6082
/// `hm run` driver use. `passed` → 0, `canceled` → 130 (SIGINT-cancel, mirrors
6183
/// [`crate::BuildStatus::Canceled`]), everything else (`failed`, and any
@@ -131,18 +153,7 @@ pub async fn watch_build(
131153
// state where logs exist (running or already terminal).
132154
let jobs = client.list_jobs(org, pipeline, number).await?;
133155
for job in &jobs {
134-
let state = job.state.to_string();
135-
let logs_available = matches!(
136-
state.as_str(),
137-
"running"
138-
| "passed"
139-
| "failed"
140-
| "timed_out"
141-
| "canceling"
142-
| "canceled"
143-
| "timing_out"
144-
);
145-
if logs_available && streaming.insert(job.id) {
156+
if job_logs_available(job.state) && streaming.insert(job.id) {
146157
let name = job.name.clone().unwrap_or_else(|| "job".to_string());
147158
let idx = *chain_idx.entry(job.id).or_insert_with(|| {
148159
let i = next_idx;
@@ -400,7 +411,34 @@ async fn emit(
400411

401412
#[cfg(test)]
402413
mod tests {
403-
use super::exit_code_for_state;
414+
use super::{JobState, exit_code_for_state, job_logs_available};
415+
416+
#[test]
417+
fn logs_available_for_running_and_terminal_states() {
418+
for state in [
419+
JobState::Running,
420+
JobState::Passed,
421+
JobState::Failed,
422+
JobState::TimedOut,
423+
JobState::Canceling,
424+
JobState::Canceled,
425+
JobState::TimingOut,
426+
] {
427+
assert!(job_logs_available(state), "expected logs for {state}");
428+
}
429+
}
430+
431+
#[test]
432+
fn no_logs_before_start_or_when_skipped() {
433+
for state in [
434+
JobState::Pending,
435+
JobState::Scheduled,
436+
JobState::Assigned,
437+
JobState::Skipped,
438+
] {
439+
assert!(!job_logs_available(state), "expected no logs for {state}");
440+
}
441+
}
404442

405443
#[test]
406444
fn passed_is_zero_canceled_is_130_else_is_one() {

0 commit comments

Comments
 (0)