use std::cell::RefCell;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
struct Invoking {
name: String,
file: Option<PathBuf>,
}
thread_local! {
static INVOKING: RefCell<Option<Invoking>> = const { RefCell::new(None) };
}
pub struct StampOverride {
previous: Option<Invoking>,
}
impl StampOverride {
#[must_use]
pub fn invoking(name: &str, file: Option<&Path>) -> Self {
let invoking = Invoking {
name: name.to_string(),
file: file.map(Path::to_path_buf),
};
let previous = INVOKING.with(|c| c.borrow_mut().replace(invoking));
Self { previous }
}
}
impl Drop for StampOverride {
fn drop(&mut self) {
let previous = self.previous.take();
INVOKING.with(|c| *c.borrow_mut() = previous);
}
}
#[must_use]
pub fn stamping_name(config_name: &str) -> String {
INVOKING
.with(|c| c.borrow().as_ref().map(|i| i.name.clone()))
.unwrap_or_else(|| config_name.to_string())
}
#[must_use]
pub fn stamping_file(config_file: Option<&Path>) -> Option<PathBuf> {
match INVOKING.with(|c| c.borrow().clone()) {
Some(invoking) => invoking.file,
None => config_file.map(Path::to_path_buf),
}
}
#[cfg(test)]
mod tests {
use super::super::{apply_stamp, stamped_config_file};
use super::*;
use crate::core::state::new_global_lock;
fn config_file(dir: &Path, name: &str) -> PathBuf {
let path = dir.join(format!("{name}.yaml"));
std::fs::write(&path, format!("version: '1.0'\nname: {name}\n")).unwrap();
path
}
fn results() -> Vec<(String, usize, usize, usize)> {
vec![("mini".to_string(), 1_usize, 1_usize, 0_usize)]
}
#[test]
fn without_the_guard_the_config_names_itself() {
let dir = tempfile::tempdir().unwrap();
let file = config_file(dir.path(), "alpha");
assert_eq!(stamping_name("alpha"), "alpha");
assert_eq!(stamping_file(Some(&file)), Some(file.clone()));
assert_eq!(stamping_file(None), None);
let mut lock = new_global_lock("alpha");
apply_stamp(&mut lock, dir.path(), ("alpha", Some(&file)), &results());
assert_eq!(lock.stacks.keys().collect::<Vec<_>>(), ["alpha"]);
assert_eq!(lock.name, "alpha");
}
#[test]
fn the_guard_stamps_the_invoking_stack_and_keeps_its_file() {
let dir = tempfile::tempdir().unwrap();
let real = config_file(dir.path(), "alpha");
let staged = config_file(dir.path(), ".forjar-undo-gen0-1");
let state = dir.path().join("state");
let mut lock = new_global_lock("alpha-old");
apply_stamp(&mut lock, &state, ("alpha-old", Some(&real)), &results());
let _guard = StampOverride::invoking("alpha", Some(&real));
assert_eq!(stamping_name("alpha-old"), "alpha");
assert_eq!(stamping_file(Some(&staged)), Some(real.clone()));
apply_stamp(&mut lock, &state, ("alpha-old", Some(&staged)), &results());
assert_eq!(
lock.stacks.keys().collect::<Vec<_>>(),
["alpha"],
"the replay must retire the historical name rather than stand beside \
it — two stamps is what `multi_stack_restore_refusal` counts"
);
assert_eq!(
lock.name, "alpha",
"and the top-level name is the invoking one"
);
assert_eq!(
lock.stamp_for("alpha").unwrap().file,
stamped_config_file(&state, Some(&real)),
"the recorded `-f` is the operator's, never the staged temp config"
);
assert_eq!(lock.stamp_for("alpha").unwrap().machines, ["mini"]);
}
#[test]
fn the_guard_is_released_on_drop_and_nests() {
let dir = tempfile::tempdir().unwrap();
let outer = config_file(dir.path(), "outer");
let inner = config_file(dir.path(), "inner");
{
let _o = StampOverride::invoking("outer", Some(&outer));
{
let _i = StampOverride::invoking("inner", Some(&inner));
assert_eq!(stamping_name("x"), "inner");
assert_eq!(stamping_file(None), Some(inner.clone()));
}
assert_eq!(stamping_name("x"), "outer");
assert_eq!(stamping_file(None), Some(outer.clone()));
}
assert_eq!(stamping_name("x"), "x");
assert_eq!(stamping_file(None), None);
}
}