use super::{Client, TestResult, config_with};
use crate::config::types::ServerConfig;
use crate::server::connection::LiminalConnectionServices;
use crate::server::self_streams::owner::PublisherRuntime;
use crate::server::self_streams::sink::SelfStreams;
use crate::server::shutdown::{ShutdownHandle, run_shutdown_sequence};
use crate::server::signals::SignalCounts;
use crate::server::{ConnectionSupervisor, ServerListener};
use liminal::durability::StoreCountsHandle;
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
pub(super) type Server = Fixture<Running>;
pub(super) struct Fixture<State> {
state: Option<State>,
shutdown: fn(State) -> TestResult,
}
impl<State> Fixture<State> {
fn new(state: State, shutdown: fn(State) -> TestResult) -> Self {
Self {
state: Some(state),
shutdown,
}
}
pub(super) fn stop(mut self) -> TestResult {
let state = self.state.take().ok_or("server fixture already stopped")?;
(self.shutdown)(state)
}
}
impl<State> Deref for Fixture<State> {
type Target = State;
fn deref(&self) -> &Self::Target {
match self.state.as_ref() {
Some(state) => state,
None => panic!("server fixture already stopped"),
}
}
}
impl<State> Drop for Fixture<State> {
fn drop(&mut self) {
let Some(state) = self.state.take() else {
return;
};
if let Err(error) = (self.shutdown)(state) {
eprintln!("console wire fixture shutdown failed: {error}");
if !std::thread::panicking() {
panic!("console wire fixture shutdown failed: {error}");
}
}
}
}
pub(super) struct Running {
pub(super) services: Arc<LiminalConnectionServices>,
pub(super) listener: ServerListener,
pub(super) publisher: PublisherRuntime,
pub(super) counts: StoreCountsHandle,
store: TempDir,
}
impl Fixture<Running> {
pub(super) fn start(extra_channels: usize) -> TestResult<Self> {
Self::start_with(extra_channels, &[])
}
pub(super) fn start_with(extra_channels: usize, named: &[&str]) -> TestResult<Self> {
Self::start_verifying(extra_channels, named, None)
}
pub(super) fn start_verifying(
extra_channels: usize,
named: &[&str],
pass: Option<crate::auth_pass::PassVerifier>,
) -> TestResult<Self> {
Self::start_configuring(extra_channels, named, pass, |_| Ok(()))
}
pub(super) fn start_configuring(
extra_channels: usize,
named: &[&str],
pass: Option<crate::auth_pass::PassVerifier>,
configure: impl FnOnce(&mut ServerConfig) -> TestResult,
) -> TestResult<Self> {
let store = TempDir::new()?;
let mut config = config_with(store.path(), extra_channels, named)?;
configure(&mut config)?;
let services = Arc::new(LiminalConnectionServices::from_config(&config)?);
services.signals().wake().hold();
let counts = services.store_counts();
let shutdown = ShutdownHandle::new();
let supervisor = ConnectionSupervisor::with_fatal_shutdown(
services.clone(),
None,
pass,
config.limits,
shutdown.clone(),
)?;
let publisher = crate::server::runtime::start_self_stream_publisher(
&services,
&supervisor,
&config,
&shutdown,
)?
.ok_or("no self-stream publisher: the console streams need one")?;
let listener = ServerListener::bind(&config, supervisor)?;
Ok(Self::new(
Running {
services,
listener,
publisher,
counts,
store,
},
Running::stop,
))
}
pub(super) fn signals(&self) -> SignalCounts {
self.services.signals().counts()
}
pub(super) fn settled(&self) -> TestResult {
let signals = self.signals();
let events = signals.connections_opened + signals.connections_closed;
if self
.services
.signals()
.await_events(events, Duration::from_secs(30))
{
let streams = self.streams()?;
streams.release_drainer()?;
Ok(streams.drain_all()?)
} else {
Err(format!("{events} connection events not queued in 30 s").into())
}
}
pub(super) fn connect(&self) -> TestResult<Client> {
Client::connect(self.listener.local_addr())
}
pub(super) fn streams(&self) -> TestResult<Arc<SelfStreams>> {
Ok(Arc::clone(
self.services
.signals()
.streams()
.ok_or("no self-stream publisher is installed")?,
))
}
}
impl Running {
fn stop(self) -> TestResult {
let Self {
services,
mut listener,
mut publisher,
counts,
store,
} = self;
let supervisor = listener.supervisor();
let stopped = run_shutdown_sequence(&mut listener, None, &supervisor, Duration::ZERO);
if stopped.is_err() {
supervisor.shutdown();
}
let finished = publisher.finish();
services.channel_cluster().supervisor().shutdown();
drop((supervisor, listener, publisher, counts, services));
let closed = store.close();
let failures: Vec<_> = [
stopped
.err()
.map(|error| format!("server shutdown: {error}")),
finished
.err()
.map(|error| format!("publisher finish: {error}")),
closed.err().map(|error| format!("store removal: {error}")),
]
.into_iter()
.flatten()
.collect();
if failures.is_empty() {
Ok(())
} else {
Err(failures.join("; ").into())
}
}
}
#[test]
fn a_panicking_test_runs_fixture_shutdown_before_releasing_state() -> TestResult {
use std::sync::Mutex;
type Events = Arc<Mutex<Vec<(&'static str, std::thread::ThreadId)>>>;
struct Probe {
events: Events,
fails: bool,
}
impl Probe {
fn shutdown(self) -> TestResult {
self.events
.lock()
.map_err(|_| "events poisoned")?
.push(("shutdown", std::thread::current().id()));
if self.fails {
Err("forced shutdown failure".into())
} else {
Ok(())
}
}
}
impl Drop for Probe {
fn drop(&mut self) {
match self.events.lock() {
Ok(mut events) => events.push(("release", std::thread::current().id())),
Err(error) => panic!("fixture release probe failed: {error}"),
}
}
}
for fails in [false, true] {
let events = Arc::new(Mutex::new(vec![]));
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let fixture = Fixture::new(
Probe {
events: Arc::clone(&events),
fails,
},
Probe::shutdown,
);
assert!(fixture.state.is_some());
panic!("forced fixture assertion failure");
}));
let panic = caught.err().ok_or("fixture assertion did not panic")?;
assert_eq!(
panic.downcast_ref::<&str>(),
Some(&"forced fixture assertion failure")
);
let events = events.lock().map_err(|_| "events poisoned")?;
let thread = std::thread::current().id();
assert_eq!(*events, vec![("shutdown", thread), ("release", thread)]);
}
let events = Arc::new(Mutex::new(vec![]));
Fixture::new(
Probe {
events: Arc::clone(&events),
fails: false,
},
Probe::shutdown,
)
.stop()?;
let events = events.lock().map_err(|_| "events poisoned")?;
let thread = std::thread::current().id();
assert_eq!(*events, vec![("shutdown", thread), ("release", thread)]);
Ok(())
}