crates/dwarves-runa/tests/runs_on.rs
//! `runs_on` — WHERE a pipeline's steps run: `Container` (the default), `Vm`,
//! `Kubernetes`. Set at pipeline level, overridden per stage.
//!
//! Red-when-broken: every file written before `runs_on` existed must still load
//! (and mean Container), the setting must round-trip, a stage override must win,
//! and a value with no backend (`Kubernetes`) is refused BY NAME at load.
use dwarves_runa::{Pipeline, RunaError, RunsOn, ValidationError};
/// A pipeline as every `*.runa.ron` before `runs_on` was written.
const OLD: &str = r#"Pipeline(
name: "old",
stages: [
Stage(name: "build", steps: [ CargoBuild(profile: Release) ]),
Stage(name: "test", needs: ["build"], steps: [ Nextest(all_features: true) ]),
],
)"#;
const MIXED: &str = r#"Pipeline(
name: "mixed",
runs_on: Vm,
stages: [
Stage(name: "build", runs_on: Container, steps: [ CargoBuild(profile: Release) ]),
Stage(name: "smoke", needs: ["build"], steps: [ VmBoot ]),
],
)"#;
#[test]
fn a_file_without_runs_on_still_loads_and_runs_in_a_container() {
let p = Pipeline::from_ron_str(OLD).expect("an old file loads");
assert_eq!(p.runs_on, RunsOn::Container, "the default is Container");
assert_eq!(RunsOn::default(), RunsOn::Container);
for s in &p.stages {
assert_eq!(s.runs_on, None, "no stage override in an old file");
assert_eq!(p.runs_on_for(s), RunsOn::Container, "stage `{}`", s.name);
}
}
#[test]
fn a_stage_override_wins_over_the_pipeline_setting() {
let p = Pipeline::from_ron_str(MIXED).expect("loads");
assert_eq!(p.runs_on, RunsOn::Vm);
assert_eq!(p.stages[0].runs_on, Some(RunsOn::Container));
assert_eq!(p.runs_on_for(&p.stages[0]), RunsOn::Container, "the stage says Container");
assert_eq!(p.runs_on_for(&p.stages[1]), RunsOn::Vm, "no override: the pipeline's Vm");
}
#[test]
fn runs_on_round_trips() {
let p = Pipeline::from_ron_str(MIXED).expect("loads");
let again = Pipeline::from_ron_str(&p.to_ron_string().expect("serialise")).expect("reloads");
assert_eq!(again, p, "parse -> serialise -> parse is an identity with runs_on set");
}
#[test]
fn an_old_file_serialises_without_runs_on() {
// The default is not written back: an old file re-saved stays the file it was.
let p = Pipeline::from_ron_str(OLD).expect("loads");
let out = p.to_ron_string().expect("serialise");
assert!(!out.contains("runs_on"), "default runs_on leaked into the output:\n{out}");
assert_eq!(Pipeline::from_ron_str(&out).expect("reloads"), p);
}
#[test]
fn kubernetes_is_refused_by_name_at_load_until_it_has_a_backend() {
let at_pipeline = OLD.replace("name: \"old\",", "name: \"old\", runs_on: Kubernetes,");
match Pipeline::from_ron_str(&at_pipeline) {
Err(RunaError::Validation(e @ ValidationError::RunsOnNotWired { .. })) => {
assert_eq!(e, ValidationError::RunsOnNotWired { stage: None, runs_on: RunsOn::Kubernetes });
let msg = e.to_string();
assert!(msg.contains("Kubernetes"), "names the value: {msg}");
}
other => panic!("pipeline-level Kubernetes must be refused at load, got {other:?}"),
}
let at_stage = OLD.replace("Stage(name: \"test\",", "Stage(name: \"test\", runs_on: Kubernetes,");
match Pipeline::from_ron_str(&at_stage) {
Err(RunaError::Validation(e)) => {
assert_eq!(e, ValidationError::RunsOnNotWired { stage: Some("test".into()), runs_on: RunsOn::Kubernetes });
let msg = e.to_string();
assert!(msg.contains("Kubernetes") && msg.contains("`test`"), "names the value and the stage: {msg}");
}
other => panic!("stage-level Kubernetes must be refused at load, got {other:?}"),
}
assert!(!RunsOn::Kubernetes.has_backend());
assert!(RunsOn::Container.has_backend() && RunsOn::Vm.has_backend());
}
#[test]
fn a_misspelled_runs_on_value_is_refused() {
let bad = OLD.replace("name: \"old\",", "name: \"old\", runs_on: Docker,");
assert!(matches!(Pipeline::from_ron_str(&bad), Err(RunaError::Parse(_))), "an unknown value never falls back to the default");
}