summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@noreply.codeberg.org>2024-03-29 08:26:23 +0000
committerLars Wirzenius <liw@noreply.codeberg.org>2024-03-29 08:26:23 +0000
commit9f81795ce701a917e8f2db5f4e3ec501729e4bf0 (patch)
tree4472a3cbe7b842becda74dfe9aef2fdf9ba8861e
parente474125273977d77c7a49cb76bffad39848d510d (diff)
parent8ed6851ef0015d8847e824e90d919b389c8a0c38 (diff)
downloadambient-driver-9f81795ce701a917e8f2db5f4e3ec501729e4bf0.tar.gz
Merge pull request 'refactor: add type to capture the "run or don't run" decision' (#96) from run-decision-test into main
Reviewed-on: https://codeberg.org/ambient/ambient-driver/pulls/96
-rw-r--r--ambient-driver.md6
-rw-r--r--src/run.rs329
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}
diff --git a/src/run.rs b/src/run.rs
index b3acb86..5426f18 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -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);
+ }
+}