use std::fs;
use crate::config::DogConfig;
use crate::deploy::test_support::{
FLOCKFILE, Fixture, Shepherd, commit_on_origin, fixture_with_previous_release,
};
use crate::deploy::{Outcome, deploy};
use crate::error::Error;
use crate::flockfile::OVERRIDE;
use crate::hook::Ending;
use crate::state::State;
use crate::{fixtures, swap};
fn operators_override(fixture: &Fixture, text: &str) {
fs::write(
fixture.origin.path().join(".gitignore"),
format!("{OVERRIDE}\n"),
)
.expect("write .gitignore");
fs::write(fixture.origin.path().join(OVERRIDE), text).expect("write override");
}
#[tokio::test]
async fn a_release_hook_runs_after_the_build_and_before_the_swap() {
let mut fixture = fixture_with_previous_release();
let previous = fixture.state.deployed.clone().expect("a previous release");
fs::write(
fixture.origin.path().join("Flockfile.toml"),
format!("{FLOCKFILE}\n[dog.deploy.build]\ncommand = 'touch built'\n"),
)
.expect("write Flockfile");
operators_override(
&fixture,
"[dog.deploy.release]\ncommand = 'readlink ../../current > current-at-hook.txt; \
ls built > build-seen-by-hook.txt'\n",
);
let second = commit_on_origin(&fixture, "second.txt");
let outcome = deploy(
&Shepherd::ready(),
&fixture.tree,
&mut fixture.state,
&fixtures::dog_config(),
)
.await
.expect("completes");
assert_eq!(
outcome,
Outcome::Deployed {
sha: second.clone()
}
);
let release = fixture.tree.release(&second);
let at_hook = fs::read_to_string(release.join("current-at-hook.txt")).expect("hook ran");
assert_eq!(
std::path::Path::new(at_hook.trim()),
fixture.tree.release(&previous)
);
assert!(
release.join("build-seen-by-hook.txt").is_file(),
"hook ran before the build"
);
assert_eq!(
swap::resolve(&fixture.tree.current()).unwrap(),
Some(release),
"and the deploy went on to swap"
);
}
#[tokio::test]
async fn a_failing_release_hook_never_moves_current_and_holds_the_sha() {
let mut fixture = fixture_with_previous_release();
let previous = fixture.state.deployed.clone().expect("a previous release");
operators_override(&fixture, "[dog.deploy.release]\ncommand = 'exit 4'\n");
let second = commit_on_origin(&fixture, "second.txt");
let daemon = Shepherd::ready();
let err = deploy(
&daemon,
&fixture.tree,
&mut fixture.state,
&fixtures::dog_config(),
)
.await
.expect_err("the hook fails");
assert!(
matches!(
err,
Error::Hook {
name: "release",
ending: Ending::Exited(Some(4))
}
),
"{err:?}"
);
assert_eq!(
swap::resolve(&fixture.tree.current()).unwrap(),
Some(fixture.tree.release(&previous))
);
assert_eq!(daemon.reload_count(), 0, "nothing may be reloaded");
assert_eq!(fixture.state.deployed.as_deref(), Some(previous.as_str()));
assert_eq!(fixture.state.failed.as_deref(), Some(second.as_str()));
}
#[tokio::test]
async fn a_failing_build_runs_no_release_hook() {
let mut fixture = fixture_with_previous_release();
fs::write(
fixture.origin.path().join("Flockfile.toml"),
format!("{FLOCKFILE}\n[dog.deploy.build]\ncommand = 'exit 3'\n"),
)
.expect("write Flockfile");
operators_override(
&fixture,
"[dog.deploy.release]\ncommand = 'touch hook-ran'\n",
);
let second = commit_on_origin(&fixture, "second.txt");
let err = deploy(
&Shepherd::ready(),
&fixture.tree,
&mut fixture.state,
&fixtures::dog_config(),
)
.await
.expect_err("the build fails");
assert!(matches!(err, Error::Build { status: Some(3) }), "{err:?}");
assert!(
!fixture.tree.release(&second).join("hook-ran").exists(),
"the hook ran after a failed build"
);
}
#[tokio::test]
async fn an_after_hook_runs_once_the_release_is_verified_and_recorded() {
let mut fixture = fixture_with_previous_release();
operators_override(
&fixture,
"[dog.deploy.after]\ncommand = 'cp ../../deploy.toml record-at-hook.toml; \
readlink ../../current > current-at-hook.txt; \
printenv SHEP_DEPLOY_SHEEP > sheep.txt; printenv SHEP_DEPLOY_SHA > sha.txt; \
printenv SHEP_DEPLOY_RELEASE > release.txt; echo ran >> runs.txt'\n",
);
let second = commit_on_origin(&fixture, "second.txt");
let daemon = Shepherd::ready();
let outcome = deploy(
&daemon,
&fixture.tree,
&mut fixture.state,
&fixtures::dog_config(),
)
.await
.expect("completes");
assert_eq!(
outcome,
Outcome::Deployed {
sha: second.clone()
}
);
let release = fixture.tree.release(&second);
let record = State::read(&release.join("record-at-hook.toml")).expect("the hook ran");
assert_eq!(record.deployed.as_deref(), Some(second.as_str()));
let at_hook = fs::read_to_string(release.join("current-at-hook.txt")).expect("current");
assert_eq!(std::path::Path::new(at_hook.trim()), release);
let seen = |name: &str| fs::read_to_string(release.join(name)).expect(name);
assert_eq!(seen("sheep.txt").trim(), fixture.tree.sheep());
assert_eq!(seen("sha.txt").trim(), second);
assert_eq!(
std::path::Path::new(seen("release.txt").trim())
.canonicalize()
.expect("exists"),
release.canonicalize().expect("canonical")
);
assert_eq!(seen("runs.txt"), "ran\n", "once per deploy");
assert_eq!(daemon.reload_count(), 1);
}
#[tokio::test(start_paused = true)]
async fn an_after_hook_does_not_run_when_the_release_is_rolled_back() {
let mut fixture = fixture_with_previous_release();
operators_override(
&fixture,
"[dog.deploy.after]\ncommand = 'touch after-ran'\n",
);
let second = commit_on_origin(&fixture, "second.txt");
let outcome = deploy(
&Shepherd::never_ready(),
&fixture.tree,
&mut fixture.state,
&fixtures::dog_config(),
)
.await
.expect("completes");
assert!(matches!(outcome, Outcome::RolledBack { .. }), "{outcome:?}");
assert!(
!fixture.tree.release(&second).join("after-ran").exists(),
"the after hook ran over a release that was rolled back"
);
}
#[tokio::test]
async fn a_failing_after_hook_still_reports_deployed_and_writes_no_hold() {
let mut fixture = fixture_with_previous_release();
operators_override(&fixture, "[dog.deploy.after]\ncommand = 'exit 5'\n");
let second = commit_on_origin(&fixture, "second.txt");
let daemon = Shepherd::ready();
let outcome = deploy(
&daemon,
&fixture.tree,
&mut fixture.state,
&fixtures::dog_config(),
)
.await
.expect("a failing after hook is not a failed deploy");
assert_eq!(
outcome,
Outcome::Deployed {
sha: second.clone()
}
);
assert_eq!(
swap::resolve(&fixture.tree.current()).unwrap(),
Some(fixture.tree.release(&second))
);
assert_eq!(daemon.reload_count(), 1, "nothing may be rolled back");
assert_eq!(fixture.state.deployed.as_deref(), Some(second.as_str()));
assert_eq!(fixture.state.failed, None);
let written = State::read(&fixture.tree.state_file()).expect("deploy.toml");
assert_eq!(written.deployed.as_deref(), Some(second.as_str()));
assert_eq!(written.failed, None);
}
#[tokio::test]
async fn an_after_hook_that_never_finishes_is_abandoned_and_the_deploy_is_still_deployed() {
let mut fixture = fixture_with_previous_release();
operators_override(
&fixture,
"[dog.deploy.after]\ncommand = 'sleep 600 & echo $! > descendant.pid; wait'\n",
);
let second = commit_on_origin(&fixture, "second.txt");
let config = DogConfig {
build_timeout: std::time::Duration::from_secs(2),
..fixtures::dog_config()
};
let outcome = deploy(
&Shepherd::ready(),
&fixture.tree,
&mut fixture.state,
&config,
)
.await
.expect("a timed-out after hook is not a failed deploy");
assert_eq!(
outcome,
Outcome::Deployed {
sha: second.clone()
}
);
assert_eq!(fixture.state.failed, None);
}
#[tokio::test]
async fn an_after_hook_does_not_run_when_the_record_cannot_be_written() {
let mut fixture = fixture_with_previous_release();
operators_override(
&fixture,
"[dog.deploy.after]\ncommand = 'touch after-ran'\n",
);
let second = commit_on_origin(&fixture, "second.txt");
let record = fixture.tree.state_file();
fs::remove_file(&record).expect("drop the record");
fs::create_dir(&record).expect("a directory where the record goes");
let outcome = deploy(
&Shepherd::ready(),
&fixture.tree,
&mut fixture.state,
&fixtures::dog_config(),
)
.await
.expect("the deploy landed whatever the record did");
assert_eq!(
outcome,
Outcome::Deployed {
sha: second.clone()
}
);
assert!(
!fixture.tree.release(&second).join("after-ran").exists(),
"the after hook ran against a record that does not name the new sha"
);
}