use std::time::Duration;
use crate::live::{Declared, Running, Stage};
pub const POLL_EVERY: Duration = Duration::from_secs(10);
pub const CONFIRM_BUDGET: Duration = Duration::from_secs(15 * 60);
pub const ROLLED_BACK_AFTER: Duration = Duration::from_secs(60);
pub const STAGED_SELF_REL: &str = "update/staged-self";
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LiveApp {
pub base: String,
pub app: String,
pub app_len: u64,
pub sig: String,
pub url: String,
pub sha256: String,
pub release: String,
pub release_sig: String,
pub env: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Take {
pub open: bool,
pub release: String,
pub live: bool,
pub role: String,
pub app: Option<LiveApp>,
pub front: Option<LiveApp>,
pub words: String,
}
pub trait Shop {
fn take(&self) -> Result<Take, String>;
fn report(&self, role: &str, outcome: &str, words: &str) -> Result<String, String>;
}
pub trait Hands {
fn running(&self, role: &str) -> Result<Running, String>;
fn stage(&self, role: &str, stage: &Stage, declared: &Declared) -> Result<String, String>;
fn fetch(&self, url: &str) -> Result<Vec<u8>, String>;
fn keep(&self, role: &str) -> Result<String, String>;
fn wait(&self, d: Duration);
fn staged_self(&self, release: &str) -> bool;
fn mark_staged_self(&self, release: &str) -> Result<(), String>;
fn front_keep(&self) -> String {
"the front from its keep medium".into()
}
}
pub const DONE_KEEP: &str = "done:keep";
pub const DONE_LIVE: &str = "done:live";
pub const DONE_NOTHING: &str = "done:nothing";
pub const NEEDS_KEEP: &str = "needs-keep";
pub const FAILED: &str = "failed";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Plan {
Nothing,
Live,
Keep(String),
Wait(String),
}
fn on_trial(slot: &str) -> bool {
slot.split_whitespace().nth(1).is_some()
}
pub fn decide(r: &Running, app: Option<&LiveApp>, live: bool) -> Plan {
let Some(app) = app else {
return Plan::Keep(if live { "the release carries no live app for it".into() } else { "live updates are OFF for this estate".into() });
};
if on_trial(&r.slot) {
return Plan::Wait(format!("[live-trial-open] slot {}: its last update is not confirmed yet", r.slot));
}
if r.app.trim() == app.app.trim() {
return Plan::Nothing;
}
if !live {
return Plan::Keep("live updates are OFF for this estate".into());
}
if !r.live_update {
return Plan::Keep("live update is OFF on the box (korp.live_update=off, its BASE)".into());
}
if r.base.trim() != app.base.trim() {
return Plan::Keep(format!("its BASE moved ({} → {}): a base update", short(&r.base), short(&app.base)));
}
Plan::Live
}
fn short(h: &str) -> &str {
&h[..h.len().min(12)]
}
fn unhex(s: &str) -> Result<Vec<u8>, String> {
let s = s.trim();
if !s.len().is_multiple_of(2) {
return Err("odd hex".into());
}
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string())).collect()
}
pub fn staged_of(h: &dyn Hands, app: &LiveApp) -> Result<Stage, String> {
if app.sig.trim().is_empty() {
return Err("[update-unsigned] the release's live app carries no signature: never staged".into());
}
let archive = h.fetch(&app.url)?;
let sha: String = ring::digest::digest(&ring::digest::SHA256, &archive).as_ref().iter().map(|b| format!("{b:02x}")).collect();
if !app.sha256.trim().is_empty() && sha != app.sha256.trim() {
return Err(format!("[update-fetch] the archive's sha256 is {}, the release says {}", short(&sha), short(&app.sha256)));
}
if archive.len() as u64 != app.app_len {
return Err(format!("[update-fetch] the archive is {} bytes, the release says {}", archive.len(), app.app_len));
}
let pin = blake3::hash(&archive).to_hex().to_string();
if pin != app.app.trim() {
return Err(format!("[update-fetch] the archive's pin is {}, the release says {}", short(&pin), short(&app.app)));
}
Ok(Stage { pin_hex: app.app.trim().to_string(), signature: unhex(&app.sig).map_err(|e| format!("[update-unsigned] the signature: {e}"))?, base: app.base.trim().to_string(), archive })
}
pub fn declared_of(app: &LiveApp) -> Result<Declared, String> {
let release = match app.release.trim() {
"" => None,
b => Some((unhex(b).map_err(|e| format!("[update-release] the release declaration: {e}"))?, unhex(&app.release_sig).map_err(|e| format!("[update-release] its signature: {e}"))?)),
};
Ok(Declared { release, env: (!app.env.is_empty()).then(|| app.env.clone()) })
}
pub fn wait_confirmed(h: &dyn Hands, role: &str, app: &str, old: &str) -> Result<String, String> {
let mut waited = Duration::ZERO;
let mut last = String::new();
while waited < CONFIRM_BUDGET {
h.wait(POLL_EVERY);
waited += POLL_EVERY;
match h.running(role) {
Ok(r) if r.app.trim() == app.trim() && !on_trial(&r.slot) => return Ok(format!("the {role} runs app {} confirmed (slot {})", short(app), r.slot)),
Ok(r) if r.app.trim() == old.trim() && !on_trial(&r.slot) && waited >= ROLLED_BACK_AFTER => {
return Err(format!("[update-rolled-back] the {role} runs its old app {} again (slot {}): the new one never confirmed", short(old), r.slot))
}
Ok(r) => last = format!("slot {}, app {}", r.slot, short(&r.app)),
Err(e) => last = e,
}
}
Err(format!("[update-not-confirmed] the {role} did not confirm within {} min ({last})", CONFIRM_BUDGET.as_secs() / 60))
}
pub type Report = (String, String);
pub fn step(h: &dyn Hands, t: &Take) -> Option<Report> {
if !t.open || t.role.is_empty() {
return None;
}
let role = t.role.as_str();
if !matches!(role, "twin" | "main" | "front") {
return Some((FAILED.into(), format!("this front updates twin, main and front, not {role:?}")));
}
let _busy = crate::frontside::busy::on(role);
let done = |how: &str, words: String| Some((format!("done:{how}"), words));
let failed = |words: String| Some((FAILED.to_string(), words));
let front = role == "front";
let app = if front { t.front.as_ref() } else { t.app.as_ref() };
let running = match h.running(role) {
Ok(r) => r,
Err(e) if !front && e.contains("[live-unsupported]") => {
return match h.keep(role) {
Ok(w) => done("keep", format!("its live door: {e}; {w}")),
Err(k) => failed(format!("its live door: {e}; the keep upgrade: {k}")),
}
}
Err(e) if front => return Some((NEEDS_KEEP.into(), format!("the front's own slot: {e}"))),
Err(e) => return failed(format!("its live door did not answer: {e}")),
};
match decide(&running, app, t.live) {
Plan::Wait(_) => None,
Plan::Nothing => {
let how = if front && h.staged_self(&t.release) { "live" } else { "nothing" };
done(how, format!("the {role} runs app {}", short(&running.app)))
}
Plan::Keep(why) if front => Some((NEEDS_KEEP.into(), format!("{why}: the shop keep-boots {}", h.front_keep()))),
Plan::Keep(why) => match h.keep(role) {
Ok(w) => done("keep", format!("{why}: {w}")),
Err(e) => failed(format!("{why}; the keep upgrade of the {role}: {e}")),
},
Plan::Live => {
let app = app.expect("Live is decided on an app");
let (stage, declared) = match staged_of(h, app).and_then(|s| declared_of(app).map(|d| (s, d))) {
Ok(s) => s,
Err(e) => return failed(e),
};
if front {
if let Err(e) = h.mark_staged_self(&t.release) {
return failed(format!("the marker before its own stage: {e}"));
}
return match h.stage(role, &stage, &declared) {
Ok(_) => None,
Err(e) if crate::live::falls_to_keep(&e) => {
h.staged_self(&t.release);
Some((NEEDS_KEEP.into(), format!("its own slot refused the live stage ({}): the shop keep-boots {}", e.trim(), h.front_keep())))
}
Err(e) => failed(format!("its own stage was refused: {e}")),
};
}
match h.stage(role, &stage, &declared) {
Ok(_) => {}
Err(e) if crate::live::falls_to_keep(&e) => {
let why = format!("the {role} refused the live stage ({})", e.trim());
return match h.keep(role) {
Ok(w) => done("keep", format!("{why}: {w}")),
Err(k) => failed(format!("{why}; the keep upgrade of the {role}: {k}")),
};
}
Err(e) => return failed(format!("the stage was refused: {e}")),
}
match wait_confirmed(h, role, &app.app, &running.app) {
Ok(w) => done("live", w),
Err(e) => failed(e),
}
}
}
}
pub fn look(shop: &dyn Shop, h: &dyn Hands) -> Result<Option<(String, String, String)>, String> {
let t = shop.take()?;
let Some((outcome, words)) = step(h, &t) else { return Ok(None) };
let said = shop.report(&t.role, &outcome, &words)?;
Ok(Some((t.role, outcome, said)))
}
pub fn reachable(url: &str) -> String {
match url.strip_prefix("http://127.0.0.1:") {
Some(rest) => format!("http://10.0.2.100:{rest}"),
None => url.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::collections::BTreeMap;
const OLD: &str = "0101010101010101010101010101010101010101010101010101010101010101";
fn archive() -> Vec<u8> {
(0..4096u32).map(|i| (i % 251) as u8).collect()
}
fn app_of(base: &str) -> LiveApp {
let a = archive();
LiveApp {
base: base.into(),
app: blake3::hash(&a).to_hex().to_string(),
app_len: a.len() as u64,
sig: "ab".repeat(80),
url: "https://objsto.example/release/holger/03.live-app.znippy?X-Amz=…".into(),
sha256: ring::digest::digest(&ring::digest::SHA256, &a).as_ref().iter().map(|b| format!("{b:02x}")).collect(),
..LiveApp::default()
}
}
fn running(base: &str, app: &str, slot: &str) -> Running {
Running { base: base.into(), app: app.into(), slot: slot.into(), live_update: true }
}
#[derive(Default)]
struct Fake {
boxes: RefCell<BTreeMap<String, Running>>,
comes_back: RefCell<Option<String>>,
refuse_stage: Option<&'static str>,
no_door: bool,
staged: RefCell<Vec<String>>,
declared: RefCell<Vec<Declared>>,
kept: RefCell<Vec<String>>,
marker: RefCell<Option<String>>,
waited: RefCell<Duration>,
down_polls: RefCell<u32>,
}
impl Hands for Fake {
fn running(&self, role: &str) -> Result<Running, String> {
if self.no_door {
return Err("REFUSED [live-unsupported] this VM takes no live update".into());
}
if *self.down_polls.borrow() > 0 {
*self.down_polls.borrow_mut() -= 1;
return Err("connection refused (re-exec)".into());
}
self.boxes.borrow().get(role).cloned().ok_or_else(|| "no such box".into())
}
fn stage(&self, role: &str, stage: &Stage, declared: &Declared) -> Result<String, String> {
self.declared.borrow_mut().push(declared.clone());
if let Some(w) = self.refuse_stage {
return Err(w.into());
}
self.staged.borrow_mut().push(role.into());
if role != "front" {
*self.down_polls.borrow_mut() = 2;
}
let mut b = self.boxes.borrow_mut();
let r = b.get_mut(role).unwrap();
if let Some(app) = self.comes_back.borrow().clone() {
assert_eq!(app, stage.pin_hex);
r.app = app;
}
r.slot = "a".into();
Ok("staged".into())
}
fn fetch(&self, _: &str) -> Result<Vec<u8>, String> {
Ok(archive())
}
fn keep(&self, role: &str) -> Result<String, String> {
self.kept.borrow_mut().push(role.into());
Ok(format!("the {role} was booted from the release's keep medium"))
}
fn wait(&self, d: Duration) {
*self.waited.borrow_mut() += d;
}
fn staged_self(&self, release: &str) -> bool {
self.marker.borrow_mut().take().is_some_and(|r| r == release)
}
fn mark_staged_self(&self, release: &str) -> Result<(), String> {
*self.marker.borrow_mut() = Some(release.into());
Ok(())
}
}
fn take(role: &str, live: bool) -> Take {
let base = "b".repeat(64);
Take { open: true, release: "03".into(), live, role: role.into(), app: live.then(|| app_of(&base)), front: live.then(|| app_of(&base)), words: String::new() }
}
#[test]
fn same_base_is_staged_live_and_waited_for_until_confirmed() {
let f = Fake::default();
let app = app_of(&"b".repeat(64));
f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
*f.comes_back.borrow_mut() = Some(app.app.clone());
let (o, w) = step(&f, &take("twin", true)).expect("reported");
assert_eq!(o, DONE_LIVE, "{w}");
assert!(w.contains("confirmed"), "{w}");
assert_eq!(*f.staged.borrow(), vec!["twin".to_string()]);
assert!(f.kept.borrow().is_empty());
}
#[test]
fn a_box_that_rolls_back_or_refuses_the_stage_is_reported_failed() {
let f = Fake::default();
f.boxes.borrow_mut().insert("main".into(), running(&"b".repeat(64), OLD, "a"));
let (o, w) = step(&f, &take("main", true)).unwrap();
assert_eq!(o, FAILED);
assert!(w.contains("[update-rolled-back]"), "{w}");
assert!(*f.waited.borrow() >= ROLLED_BACK_AFTER);
let r = Fake { refuse_stage: Some("[live-verify] the signature leg"), ..Fake::default() };
r.boxes.borrow_mut().insert("main".into(), running(&"b".repeat(64), OLD, "a"));
let (o, w) = step(&r, &take("main", true)).unwrap();
assert_eq!(o, FAILED);
assert!(w.contains("[live-verify]") && r.kept.borrow().is_empty(), "{w}");
}
#[test]
fn a_moved_base_live_off_the_boxs_switch_or_no_live_door_go_through_the_keep_medium() {
for (t, r, no_door) in [
(take("twin", true), running(&"c".repeat(64), OLD, "a"), false),
(take("twin", false), running(&"b".repeat(64), OLD, "a"), false),
(take("twin", true), Running { live_update: false, ..running(&"b".repeat(64), OLD, "a") }, false),
(take("twin", true), running(&"b".repeat(64), OLD, "a"), true),
] {
let f = Fake { no_door, ..Fake::default() };
f.boxes.borrow_mut().insert("twin".into(), r);
let (o, w) = step(&f, &t).unwrap();
assert_eq!(o, DONE_KEEP, "{w}");
assert!(f.staged.borrow().is_empty());
assert_eq!(*f.kept.borrow(), vec!["twin".to_string()]);
}
}
#[test]
fn the_front_stages_itself_and_reports_after_its_reexec_or_asks_for_its_keep_boot() {
let f = Fake::default();
let app = app_of(&"b".repeat(64));
f.boxes.borrow_mut().insert("front".into(), running(&"b".repeat(64), OLD, "a"));
*f.comes_back.borrow_mut() = Some(app.app.clone());
assert_eq!(step(&f, &take("front", true)), None, "staged: it re-execs, nothing reported now");
assert_eq!(f.marker.borrow().as_deref(), Some("03"));
*f.down_polls.borrow_mut() = 0;
let (o, _) = step(&f, &take("front", true)).unwrap();
assert_eq!(o, DONE_LIVE, "the marker says it was this front's own live update");
let moved = Fake::default();
moved.boxes.borrow_mut().insert("front".into(), running(&"c".repeat(64), OLD, "a"));
assert_eq!(step(&moved, &take("front", true)).unwrap().0, NEEDS_KEEP);
assert!(moved.kept.borrow().is_empty(), "the front never upgrades itself through Upgrade");
let trial = Fake::default();
trial.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "b booted"));
assert_eq!(step(&trial, &take("twin", true)), None);
}
#[test]
fn the_release_declaration_and_env_ride_the_stage() {
let f = Fake::default();
let mut app = app_of(&"b".repeat(64));
app.release = "6b6f7270".into();
app.release_sig = "77".repeat(64);
app.env = vec!["korp.env.HOLGER_DATA=/data".into()];
f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
*f.comes_back.borrow_mut() = Some(app.app.clone());
let t = Take { app: Some(app), ..take("twin", true) };
assert_eq!(step(&f, &t).unwrap().0, DONE_LIVE);
assert_eq!(f.declared.borrow()[0], Declared { release: Some((b"korp".to_vec(), vec![0x77; 64])), env: Some(vec!["korp.env.HOLGER_DATA=/data".into()]) });
let plain = Fake::default();
plain.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
step(&plain, &take("twin", true));
assert_eq!(plain.declared.borrow()[0], Declared::default(), "nothing declared");
}
#[test]
fn a_keep_refusal_of_the_stage_is_answered_with_a_keep_boot() {
for w in ["REFUSED [live-env-moved] the release changes korp.env.*", "REFUSED [live-release-absent] none rides", "REFUSED [live-declared-unsupported] x"] {
let f = Fake { refuse_stage: Some(w), ..Fake::default() };
f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
let (o, said) = step(&f, &take("twin", true)).unwrap();
assert_eq!(o, DONE_KEEP, "{said}");
assert_eq!(*f.kept.borrow(), vec!["twin".to_string()]);
let front = Fake { refuse_stage: Some(w), ..Fake::default() };
front.boxes.borrow_mut().insert("front".into(), running(&"b".repeat(64), OLD, "a"));
let (o, said) = step(&front, &take("front", true)).unwrap();
assert_eq!(o, NEEDS_KEEP, "{said}");
assert!(front.marker.borrow().is_none(), "the front's marker is forgotten: its keep boot is no live update");
}
let f = Fake { refuse_stage: Some("REFUSED [release-sig] does not hold"), ..Fake::default() };
f.boxes.borrow_mut().insert("twin".into(), running(&"b".repeat(64), OLD, "a"));
assert_eq!(step(&f, &take("twin", true)).unwrap().0, FAILED);
assert!(f.kept.borrow().is_empty());
}
#[test]
fn an_archive_that_is_not_the_releases_is_never_staged() {
let f = Fake::default();
let mut bad = app_of(&"b".repeat(64));
bad.app = "9".repeat(64);
assert!(staged_of(&f, &bad).unwrap_err().contains("pin"));
let mut unsigned = app_of(&"b".repeat(64));
unsigned.sig.clear();
assert!(staged_of(&f, &unsigned).unwrap_err().contains("[update-unsigned]"));
let mut sha = app_of(&"b".repeat(64));
sha.sha256 = "0".repeat(64);
assert!(staged_of(&f, &sha).unwrap_err().contains("sha256"));
assert!(step(&Fake::default(), &Take { open: false, ..take("twin", true) }).is_none());
}
struct FakeShop {
turn: RefCell<Vec<&'static str>>,
reported: RefCell<Vec<(String, String)>>,
refuse_take: Option<&'static str>,
live: bool,
}
impl FakeShop {
fn open(live: bool) -> FakeShop {
FakeShop { turn: RefCell::new(vec!["twin", "main", "front"]), reported: RefCell::default(), refuse_take: None, live }
}
}
impl Shop for FakeShop {
fn take(&self) -> Result<Take, String> {
if let Some(w) = self.refuse_take {
return Err(format!("refused: {w}"));
}
Ok(match self.turn.borrow().first() {
Some(r) => take(r, self.live),
None => Take::default(),
})
}
fn report(&self, role: &str, outcome: &str, _: &str) -> Result<String, String> {
let mut turn = self.turn.borrow_mut();
if turn.first() != Some(&role) {
return Err(format!("refused: [update-out-of-turn] {role} reported, the turn is {:?}", turn.first()));
}
turn.remove(0);
self.reported.borrow_mut().push((role.into(), outcome.into()));
Ok(format!("{role}: {outcome} recorded"))
}
}
#[test]
fn live_off_twin_then_main_are_kept_and_the_front_needs_keep_in_the_shops_order() {
let shop = FakeShop::open(false);
let h = Fake::default();
for r in ["twin", "main", "front"] {
h.boxes.borrow_mut().insert(r.into(), running(&"b".repeat(64), OLD, "a"));
}
for _ in 0..4 {
look(&shop, &h).expect("every look is in turn");
}
assert_eq!(shop.reported.borrow().iter().map(|(r, o)| format!("{r} {o}")).collect::<Vec<_>>(), ["twin done:keep", "main done:keep", "front needs-keep"]);
assert_eq!(h.kept.borrow().as_slice(), ["twin", "main"], "the front never upgrades itself");
assert_eq!(look(&shop, &h).unwrap(), None, "nothing open: nothing reported");
}
#[test]
fn live_on_twin_main_and_front_take_the_release_live() {
let shop = FakeShop::open(true);
let h = Fake::default();
for r in ["twin", "main", "front"] {
h.boxes.borrow_mut().insert(r.into(), running(&"b".repeat(64), OLD, "a"));
}
*h.comes_back.borrow_mut() = Some(app_of(&"b".repeat(64)).app);
for _ in 0..4 {
look(&shop, &h).expect("every look is in turn");
}
assert_eq!(shop.reported.borrow().iter().map(|(r, o)| format!("{r} {o}")).collect::<Vec<_>>(), ["twin done:live", "main done:live", "front done:live"]);
assert!(h.kept.borrow().is_empty());
}
#[test]
fn a_take_refused_or_an_unknown_role_is_said_by_name() {
let shop = FakeShop { refuse_take: Some("[update-none-open] no update is open for acme"), ..FakeShop::open(false) };
assert!(look(&shop, &Fake::default()).unwrap_err().contains("update-none-open"));
assert_eq!(step(&Fake::default(), &Take { open: true, role: "nas".into(), ..Default::default() }).map(|r| r.0).as_deref(), Some(FAILED));
}
#[test]
fn a_bench_loopback_release_object_is_reached_at_the_mocks_guestfwd() {
assert_eq!(reachable("http://127.0.0.1:17463/objsto/x?X=1"), "http://10.0.2.100:17463/objsto/x?X=1");
assert_eq!(reachable("https://objsto.example/holger-release/x?X=1"), "https://objsto.example/holger-release/x?X=1");
}
#[test]
fn a_box_is_busy_while_its_step_runs_and_only_then() {
struct Peek(Fake);
impl Hands for Peek {
fn running(&self, role: &str) -> Result<Running, String> {
assert!(crate::frontside::busy::busy(Some(role)) && crate::frontside::busy::busy(None), "the step's own box is busy");
self.0.running(role)
}
fn stage(&self, role: &str, s: &Stage, d: &Declared) -> Result<String, String> {
self.0.stage(role, s, d)
}
fn fetch(&self, u: &str) -> Result<Vec<u8>, String> {
self.0.fetch(u)
}
fn keep(&self, role: &str) -> Result<String, String> {
assert!(crate::frontside::busy::busy(Some(role)), "busy through its keep boot");
self.0.keep(role)
}
fn wait(&self, d: Duration) {
self.0.wait(d)
}
fn staged_self(&self, r: &str) -> bool {
self.0.staged_self(r)
}
fn mark_staged_self(&self, r: &str) -> Result<(), String> {
self.0.mark_staged_self(r)
}
}
let p = Peek(Fake { no_door: true, ..Fake::default() });
assert_eq!(step(&p, &take("twin", true)).unwrap().0, DONE_KEEP);
}
#[test]
fn the_fronts_keep_boot_is_said_in_the_products_words() {
struct Named(Fake);
impl Hands for Named {
fn running(&self, role: &str) -> Result<Running, String> {
self.0.running(role)
}
fn stage(&self, role: &str, s: &Stage, d: &Declared) -> Result<String, String> {
self.0.stage(role, s, d)
}
fn fetch(&self, u: &str) -> Result<Vec<u8>, String> {
self.0.fetch(u)
}
fn keep(&self, role: &str) -> Result<String, String> {
self.0.keep(role)
}
fn wait(&self, d: Duration) {
self.0.wait(d)
}
fn staged_self(&self, r: &str) -> bool {
self.0.staged_self(r)
}
fn mark_staged_self(&self, r: &str) -> Result<(), String> {
self.0.mark_staged_self(r)
}
fn front_keep(&self) -> String {
"Acme's front from acme-front-keep".into()
}
}
let named = Named(Fake::default());
named.0.boxes.borrow_mut().insert("front".into(), running(&"c".repeat(64), OLD, "a"));
let (o, w) = step(&named, &take("front", true)).unwrap();
assert_eq!(o, NEEDS_KEEP);
assert!(w.ends_with("the shop keep-boots Acme's front from acme-front-keep"), "{w}");
let refused = Named(Fake { refuse_stage: Some("REFUSED [live-env-moved] x"), ..Fake::default() });
refused.0.boxes.borrow_mut().insert("front".into(), running(&"b".repeat(64), OLD, "a"));
let (o, w) = step(&refused, &take("front", true)).unwrap();
assert_eq!(o, NEEDS_KEEP);
assert!(w.contains("[live-env-moved]") && w.ends_with("the shop keep-boots Acme's front from acme-front-keep"), "{w}");
let plain = Fake::default();
plain.boxes.borrow_mut().insert("front".into(), running(&"c".repeat(64), OLD, "a"));
assert!(step(&plain, &take("front", true)).unwrap().1.ends_with("the shop keep-boots the front from its keep medium"));
}
}