#[allow(dead_code)]
mod sim_daemon;
use std::time::Duration;
use microvms_core::protocol::exec::{Phase, StartRequest};
use microvms_core::session::{
ClientDeadline, CompletionOptions, ExecResult, KillAnswer, OutputSink, StreamOptions,
};
use sim_daemon::{Ending, KillMode, Script, SimDaemon, StreamMode};
#[derive(Debug, bolero::TypeGenerator)]
struct Plan {
streaming: bool,
finishes_after: Option<u8>,
ending: u8,
code: u8,
cut: bool,
kill_fails: bool,
kill_ends_after: u8,
ack_failures: u8,
poll_failures: u8,
timeout_sec: Option<u8>,
grace: u8,
truncated: bool,
}
const SPAN: u8 = 12;
fn script_of(plan: &Plan) -> Script {
let signal = [9, 11, 15][usize::from(plan.code % 3)];
let ending = match plan.ending % 4 {
0 => Ending {
exit_code: Some(i32::from(plan.code % 5)),
..Ending::default()
},
1 => Ending {
signal: Some(signal),
..Ending::default()
},
2 => Ending {
signal: Some(if plan.code.is_multiple_of(2) { 15 } else { 9 }),
timed_out: true,
..Ending::default()
},
_ => Ending {
exit_code: Some(0),
timed_out: true,
..Ending::default()
},
};
Script {
stdout: "out".into(),
finishes_after: plan
.finishes_after
.map(|after| Duration::from_secs(u64::from(after % SPAN))),
ending,
truncated: plan.truncated,
stream: if plan.cut {
StreamMode::Cut
} else {
StreamMode::Exit
},
kill: if plan.kill_fails {
KillMode::Fails
} else {
KillMode::Ends(Duration::from_secs(u64::from(plan.kill_ends_after % 4)))
},
ack_failures: u32::from(plan.ack_failures % 3),
poll_failures: u32::from(plan.poll_failures % 3),
}
}
fn model_posix(result: &ExecResult) -> Option<i32> {
if let Some(client) = &result.client_deadline
&& (client.ack_error.is_some() || client.kill == KillAnswer::Signalled)
{
return Some(124);
}
let outcome = result.outcome.as_ref()?;
if outcome.timed_out {
return Some(124);
}
match (outcome.exit_code, outcome.signal) {
(Some(code), _) => Some(code),
(None, Some(signal)) => Some(128 + signal),
(None, None) => None,
}
}
fn drive(plan: &Plan) -> (ExecResult, Vec<String>, bool) {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.start_paused(true)
.build()
.expect("a runtime");
let script = script_of(plan);
let timeout_sec = match (plan.timeout_sec, &script.finishes_after) {
(Some(timeout), _) => Some(f64::from(timeout % SPAN)),
(None, None) => Some(5.0),
(None, Some(_)) => None,
};
runtime.block_on(async {
let daemon = SimDaemon::new(script);
let session = daemon.session();
let request =
StartRequest::new("x-fuzz", vec!["bash".into(), "-c".into(), "fuzzed".into()])
.with_timeout_sec(timeout_sec);
let options = CompletionOptions {
client_grace: Duration::from_secs(u64::from(plan.grace % SPAN)),
stream: StreamOptions {
max_reconnects: 2,
..StreamOptions::default()
},
};
let sink: Option<OutputSink> = plan.streaming.then(|| {
Box::new(|_| {
Box::pin(std::future::ready(std::ops::ControlFlow::Continue(())))
as microvms_core::session::OutputFlow
}) as OutputSink
});
let result = session
.run_to_completion(request, options, sink)
.await
.expect("BIND-8: every scripted plan returns a result");
(result, daemon.log(), daemon.acked())
})
}
#[test]
fn run_to_completion_plan() {
bolero::check!().with_type::<Plan>().for_each(|plan| {
let (result, log, acked) = drive(plan);
let position = |suffix: &str| log.iter().position(|line| line.ends_with(suffix));
let polls_or_acks: Vec<usize> = log
.iter()
.enumerate()
.filter(|(_, line)| line.ends_with("/ack") || line.ends_with("/x-fuzz"))
.map(|(index, _)| index)
.collect();
assert_eq!(
result.synthesized(),
!acked,
"BIND-10: synthesized={} but the daemon recorded acked={acked}: {plan:?} {log:?}",
result.synthesized()
);
if result.synthesized() {
assert_eq!(result.posix_exit_code(), Some(124), "BIND-10: {plan:?}");
assert!(
result
.notes()
.iter()
.any(|note| note.contains("synthesized")),
"BIND-10: {:?}",
result.notes()
);
assert!(position("/kill").is_some(), "BIND-10: no kill: {log:?}");
} else {
assert_eq!(result.phase, Phase::Acked, "BIND-8: {plan:?} {result:?}");
assert!(result.outcome.is_some(), "BIND-8: {plan:?} {result:?}");
}
if plan.streaming && plan.cut && result.client_deadline.is_none() {
let stream = position("/stream").expect("a streaming plan attached");
assert!(
log[stream..].iter().any(|line| line.ends_with("/x-fuzz")),
"BIND-8: a cut stream did not fall back to wait: {plan:?} {log:?}"
);
}
match (&result.client_deadline, position("/kill")) {
(None, None) => {}
(Some(ClientDeadline { .. }), Some(kill)) => {
assert!(
polls_or_acks.last().is_some_and(|last| *last > kill),
"BIND-9: nothing after the kill: {plan:?} {log:?}"
);
assert_eq!(
log.iter().filter(|line| line.ends_with("/kill")).count(),
1,
"BIND-9: one kill per deadline: {log:?}"
);
}
(deadline, kill) => {
panic!("BIND-9: deadline {deadline:?} and kill at {kill:?}: {plan:?} {log:?}")
}
}
assert_eq!(
result.posix_exit_code(),
model_posix(&result),
"BIND-6: {plan:?} {result:?}"
);
let outcome = result.outcome.as_ref();
let expected = usize::from(outcome.is_some_and(|o| o.truncated))
+ usize::from(outcome.is_some_and(|o| o.timed_out))
+ usize::from(outcome.is_some_and(|o| o.writers_may_be_alive))
+ usize::from(result.client_deadline.is_some());
assert_eq!(
result.notes().len(),
expected,
"BIND-7: {:?} for {result:?}",
result.notes()
);
});
}