use std::path::PathBuf;
use std::process::Command;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use futures::executor::block_on;
use microde_application::{
MicrodeApplication, MicrodeApplicationState, MicrodeContextHandle, MicrodeError,
MicrodeExecutionResult, MicrodeModule, MicrodeStopRequest, ModuleFuture, ModuleKind,
};
struct RequestingModule {
context: MicrodeContextHandle,
}
struct CleanupNotifier(Option<mpsc::Sender<()>>);
impl MicrodeModule for CleanupNotifier {
const KIND: ModuleKind = ModuleKind::Passive;
fn cleanup(&mut self) -> ModuleFuture {
let sender = self.0.take().unwrap();
Box::pin(async move {
sender.send(()).unwrap();
Ok(())
})
}
fn run(&mut self) -> ModuleFuture {
Box::pin(async { Ok(()) })
}
fn stop(&mut self) -> ModuleFuture {
Box::pin(async { Ok(()) })
}
}
struct UnexpectedPanic(mpsc::Sender<()>);
impl MicrodeModule for UnexpectedPanic {
const KIND: ModuleKind = ModuleKind::Passive;
fn run(&mut self) -> ModuleFuture {
self.0.send(()).unwrap();
panic!("unexpected module panic")
}
fn stop(&mut self) -> ModuleFuture {
Box::pin(async { Ok(()) })
}
}
struct PanickingModule {
context: MicrodeContextHandle,
error: Option<MicrodeError>,
shutdown_marker: PathBuf,
}
impl MicrodeModule for PanickingModule {
const KIND: ModuleKind = ModuleKind::Passive;
fn shutdown(&mut self) -> ModuleFuture {
let marker = self.shutdown_marker.clone();
Box::pin(async move {
std::fs::write(marker, "shutdown ran").unwrap();
Ok(())
})
}
fn run(&mut self) -> ModuleFuture {
self.context.panic(self.error.clone())
}
fn stop(&mut self) -> ModuleFuture {
Box::pin(async { Ok(()) })
}
}
impl MicrodeModule for RequestingModule {
const KIND: ModuleKind = ModuleKind::Passive;
fn run(&mut self) -> ModuleFuture {
self.context
.request_stop(MicrodeStopRequest::with_exit_code(6));
Box::pin(async { Ok(()) })
}
fn stop(&mut self) -> ModuleFuture {
Box::pin(async { Ok(()) })
}
}
#[test]
fn public_construction_supports_module_requested_shutdown() {
let mut service = MicrodeApplication::default();
service
.install(|context| RequestingModule { context })
.unwrap();
let result = block_on(service.serve()).unwrap();
assert_eq!(
result,
MicrodeExecutionResult {
exit_code: 6,
error: None,
errors: None,
}
);
assert_eq!(service.state(), MicrodeApplicationState::Failed);
}
#[test]
fn dropping_the_completion_future_does_not_cancel_the_lifecycle() {
let (sender, receiver) = mpsc::channel();
let mut service = MicrodeApplication::new();
service.install(|_| CleanupNotifier(Some(sender))).unwrap();
drop(service.serve());
receiver.recv_timeout(Duration::from_secs(1)).unwrap();
let deadline = Instant::now() + Duration::from_secs(1);
while service.state() != MicrodeApplicationState::Finished && Instant::now() < deadline {
std::thread::yield_now();
}
assert_eq!(service.state(), MicrodeApplicationState::Finished);
}
#[test]
fn an_unexpected_lifecycle_panic_completes_all_waiters_with_an_error() {
let (sender, receiver) = mpsc::channel();
let mut service = MicrodeApplication::new();
service.install(|_| UnexpectedPanic(sender)).unwrap();
let run = service.serve();
receiver.recv_timeout(Duration::from_secs(1)).unwrap();
let stop = service.stop(MicrodeStopRequest::success());
let (run, stop) = block_on(futures::future::join(run, stop));
assert_eq!(
run.unwrap_err(),
MicrodeError::new("unexpected module panic")
);
assert_eq!(
stop.unwrap_err(),
MicrodeError::new("unexpected module panic")
);
assert_eq!(service.state(), MicrodeApplicationState::Failed);
}
#[test]
fn production_panic_terminates_the_process() {
const CHILD_MARKER: &str = "MICRODE_PANIC_CHILD";
const SHUTDOWN_MARKER: &str = "MICRODE_SHUTDOWN_MARKER";
if let Some(mode) = std::env::var_os(CHILD_MARKER) {
let shutdown_marker = PathBuf::from(std::env::var_os(SHUTDOWN_MARKER).unwrap());
let mut service = MicrodeApplication::new();
service
.install(|context| PanickingModule {
context,
error: (mode == "with-error").then(|| MicrodeError::new("fatal child error")),
shutdown_marker,
})
.unwrap();
let _ = block_on(service.serve());
unreachable!("panic must terminate before the lifecycle returns");
}
for mode in ["without-error", "with-error"] {
let shutdown_marker = std::env::temp_dir().join(format!(
"microde-panic-{}-{mode}.marker",
std::process::id()
));
let output = Command::new(std::env::current_exe().unwrap())
.arg("--exact")
.arg("production_panic_terminates_the_process")
.arg("--nocapture")
.env(CHILD_MARKER, mode)
.env(SHUTDOWN_MARKER, &shutdown_marker)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
assert!(!shutdown_marker.exists());
}
}
#[test]
fn runtime_context_rejects_stop_requests_before_execution() {
let mut service = MicrodeApplication::new();
let mut captured_context = None;
service
.install(|context| {
captured_context = Some(context.clone());
RequestingModule { context }
})
.unwrap();
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
captured_context
.unwrap()
.request_stop(MicrodeStopRequest::success());
}));
assert!(panic.is_err());
assert_eq!(service.state(), MicrodeApplicationState::Idle);
}