#![allow(clippy::all)]
use crate::cx::Cx;
use crate::lab::{LabConfig, LabRuntime};
use crate::runtime::RuntimeBuilder;
pub use crate::test_logging::{
ARTIFACT_SCHEMA_VERSION, AllocatedPort, DockerFixtureService, EnvironmentMetadata,
FixtureService, InProcessService, NoOpFixtureService, PortAllocator, ReproManifest,
TempDirFixture, TestContext, TestEnvironment, derive_component_seed, derive_entropy_seed,
derive_scenario_seed, wait_until_healthy,
};
pub use crate::test_ndjson::{
NDJSON_SCHEMA_VERSION, NdjsonEvent, NdjsonLogger, artifact_base_dir, artifact_bundle_dir,
ndjson_file_name, trace_file_name, write_artifact_bundle,
};
use crate::time::timeout;
use parking_lot::Mutex;
use std::future::Future;
use std::sync::{Arc, Once};
use std::time::Duration;
use tracing::Dispatch;
use tracing_subscriber::fmt::format::FmtSpan;
static GLOBAL_INIT_LOGGING: Once = Once::new();
#[allow(dead_code)] static ENV_LOCK: Mutex<()> = Mutex::new(());
pub const DEFAULT_TEST_SEED: u64 = 0xDEAD_BEEF;
#[derive(Debug, Clone)]
pub struct RuntimeSubscriberHandle {
_dispatch: Arc<Dispatch>,
#[allow(dead_code)]
runtime_id: String,
}
impl RuntimeSubscriberHandle {
pub fn new_isolated(runtime_id: String, level: tracing::Level) -> Self {
let subscriber = tracing_subscriber::fmt()
.with_max_level(level)
.with_test_writer()
.with_file(true)
.with_line_number(true)
.with_target(true)
.with_thread_ids(true)
.with_span_events(FmtSpan::CLOSE)
.with_ansi(false)
.finish();
let dispatch = Arc::new(Dispatch::new(subscriber));
Self {
_dispatch: dispatch,
runtime_id,
}
}
pub fn with_subscriber<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R,
{
tracing::dispatcher::with_default(&*self._dispatch, f)
}
}
pub fn init_test_logging() {
init_test_logging_with_level(tracing::Level::TRACE);
}
pub fn init_test_logging_with_level(level: tracing::Level) {
GLOBAL_INIT_LOGGING.call_once(|| {
let _ = tracing_subscriber::fmt()
.with_max_level(level)
.with_test_writer()
.with_file(true)
.with_line_number(true)
.with_target(true)
.with_thread_ids(true)
.with_span_events(FmtSpan::CLOSE)
.with_ansi(false)
.try_init();
});
}
pub fn init_runtime_logging(runtime_id: String) -> RuntimeSubscriberHandle {
init_runtime_logging_with_level(runtime_id, tracing::Level::TRACE)
}
pub fn init_runtime_logging_with_level(
runtime_id: String,
level: tracing::Level,
) -> RuntimeSubscriberHandle {
RuntimeSubscriberHandle::new_isolated(runtime_id, level)
}
#[allow(dead_code)] pub(crate) fn env_lock() -> parking_lot::MutexGuard<'static, ()> {
ENV_LOCK.lock()
}
#[must_use]
pub fn test_lab() -> LabRuntime {
LabRuntime::new(LabConfig::new(DEFAULT_TEST_SEED))
}
#[must_use]
pub fn test_lab_with_seed(seed: u64) -> LabRuntime {
LabRuntime::new(LabConfig::new(seed))
}
#[must_use]
pub fn test_lab_with_tracing() -> LabRuntime {
LabRuntime::new(LabConfig::new(DEFAULT_TEST_SEED).trace_capacity(64 * 1024))
}
#[must_use]
pub fn test_lab_from_context(ctx: &TestContext) -> LabRuntime {
LabRuntime::new(LabConfig::new(ctx.seed))
}
pub fn lab_with_config<F, R>(f: F) -> R
where
F: FnOnce(&mut LabRuntime) -> R,
{
init_test_logging();
let mut lab = test_lab();
f(&mut lab)
}
#[must_use]
pub fn test_context(test_id: &str) -> TestContext {
TestContext::new(test_id, DEFAULT_TEST_SEED)
}
#[must_use]
pub fn test_context_with_seed(test_id: &str, seed: u64) -> TestContext {
TestContext::new(test_id, seed)
}
pub fn run_test<F, Fut>(f: F)
where
F: FnOnce() -> Fut,
Fut: Future<Output = ()>,
{
init_test_logging();
let runtime = RuntimeBuilder::current_thread()
.build()
.expect("failed to build test runtime");
runtime.block_on(f());
}
pub fn run_test_with_cx<F, Fut>(f: F)
where
F: FnOnce(Cx) -> Fut,
Fut: Future<Output = ()>,
{
init_test_logging();
let cx: Cx = Cx::for_testing();
let runtime = RuntimeBuilder::current_thread()
.build()
.expect("failed to build test runtime");
runtime.block_on(f(cx));
}
pub async fn assert_completes_within<F, Fut, T>(
timeout_duration: Duration,
description: &str,
f: F,
) -> T
where
F: FnOnce() -> Fut,
Fut: Future<Output = T> + Unpin,
{
let now = Cx::current()
.and_then(|cx| cx.timer_driver())
.map_or_else(crate::time::wall_now, |driver| driver.now());
let Ok(value) = timeout(now, timeout_duration, f()).await else {
unreachable!("operation '{description}' did not complete within {timeout_duration:?}");
};
tracing::debug!(
description = %description,
timeout_ms = timeout_duration.as_millis(),
"operation completed within timeout"
);
value
}
#[cfg(test)]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::*;
use crate::conformance::{ConformanceTarget, LabRuntimeTarget};
use futures_lite::future;
#[test]
fn assert_completes_within_uses_wall_time_when_no_runtime_is_active() {
let _t0 = crate::time::wall_now();
std::thread::sleep(Duration::from_millis(50));
let value = future::block_on(assert_completes_within(
Duration::from_millis(10),
"standalone immediate future",
|| std::future::ready(7_u8),
));
assert_eq!(value, 7);
}
#[test]
fn lab_with_config_exposes_a_usable_lab_runtime() {
let (seed, value) = lab_with_config(|runtime| {
let seed = runtime.config().seed;
let value = LabRuntimeTarget::block_on(runtime, async { 42_u8 });
(seed, value)
});
assert_eq!(seed, DEFAULT_TEST_SEED);
assert_eq!(value, 42);
}
}
#[macro_export]
macro_rules! test_phase {
($name:expr) => {
tracing::info!(phase = %$name, "========================================");
tracing::info!(phase = %$name, "TEST PHASE: {}", $name);
tracing::info!(phase = %$name, "========================================");
};
}
#[macro_export]
macro_rules! test_section {
($name:expr) => {
tracing::debug!(section = %$name, "--- {} ---", $name);
};
}
#[macro_export]
macro_rules! test_complete {
($name:expr) => {
tracing::info!(test = %$name, "test completed successfully: {}", $name);
};
($name:expr, $($key:ident = $value:expr),* $(,)?) => {
tracing::info!(
test = %$name,
$($key = %$value,)*
"test completed successfully: {}",
$name
);
};
}
#[macro_export]
macro_rules! assert_with_log {
($cond:expr, $msg:expr, $expected:expr, $actual:expr) => {{
tracing::debug!(
expected = ?$expected,
actual = ?$actual,
"Asserting: {}",
$msg
);
assert!($cond, "{}: expected {:?}, got {:?}", $msg, $expected, $actual);
}};
}
#[macro_export]
macro_rules! assert_outcome_ok {
($outcome:expr, $expected:expr) => {
match $outcome {
$crate::types::Outcome::Ok(v) => assert_eq!(v, $expected),
other => unreachable!("expected Outcome::Ok({:?}), got {:?}", $expected, other),
}
};
}
#[macro_export]
macro_rules! assert_outcome_cancelled {
($outcome:expr) => {
match $outcome {
$crate::types::Outcome::Cancelled(_) => {}
other => unreachable!("expected Outcome::Cancelled, got {:?}", other),
}
};
}
#[macro_export]
macro_rules! assert_outcome_err {
($outcome:expr) => {
match $outcome {
$crate::types::Outcome::Err(_) => {}
other => unreachable!("expected Outcome::Err, got {:?}", other),
}
};
}
#[macro_export]
macro_rules! assert_outcome_panicked {
($outcome:expr) => {
match $outcome {
$crate::types::Outcome::Panicked(_) => {}
other => unreachable!("expected Outcome::Panicked, got {:?}", other),
}
};
}
#[derive(Debug)]
pub struct TestConnection {
id: usize,
query_count: std::sync::atomic::AtomicUsize,
}
impl TestConnection {
#[must_use]
pub fn new(id: usize) -> Self {
Self {
id,
query_count: std::sync::atomic::AtomicUsize::new(0),
}
}
#[must_use]
pub const fn id(&self) -> usize {
self.id
}
#[must_use]
pub fn query_count(&self) -> usize {
self.query_count.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn query(&self, _sql: &str) -> Result<(), TestError> {
self.query_count
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TestError(pub String);
impl std::error::Error for TestError {}
impl std::fmt::Display for TestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TestError: {}", self.0)
}
}
use crate::test_logging::{TestEvent, TestLogLevel};
use std::path::PathBuf;
pub struct EvidenceSink {
logger: NdjsonLogger,
test_name: String,
current_phase: String,
}
impl EvidenceSink {
pub fn for_test(test_name: &str) -> Self {
let ctx = TestContext::new(test_name, DEFAULT_TEST_SEED);
let logger = NdjsonLogger::enabled(TestLogLevel::Info, Some(ctx));
Self {
logger,
test_name: test_name.to_string(),
current_phase: "init".to_string(),
}
}
pub fn with_context(test_name: &str, ctx: TestContext) -> Self {
let logger = NdjsonLogger::enabled(TestLogLevel::Info, Some(ctx));
Self {
logger,
test_name: test_name.to_string(),
current_phase: "init".to_string(),
}
}
pub fn phase(&mut self, phase: &str) {
self.current_phase = phase.to_string();
self.logger.log(TestEvent::Custom {
category: "test",
message: format!(
"phase_transition: phase={} test_name={}",
phase, self.test_name
),
});
}
pub fn event(&self, event: &str, data: &[(&str, &str)]) {
let data_str = data
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.chain(std::iter::once(format!("phase={}", self.current_phase)))
.chain(std::iter::once(format!("test_name={}", self.test_name)))
.collect::<Vec<_>>()
.join(" ");
self.logger.log(TestEvent::Custom {
category: "evidence",
message: format!("{}: {}", event, data_str),
});
}
pub fn outcome(&self, outcome: &str) {
self.logger.log(TestEvent::Custom {
category: "test",
message: format!(
"outcome: outcome={} test_name={} final_phase={}",
outcome, self.test_name, self.current_phase
),
});
}
pub fn cx_id(&self, cx_id: &str) {
self.logger.log(TestEvent::Custom {
category: "runtime",
message: format!(
"cx_active: cx_id={} phase={} test_name={}",
cx_id, self.current_phase, self.test_name
),
});
}
pub fn save(&self) -> std::io::Result<PathBuf> {
let evidence_dir = std::path::Path::new("tests/_evidence");
std::fs::create_dir_all(evidence_dir)?;
let file_path = evidence_dir.join(format!("{}.jsonl", self.test_name));
self.logger.write_ndjson_file(&file_path)?;
Ok(file_path)
}
pub fn logger(&self) -> &NdjsonLogger {
&self.logger
}
}
#[macro_export]
macro_rules! evidence_phase {
($sink:expr, $phase:expr) => {
$sink.phase($phase);
tracing::info!(phase = %$phase, "TEST PHASE: {}", $phase);
};
}
pub fn lab_with_evidence<F, T>(test_name: &str, f: F) -> (T, EvidenceSink)
where
F: FnOnce(&LabRuntime, &mut EvidenceSink) -> T,
{
let mut evidence = EvidenceSink::for_test(test_name);
evidence.phase("lab_setup");
let result = lab_with_config(|runtime| {
evidence.event(
"lab_start",
&[
("seed", &runtime.config().seed.to_string()),
("deterministic", "true"),
],
);
let result = f(runtime, &mut evidence);
evidence.phase("lab_complete");
result
});
evidence.outcome("passed");
(result, evidence)
}