use std::sync::Arc;
use std::time::Instant;
use bash_interop::rig::{Answer, Driving, ExitStatus, Failure, Layout, Message, Reacting, Rig, Shell, Verb};
use crate::{ENTRY, behind, gone, provisioned, report, script};
use bash_interop::scratch::{Scripts, bash};
struct Breaking {
on: Verb,
}
struct Breaks {
on: Verb,
heard: Vec<Message>,
}
impl Rig for Breaking {
type Reaction = Breaks;
fn bash(&self, _at: &Layout) -> String {
String::new()
}
async fn joined(&self, _at: &Layout, _shell: Arc<Shell>) -> Result<Breaks, Failure> {
Ok(Breaks {
on: self.on,
heard: Vec::new(),
})
}
}
impl Driving for Breaking {}
impl Reacting for Breaks {
type Kept = Vec<Message>;
async fn hear(&mut self, said: Message) -> Result<(), Failure> {
if self.on == Verb::Say {
return Err(Failure::new(
"keeping what was said",
"the sink is on fire",
));
}
self.heard.push(said);
Ok(())
}
async fn answer(&mut self, _: Message) -> Result<Answer, Failure> {
match self.on {
Verb::Ask => Err(Failure::new(
"deciding an answer",
"the operator is on fire",
)),
Verb::Say => Ok(Answer::status(0)),
}
}
async fn finish(self) -> Result<Vec<Message>, Failure> {
Ok(self.heard)
}
}
const REPORTING: &str = r#"
echo $BASHPID > "${BASH_SOURCE[0]%/*}/pid"
"#;
fn blocked(scripts: &Scripts) -> i32 {
std::fs::read_to_string(scripts.at("pid"))
.expect("the subject reported its pid")
.trim()
.parse()
.expect("a pid")
}
#[tokio::test]
async fn a_rig_that_cannot_answer_ends_the_run_and_kills_the_subject() {
let scripts = Scripts::of(&[(
ENTRY,
&format!("{REPORTING}BC_INSTR KEEP ask anything"),
)]);
let breaking = Breaking { on: Verb::Ask };
let failure = breaking
.run(
&bash(scripts.at(ENTRY)),
provisioned(&breaking),
)
.await
.err()
.expect("the run must end in the rig's failure");
assert!(
failure.to_string().contains("the operator is on fire"),
"{failure}"
);
assert!(
gone(blocked(&scripts)),
"the shell was left waiting for an answer never coming"
);
}
#[tokio::test]
async fn a_failure_while_hearing_ends_the_run_and_kills_the_subject() {
let scripts = Scripts::of(&[(
ENTRY,
&format!(
r#"{REPORTING}
bash -c 'while :; do BC_INSTR KEEP ask nothing || :; sleep 0.01; done' &
sleep 0.1
BC_INSTR KEEP say REC one
sleep 30
"#
),
)]);
let started = Instant::now();
let breaking = Breaking { on: Verb::Say };
let failure = breaking
.run(
&bash(scripts.at(ENTRY)),
provisioned(&breaking),
)
.await
.err()
.expect("the run must end in the rig's failure");
assert!(
failure.to_string().contains("the sink is on fire"),
"{failure}"
);
assert!(
started.elapsed().as_secs() < 5,
"the run must not wait the subject out"
);
assert!(
gone(blocked(&scripts)),
"the subject outlived the run"
);
}
#[tokio::test]
async fn an_unknown_verb_is_reported_rather_than_ignored() {
let ran = script(
r#"
complaint="$(BC_INSTR KEEP mumble something 2>&1)"
BC_INSTR KEEP say REC "returned $?" "$complaint"
"#,
)
.await;
assert_eq!(
ran.subject,
ExitStatus::Code(0),
"the subject carries on"
);
let said = behind(&ran.shells, "REC");
assert_eq!(said.len(), 1, "{}", report(&ran.shells));
assert_eq!(
said[0][0],
"returned 125",
"the instrumentation failed{}",
report(&ran.shells)
);
assert!(
said[0][1].contains("unknown verb mumble"),
"it says which: {:?}",
said[0][1]
);
}
impl crate::Joins for Breaking {
fn joining(&self, at: &Layout) -> String {
crate::join(at)
}
}