diff options
author | Lars Wirzenius <liw@liw.fi> | 2024-03-29 08:01:01 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2024-03-29 10:20:49 +0200 |
commit | 8ed6851ef0015d8847e824e90d919b389c8a0c38 (patch) | |
tree | 4472a3cbe7b842becda74dfe9aef2fdf9ba8861e | |
parent | e474125273977d77c7a49cb76bffad39848d510d (diff) | |
download | ambient-driver-8ed6851ef0015d8847e824e90d919b389c8a0c38.tar.gz |
refactor: add type to capture the "run or don't run" decision
The existing logic is faulty, and hard for me to follow. The new type
has tests, to hopefully making it harder for me to break things again.
Signed-off-by: Lars Wirzenius <liw@liw.fi>
Sponsored-by: author
-rw-r--r-- | ambient-driver.md | 6 | ||||
-rw-r--r-- | src/run.rs | 329 |
2 files changed, 293 insertions, 42 deletions
diff --git a/ambient-driver.md b/ambient-driver.md index d78c2cb..073ab9c 100644 --- a/ambient-driver.md +++ b/ambient-driver.md @@ -167,9 +167,9 @@ given an Ambient VM image ambient.qcow2 given file multi.yaml given a directory foo given a directory bar -when I run env AMBIENT_LOG=info ambient-driver run --dry-run multi.yaml foo -then stderr contains "foo" -then stderr doesn't contain "bar" +when I run env AMBIENT_LOG=debug ambient-driver run --dry-run multi.yaml foo +then stderr contains "project foo: NOT running CI" +then stderr doesn't contain "project bar:" ~~~ ~~~{#multi.yaml .file .yaml} @@ -45,7 +45,7 @@ fn cmd_run(config: &EffectiveConfig, run: &RunCommand) -> Result<(), RunError> { let (do_run, mut state) = should_run(run, statedir, name, project)?; if do_run { - info!("{}: running CI on {}", name, project.source().display()); + info!("project {}: running CI", name); let artifactsdir = state.artifactsdir(); recreate_dir(&artifactsdir)?; @@ -131,6 +131,8 @@ fn cmd_run(config: &EffectiveConfig, run: &RunCommand) -> Result<(), RunError> { state.set_latest_commot(None); }; state.write_to_file()?; + } else { + info!("project {}: NOT running CI", name); } } @@ -143,49 +145,60 @@ fn should_run( name: &str, project: &Project, ) -> Result<(bool, State), RunError> { + let mut decision = Decision::default(); + let state = State::from_file(statedir, name)?; - let head = if is_git(project.source()) { - Some(git_head(project.source())?) + if let Some(latest_commit) = state.latest_commit() { + debug!("latest commit: {:?}", latest_commit); + decision = decision.latest_commit(latest_commit); } else { - None - }; - debug!("latest commit: {:?}", state.latest_commit()); - debug!("HEAD commit: {:?}", head); - - let mut do_run = false; - - if !is_git(project.source()) { - debug!("not a git repository: {}", project.source().display()); - do_run = true; - } else if !git_is_clean(project.source()) { - debug!( - "git repository is not clean: {}", - project.source().display() - ); - do_run = true; - } else if state.latest_commit() != head.as_deref() { - debug!("git repository has changed"); - do_run = true; - } - - if run.force() && run.dry_run() { - debug!("both --dry-run and --force used, --dry run wins"); - do_run = false; - } else if run.force() { - debug!("--force used"); - do_run = true; - } - - if do_run { - if run.dry_run() { - info!("would run CI on {} but --dry-run used", name); - } else { - info!("running CI on {}", name); - } + debug!("no latest commit stored"); + } + + let is_git = is_git(project.source()); + if is_git { + debug!("is a git repository"); + decision = decision.is_git(); + } else { + debug!("is not a git repository"); + decision = decision.is_not_git(); + } + + if !git_is_clean(project.source()) { + debug!("git repository is clean"); + decision = decision.is_clean(); + } else { + debug!("git repository is dirty"); + decision = decision.is_dirty(); + } + + if is_git { + let head = git_head(project.source())?; + debug!("current (HEAD) commit: {head}"); + decision = decision.current_commit(&head); + } else { + debug!("no current commit due to not git repository"); + } + + if run.dry_run() { + debug!("dry run requested"); + decision = decision.dry_run(); + } else { + debug!("no dry run requested"); + decision = decision.no_dry_run(); + } + + if run.force() { + debug!("forced run requested"); + decision = decision.force(); } else { - info!("skipping {}", name); + debug!("no forced run requested"); + decision = decision.no_force(); } + let do_run = decision.should_run() == ShouldRun::Run; + debug!("run? {do_run}"); + Ok((do_run, state)) } @@ -340,3 +353,241 @@ pub enum RunError { #[error("no state directory specified")] NoState, } + +#[derive(Debug, Default)] +struct Decision { + dry_run: Option<bool>, + force_run: Option<bool>, + is_git: Option<bool>, + latest_commit: Option<String>, + current_commit: Option<String>, + source_is_dirty: Option<bool>, +} + +impl Decision { + fn dry_run(mut self) -> Self { + self.dry_run = Some(true); + self + } + + fn no_dry_run(mut self) -> Self { + self.dry_run = Some(false); + self + } + + fn force(mut self) -> Self { + self.force_run = Some(true); + self + } + + fn no_force(mut self) -> Self { + self.force_run = Some(false); + self + } + + fn is_git(mut self) -> Self { + self.is_git = Some(true); + self + } + + fn is_not_git(mut self) -> Self { + self.is_git = Some(false); + self + } + + fn latest_commit(mut self, commit: &str) -> Self { + self.latest_commit = Some(commit.into()); + self + } + + fn current_commit(mut self, commit: &str) -> Self { + self.current_commit = Some(commit.into()); + self + } + + fn is_clean(mut self) -> Self { + self.source_is_dirty = Some(false); + self + } + + fn is_dirty(mut self) -> Self { + self.source_is_dirty = Some(true); + self + } + + fn should_run(&self) -> ShouldRun { + let dry_run = self.dry_run.unwrap(); + let force = self.force_run.unwrap(); + let is_git = self.is_git.unwrap(); + let dirty = self.source_is_dirty.unwrap(); + + if dry_run { + Self::log("dry run"); + ShouldRun::DontRun + } else if force { + Self::log("force"); + ShouldRun::Run + } else if !is_git { + Self::log("not git"); + ShouldRun::Run + } else if dirty { + Self::log("dirty"); + ShouldRun::Run + } else if self.current_commit == self.latest_commit { + Self::log("commits are equal"); + ShouldRun::DontRun + } else { + Self::log("nothing prevents run"); + ShouldRun::Run + } + } + + #[allow(unused_variables)] + fn log(msg: &str) { + #[cfg(test)] + println!("{}", msg); + } +} + +// Use a custom enum to avoid confusing reader with interpreting +// boolean values, specially in tests. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum ShouldRun { + Run, + DontRun, +} + +#[cfg(test)] +mod test_run_decision { + use super::{Decision, ShouldRun}; + + #[test] + fn is_not_git() { + let d = Decision::default() + .no_dry_run() + .no_force() + .is_not_git() + .is_clean(); + assert_eq!(d.should_run(), ShouldRun::Run); + } + + #[test] + fn unchanged() { + let d = Decision::default() + .no_dry_run() + .no_force() + .is_git() + .is_clean() + .latest_commit("abcd") + .current_commit("abcd"); + assert_eq!(d.should_run(), ShouldRun::DontRun); + } + + #[test] + fn unchanged_with_force() { + let d = Decision::default() + .no_dry_run() + .force() + .is_git() + .is_clean() + .latest_commit("abcd") + .current_commit("abcd"); + assert_eq!(d.should_run(), ShouldRun::Run); + } + + #[test] + fn unchanged_commit_but_dirty() { + let d = Decision::default() + .no_dry_run() + .no_force() + .is_git() + .is_dirty() + .latest_commit("abcd") + .current_commit("abcd"); + assert_eq!(d.should_run(), ShouldRun::Run); + } + + #[test] + fn commit_changed() { + let d = Decision::default() + .no_dry_run() + .no_force() + .is_git() + .is_clean() + .latest_commit("abcd") + .current_commit("efgh"); + assert_eq!(d.should_run(), ShouldRun::Run); + } + + #[test] + fn dry_run_for_unchanged() { + let d = Decision::default() + .dry_run() + .no_force() + .is_git() + .is_clean() + .latest_commit("abcd") + .current_commit("abcd"); + assert_eq!(d.should_run(), ShouldRun::DontRun); + } + + #[test] + fn dry_run_for_unchanged_but_dirty() { + let d = Decision::default() + .dry_run() + .no_force() + .is_git() + .is_dirty() + .latest_commit("abcd") + .current_commit("efgh"); + assert_eq!(d.should_run(), ShouldRun::DontRun); + } + + #[test] + fn dry_run_for_commit_changed() { + let d = Decision::default() + .dry_run() + .no_force() + .is_git() + .is_clean() + .latest_commit("abcd") + .current_commit("efgh"); + assert_eq!(d.should_run(), ShouldRun::DontRun); + } + + #[test] + fn dry_run_for_unchanged_with_force() { + let d = Decision::default() + .dry_run() + .no_force() + .is_git() + .is_clean() + .latest_commit("abcd") + .current_commit("abcd"); + assert_eq!(d.should_run(), ShouldRun::DontRun); + } + + #[test] + fn dry_run_for_unchanged_but_dirty_with_force() { + let d = Decision::default() + .dry_run() + .no_force() + .is_git() + .is_dirty() + .latest_commit("abcd") + .current_commit("efgh"); + assert_eq!(d.should_run(), ShouldRun::DontRun); + } + + #[test] + fn dry_run_for_commit_changed_with_force() { + let d = Decision::default() + .dry_run() + .no_force() + .is_git() + .is_clean() + .latest_commit("abcd") + .current_commit("efgh"); + assert_eq!(d.should_run(), ShouldRun::DontRun); + } +} |