use std::cell::RefCell;
use std::io::{self, Write};
use std::sync::{Arc, Mutex, Once};
use tempfile::TempDir;
#[derive(Clone, Default)]
pub(crate) struct CapturedLogs(Arc<Mutex<Vec<u8>>>);
thread_local! {
static ACTIVE_SINK: RefCell<Option<Arc<Mutex<Vec<u8>>>>> = const { RefCell::new(None) };
}
static INSTALL_CAPTURE_SUBSCRIBER: Once = Once::new();
static INSTALL_ERROR: Mutex<Option<String>> = Mutex::new(None);
pub(crate) struct CapturedWriter;
impl Write for CapturedWriter {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
ACTIVE_SINK.with(|sink| {
if let Some(bytes) = sink.borrow().as_ref() {
let mut bytes = bytes
.lock()
.map_err(|_| io::Error::other("captured log lock poisoned"))?;
bytes.extend_from_slice(buffer);
}
Ok(buffer.len())
})
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct SinkGuard;
impl Drop for SinkGuard {
fn drop(&mut self) {
ACTIVE_SINK.with(|sink| sink.borrow_mut().take());
}
}
impl CapturedLogs {
pub(crate) fn capture<T>(body: impl FnOnce() -> T) -> (Self, T) {
INSTALL_CAPTURE_SUBSCRIBER.call_once(|| {
let subscriber = tracing_subscriber::fmt()
.without_time()
.with_ansi(false)
.with_writer(|| CapturedWriter)
.finish();
if let Err(error) = tracing::subscriber::set_global_default(subscriber) {
if let Ok(mut slot) = INSTALL_ERROR.lock() {
*slot = Some(format!("capture subscriber not installed: {error}"));
}
}
});
let captured = Self::default();
if let Some(reason) = INSTALL_ERROR
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_deref()
&& let Ok(mut bytes) = captured.0.lock()
{
bytes.extend_from_slice(reason.as_bytes());
}
ACTIVE_SINK.with(|sink| {
*sink.borrow_mut() = Some(Arc::clone(&captured.0));
});
let guard = SinkGuard;
let value = body();
drop(guard);
(captured, value)
}
pub(crate) fn text(&self) -> Result<String, Box<dyn std::error::Error>> {
let bytes = self.0.lock().map_err(|_| "captured log lock poisoned")?;
Ok(String::from_utf8(bytes.clone())?)
}
}
pub(crate) fn make_private(path: &std::path::Path) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
}
#[cfg(not(unix))]
{
let _ = path;
}
Ok(())
}
pub(crate) fn private_tempdir() -> std::io::Result<TempDir> {
let dir = tempfile::tempdir()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700))?;
}
Ok(dir)
}
const TEARDOWN_FAILED: &str = "aion-server test fixture teardown failed";
pub(crate) struct EngineUnderTest {
engine: std::sync::Arc<aion::Engine>,
stopped: std::sync::atomic::AtomicBool,
}
impl EngineUnderTest {
pub(crate) fn new(engine: std::sync::Arc<aion::Engine>) -> Self {
Self {
engine,
stopped: std::sync::atomic::AtomicBool::new(false),
}
}
pub(crate) fn handle(&self) -> std::sync::Arc<aion::Engine> {
std::sync::Arc::clone(&self.engine)
}
pub(crate) fn disarm(&self) {
self.stopped
.store(true, std::sync::atomic::Ordering::SeqCst);
}
pub(crate) fn shutdown(&self) -> Result<(), aion::EngineError> {
if self.stopped.swap(true, std::sync::atomic::Ordering::SeqCst) {
return Ok(());
}
self.engine.shutdown()
}
}
impl std::ops::Deref for EngineUnderTest {
type Target = std::sync::Arc<aion::Engine>;
fn deref(&self) -> &Self::Target {
&self.engine
}
}
impl Drop for EngineUnderTest {
fn drop(&mut self) {
report_teardown(self.shutdown().err().map(|error| error.to_string()));
}
}
pub(crate) struct StateUnderTest {
state: crate::ServerState,
engine: Option<EngineUnderTest>,
stopped: std::sync::atomic::AtomicBool,
}
impl StateUnderTest {
pub(crate) fn new(state: crate::ServerState) -> Self {
Self {
state,
engine: None,
stopped: std::sync::atomic::AtomicBool::new(false),
}
}
pub(crate) fn over(engine: EngineUnderTest, state: crate::ServerState) -> Self {
Self {
state,
engine: Some(engine),
stopped: std::sync::atomic::AtomicBool::new(false),
}
}
pub(crate) fn shutdown(&self) -> Result<(), crate::ServerError> {
if self.stopped.swap(true, std::sync::atomic::Ordering::SeqCst) {
return Ok(());
}
let outcome = self.state.shutdown();
if outcome.is_ok()
&& let Some(engine) = &self.engine
{
engine.disarm();
}
outcome
}
}
impl std::ops::Deref for StateUnderTest {
type Target = crate::ServerState;
fn deref(&self) -> &Self::Target {
&self.state
}
}
impl Drop for StateUnderTest {
fn drop(&mut self) {
report_teardown(self.shutdown().err().map(|error| error.to_string()));
}
}
fn report_teardown(failure: Option<String>) {
if std::thread::panicking() {
if let Some(message) = failure {
eprintln!("{TEARDOWN_FAILED}: {message}");
}
return;
}
assert!(
failure.is_none(),
"{TEARDOWN_FAILED}: {}",
failure.as_deref().unwrap_or_default()
);
}