use std::cell::{Cell, RefCell};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use shep_client::shep_core::config::{AppConfig, DogTable};
use tempfile::TempDir;
use crate::config::DogConfig;
use crate::daemon::Daemon;
use crate::error::Error;
use crate::settings::{self, Settings};
use crate::state::{State, Verify, Watch};
pub const SHA: &str = "0123456789abcdef0123456789abcdef01234567";
pub const OTHER_SHA: &str = "a1b2c3a1b2c3a1b2c3a1b2c3a1b2c3a1b2c3a1b2";
pub const TEST_BUDGET: Duration = Duration::from_secs(60);
pub fn dog_config() -> DogConfig {
DogConfig {
interval: Duration::from_secs(30),
retention: 5,
git_timeout: Duration::from_secs(60),
build_timeout: Duration::from_secs(60),
passthrough: Vec::new(),
}
}
pub fn state() -> State {
State {
remote: "https://example.com/x".to_owned(),
branch: "main".to_owned(),
deployed: None,
failed: None,
verify: Verify::default(),
watch: Watch::default(),
origin_cwd: None,
origin_script: None,
checkout: PathBuf::from("/srv/x"),
origin: None,
staged: None,
}
}
#[track_caller]
pub fn run_git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.current_dir(dir)
.args(args)
.status()
.expect("spawn git");
assert!(status.success(), "git {args:?} failed in {}", dir.display());
}
#[track_caller]
pub fn head_of(dir: &Path) -> String {
let out = Command::new("git")
.current_dir(dir)
.args(["rev-parse", "HEAD"])
.output()
.expect("rev-parse");
assert!(
out.status.success(),
"git rev-parse HEAD failed in {}: {}",
dir.display(),
String::from_utf8_lossy(&out.stderr).trim()
);
String::from_utf8(out.stdout)
.expect("utf-8 sha")
.trim()
.to_owned()
}
#[track_caller]
pub fn write_files(dir: &Path, files: &[(&str, &str)]) {
for (name, contents) in files {
let full = dir.join(name);
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent).expect("create fixture parent");
}
std::fs::write(&full, contents).expect("write fixture file");
}
}
#[track_caller]
pub fn fixture_release(files: &[(&str, &str)]) -> TempDir {
let dir = tempfile::tempdir().expect("tempdir");
write_files(dir.path(), files);
dir
}
#[track_caller]
pub fn empty_checkout() -> TempDir {
let dir = tempfile::tempdir().expect("tempdir");
run_git(dir.path(), &["init", "-q", "-b", "main"]);
run_git(dir.path(), &["config", "user.email", "test@example.com"]);
run_git(dir.path(), &["config", "user.name", "test"]);
dir
}
#[track_caller]
pub fn commit(dir: &Path, files: &[(&str, &str)], message: &str) {
write_files(dir, files);
run_git(dir, &["add", "."]);
run_git(dir, &["commit", "-q", "-m", message]);
}
#[track_caller]
pub fn checkout(files: &[(&str, &str)]) -> TempDir {
let dir = empty_checkout();
commit(dir.path(), files, "first");
dir
}
macro_rules! daemon_methods {
(@method dog_config, $body:expr) => {
async fn dog_config(&self, _name: &str) -> Result<String, crate::error::Error> {
$body
}
};
(@method list_flock, $body:expr) => {
async fn list_flock(
&self,
) -> Result<Vec<shep_client::shep_core::protocol::ProcessInfo>, crate::error::Error>
{
$body
}
};
(@method describe, $body:expr) => {
async fn describe(
&self,
_sheep: &str,
) -> Result<Vec<shep_client::shep_core::protocol::ProcessInfo>, crate::error::Error>
{
$body
}
};
(@method start, $body:expr) => {
async fn start(
&self,
_apps: Vec<shep_client::shep_core::config::AppConfig>,
) -> Result<Vec<u32>, crate::error::Error> {
$body
}
};
(@method delete, $body:expr) => {
async fn delete(&self, _id: u32) -> Result<(), crate::error::Error> {
$body
}
};
(@method reload, $body:expr) => {
async fn reload(&self, _sheep: &str) -> Result<(), crate::error::Error> {
$body
}
};
(@method restart, $body:expr) => {
async fn restart(&self, _sheep: &str) -> Result<(), crate::error::Error> {
$body
}
};
(@method save_roll, $body:expr) => {
async fn save_roll(&self) -> Result<std::path::PathBuf, crate::error::Error> {
$body
}
};
(@method sheep_config, $body:expr) => {
async fn sheep_config(
&self,
_sheep: &str,
) -> Result<shep_client::shep_core::config::AppConfig, crate::error::Error> {
$body
}
};
(@method set_sheep_dog_settings, $body:expr) => {
async fn set_sheep_dog_settings(
&self,
_sheep: &str,
_dog: &str,
_table: shep_client::shep_core::config::DogTable,
) -> Result<(), crate::error::Error> {
$body
}
};
(@method set_smit, $body:expr) => {
async fn set_smit(&self, _sheep: &str, _text: &str) -> Result<(), crate::error::Error> {
$body
}
};
(unimplemented; $($method:ident),* $(,)?) => {
$(crate::fixtures::daemon_methods!(@method $method, unimplemented!(stringify!($method)));)*
};
(answering $body:expr; $($method:ident),* $(,)?) => {
$(crate::fixtures::daemon_methods!(@method $method, $body);)*
};
}
pub(crate) use daemon_methods;
#[track_caller]
pub fn app_named(sheep: &str) -> AppConfig {
serde_json::from_value(serde_json::json!({ "name": sheep, "script": "./run.sh" }))
.expect("an app")
}
#[track_caller]
pub fn app_with_settings(sheep: &str, settings: &str) -> AppConfig {
let table: serde_json::Value = toml::from_str(settings).expect("settings are TOML");
serde_json::from_value(serde_json::json!({
"name": sheep,
"script": "./run.sh",
"dogs": { "deploy": table },
}))
.expect("an app with settings")
}
#[track_caller]
pub fn gated_app(sheep: &str, settings: &str) -> AppConfig {
let table: serde_json::Value = toml::from_str(settings).expect("settings are TOML");
serde_json::from_value(serde_json::json!({
"name": sheep,
"script": "./run.sh",
"readiness_probe": { "kind": "http", "target": "http://127.0.0.1:1/health" },
"dogs": { "deploy": table },
}))
.expect("a gated app")
}
#[derive(Debug)]
pub struct Sheep {
app: RefCell<AppConfig>,
}
impl Sheep {
#[track_caller]
pub fn of(sheep: &str, settings: &str) -> Self {
Self {
app: RefCell::new(app_with_settings(sheep, settings)),
}
}
pub fn holding(app: AppConfig) -> Self {
Self {
app: RefCell::new(app),
}
}
pub fn app(&self) -> AppConfig {
self.app.borrow().clone()
}
#[track_caller]
pub fn settings(&self) -> Settings {
settings::from_app("web", &self.app.borrow()).expect("the table parses")
}
#[track_caller]
pub fn set(&self, key: &str, value: &str) {
let mut app = self.app.borrow_mut();
let mut table = app
.dogs
.get("deploy")
.map(|held| held.as_map().clone())
.unwrap_or_default();
table.insert(key.to_owned(), serde_json::Value::String(value.to_owned()));
app.dogs.insert("deploy".to_owned(), DogTable::from(table));
}
pub fn answer(&self) -> Result<AppConfig, Error> {
Ok(self.app())
}
pub fn replace(&self, dog: &str, table: DogTable) -> Result<(), Error> {
self.app.borrow_mut().dogs.insert(dog.to_owned(), table);
Ok(())
}
}
impl Daemon for Sheep {
async fn sheep_config(&self, _sheep: &str) -> Result<AppConfig, Error> {
self.answer()
}
async fn set_sheep_dog_settings(
&self,
_sheep: &str,
dog: &str,
table: DogTable,
) -> Result<(), Error> {
self.replace(dog, table)
}
crate::fixtures::daemon_methods!(unimplemented;
dog_config, list_flock, describe, start, delete, reload, restart, save_roll, set_smit,
);
}
#[track_caller]
pub fn tempdir() -> TempDir {
tempfile::tempdir().expect("tempdir")
}
pub struct Sections {
sections: Vec<String>,
asked: Cell<usize>,
}
impl core::fmt::Debug for Sections {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Sections")
.field(
"sections",
&format_args!("<{} sections>", self.sections.len()),
)
.field("asked", &self.asked.get())
.finish()
}
}
impl Sections {
pub fn of(first: &str, rest: &[&str]) -> Self {
Self {
sections: core::iter::once(first)
.chain(rest.iter().copied())
.map(str::to_owned)
.collect(),
asked: Cell::new(0),
}
}
pub fn asked(&self) -> usize {
self.asked.get()
}
}
impl Daemon for Sections {
async fn dog_config(&self, _name: &str) -> Result<String, Error> {
let asked = self.asked.get();
self.asked.set(asked + 1);
assert!(
asked < 200,
"the section was asked for far more often than the interval allows: the loop is \
not sleeping"
);
Ok(self.sections[asked.min(self.sections.len() - 1)].clone())
}
crate::fixtures::daemon_methods!(unimplemented;
list_flock, describe, start, delete, reload, restart, save_roll, sheep_config,
set_sheep_dog_settings, set_smit,
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_does_not_print_the_sections() {
let daemon = Sections::of("retention = 9", &["interval = \"hunter2-distinctive\""]);
let shown = format!("{daemon:?}");
assert!(!shown.contains("hunter2"), "{shown}");
assert!(!shown.contains("retention"), "{shown}");
assert_eq!(shown, "Sections { sections: <2 sections>, asked: 0 }");
}
}