use crate::effects::ChoreographyError;
use crate::runtime::ChoreographicAdapter;
use crate::testing::RecordingObserver;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct TestConfig {
pub timeout: Duration,
pub trace_messages: bool,
pub trace_phases: bool,
pub max_messages: usize,
pub fail_fast: bool,
}
impl Default for TestConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
trace_messages: true,
trace_phases: true,
max_messages: 10000,
fail_fast: false,
}
}
}
impl TestConfig {
pub fn new() -> Self {
Self::default()
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_message_tracing(mut self) -> Self {
self.trace_messages = true;
self
}
pub fn without_message_tracing(mut self) -> Self {
self.trace_messages = false;
self
}
pub fn with_phase_tracing(mut self) -> Self {
self.trace_phases = true;
self
}
pub fn without_phase_tracing(mut self) -> Self {
self.trace_phases = false;
self
}
pub fn max_messages(mut self, max: usize) -> Self {
self.max_messages = max;
self
}
pub fn with_fail_fast(mut self) -> Self {
self.fail_fast = true;
self
}
pub fn without_fail_fast(mut self) -> Self {
self.fail_fast = false;
self
}
}
#[derive(Debug, Clone)]
pub struct RoleBinding {
pub role_name: String,
pub index: Option<u32>,
}
impl RoleBinding {
pub fn new(role: impl Into<String>) -> Self {
Self {
role_name: role.into(),
index: None,
}
}
pub fn indexed(role: impl Into<String>, index: u32) -> Self {
Self {
role_name: role.into(),
index: Some(index),
}
}
#[must_use]
pub fn display_name(&self) -> String {
match self.index {
Some(i) => format!("{}[{}]", self.role_name, i),
None => self.role_name.clone(),
}
}
}
#[derive(Debug)]
pub struct TestResult {
pub success: bool,
pub errors: Vec<ChoreographyError>,
pub outputs: BTreeMap<String, Vec<u8>>,
pub messages: Vec<MessageRecord>,
pub phases: Vec<PhaseRecord>,
pub duration: Duration,
}
impl TestResult {
pub fn success() -> Self {
Self {
success: true,
errors: Vec::new(),
outputs: BTreeMap::new(),
messages: Vec::new(),
phases: Vec::new(),
duration: Duration::ZERO,
}
}
pub fn failure(errors: Vec<ChoreographyError>) -> Self {
Self {
success: false,
errors,
outputs: BTreeMap::new(),
messages: Vec::new(),
phases: Vec::new(),
duration: Duration::ZERO,
}
}
#[must_use]
pub fn completed_successfully(&self) -> bool {
self.success && self.errors.is_empty()
}
#[must_use]
pub fn phase_count(&self) -> usize {
self.phases.iter().filter(|p| p.completed).count()
}
#[must_use]
pub fn message_count(&self) -> usize {
self.messages.len()
}
#[must_use]
pub fn role_output(&self, role: &str) -> Option<&[u8]> {
self.outputs.get(role).map(|v| v.as_slice())
}
pub fn deserialize_output<T: serde::de::DeserializeOwned>(
&self,
role: &str,
) -> Option<Result<T, bincode::Error>> {
self.outputs
.get(role)
.map(|bytes| bincode::deserialize(bytes))
}
#[must_use]
pub fn message_trace(&self) -> &[MessageRecord] {
&self.messages
}
#[must_use]
pub fn phase_trace(&self) -> &[PhaseRecord] {
&self.phases
}
pub fn messages_between(&self, from: &str, to: &str) -> Vec<&MessageRecord> {
self.messages
.iter()
.filter(|m| m.from == from && m.to == to)
.collect()
}
#[must_use]
pub fn first_error(&self) -> Option<&ChoreographyError> {
self.errors.first()
}
}
#[derive(Debug, Clone)]
pub struct MessageRecord {
pub from: String,
pub to: String,
pub message_type: String,
pub size: usize,
pub timestamp: std::time::Instant,
}
#[derive(Debug, Clone)]
pub struct PhaseRecord {
pub protocol: String,
pub role: String,
pub phase: String,
pub completed: bool,
pub duration: Duration,
}
#[derive(Debug)]
pub struct ProtocolTestBuilder {
protocol_name: String,
role_bindings: BTreeMap<String, Vec<RoleBinding>>,
expected_phases: Vec<String>,
config: TestConfig,
params: Option<Vec<u8>>,
}
impl ProtocolTestBuilder {
pub fn new(protocol_name: impl Into<String>) -> Self {
Self {
protocol_name: protocol_name.into(),
role_bindings: BTreeMap::new(),
expected_phases: Vec::new(),
config: TestConfig::default(),
params: None,
}
}
pub fn bind_role(mut self, role: impl Into<String>) -> Self {
let role_name = role.into();
self.role_bindings
.entry(role_name.clone())
.or_default()
.push(RoleBinding::new(role_name));
self
}
pub fn bind_roles(mut self, role: impl Into<String>, count: usize) -> Self {
let role_name = role.into();
let bindings: Vec<_> = (0..count)
.map(|i| RoleBinding::indexed(role_name.clone(), i as u32))
.collect();
self.role_bindings
.entry(role_name)
.or_default()
.extend(bindings);
self
}
pub fn with_params_bytes(mut self, params: Vec<u8>) -> Self {
self.params = Some(params);
self
}
pub fn with_params<T: serde::Serialize>(mut self, params: &T) -> Result<Self, bincode::Error> {
self.params = Some(bincode::serialize(params)?);
Ok(self)
}
pub fn expect_phases(mut self, phases: &[&str]) -> Self {
self.expected_phases = phases.iter().map(|s| (*s).to_string()).collect();
self
}
pub fn with_config(mut self, config: TestConfig) -> Self {
self.config = config;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout;
self
}
pub fn build(self) -> ProtocolTest {
ProtocolTest {
protocol_name: self.protocol_name,
role_bindings: self.role_bindings,
expected_phases: self.expected_phases,
config: self.config,
params: self.params,
observer: Arc::new(Mutex::new(RecordingObserver::new())),
}
}
}
#[derive(Debug)]
pub struct ProtocolTest {
protocol_name: String,
role_bindings: BTreeMap<String, Vec<RoleBinding>>,
expected_phases: Vec<String>,
config: TestConfig,
params: Option<Vec<u8>>,
observer: Arc<Mutex<RecordingObserver>>,
}
impl ProtocolTest {
fn validate_bindings(&self) -> Result<(), ChoreographyError> {
if self.role_bindings.is_empty() {
return Err(ChoreographyError::ExecutionError(
"No role bindings provided".to_string(),
));
}
Ok(())
}
fn handle_observer_event(
&self,
event: &crate::testing::observer::ProtocolEvent,
result: &mut TestResult,
phase_start_times: &mut BTreeMap<(String, String, String), std::time::Instant>,
) {
match event {
crate::testing::observer::ProtocolEvent::Send {
from,
to,
msg_type,
size,
} => {
if self.config.trace_messages {
result.messages.push(MessageRecord {
from: from.clone(),
to: to.clone(),
message_type: msg_type.clone(),
size: *size,
timestamp: std::time::Instant::now(),
});
}
}
crate::testing::observer::ProtocolEvent::PhaseStart {
protocol,
role,
phase,
} => {
let key = (protocol.clone(), role.clone(), phase.clone());
phase_start_times.insert(key, std::time::Instant::now());
}
crate::testing::observer::ProtocolEvent::PhaseEnd {
protocol,
role,
phase,
} => {
if self.config.trace_phases {
let key = (protocol.clone(), role.clone(), phase.clone());
let duration = phase_start_times
.remove(&key)
.map(|start| start.elapsed())
.unwrap_or(Duration::ZERO);
result.phases.push(PhaseRecord {
protocol: protocol.clone(),
role: role.clone(),
phase: phase.clone(),
completed: true,
duration,
});
}
}
_ => {}
}
}
fn collect_observer_events(&self, result: &mut TestResult) {
let mut phase_start_times: BTreeMap<(String, String, String), std::time::Instant> =
BTreeMap::new();
let observer = self.observer.lock().unwrap();
for event in observer.events() {
self.handle_observer_event(event, result, &mut phase_start_times);
}
}
fn verify_expected_phases(&self, result: &mut TestResult) {
if self.expected_phases.is_empty() {
return;
}
let completed_phases: Vec<_> = result
.phases
.iter()
.filter(|phase| phase.completed)
.map(|phase| phase.phase.as_str())
.collect();
for expected in &self.expected_phases {
if !completed_phases.contains(&expected.as_str()) {
result
.errors
.push(ChoreographyError::ExecutionError(format!(
"Expected phase '{}' was not completed",
expected
)));
}
}
}
pub fn builder(protocol_name: impl Into<String>) -> ProtocolTestBuilder {
ProtocolTestBuilder::new(protocol_name)
}
#[must_use]
pub fn protocol_name(&self) -> &str {
&self.protocol_name
}
#[must_use]
pub fn role_bindings(&self) -> &BTreeMap<String, Vec<RoleBinding>> {
&self.role_bindings
}
#[must_use]
pub fn expected_phases(&self) -> &[String] {
&self.expected_phases
}
#[must_use]
pub fn config(&self) -> &TestConfig {
&self.config
}
#[must_use]
pub fn params_bytes(&self) -> Option<&[u8]> {
self.params.as_deref()
}
pub fn params<T: serde::de::DeserializeOwned>(&self) -> Option<Result<T, bincode::Error>> {
self.params
.as_ref()
.map(|bytes| bincode::deserialize(bytes))
}
pub async fn run(self) -> Result<TestResult, ChoreographyError> {
let start = std::time::Instant::now();
self.validate_bindings()?;
let mut result = TestResult::success();
result.duration = start.elapsed();
self.collect_observer_events(&mut result);
self.verify_expected_phases(&mut result);
result.success = result.errors.is_empty();
Ok(result)
}
}
pub trait TestableAdapter: ChoreographicAdapter {
fn reset(&mut self);
fn name(&self) -> &str;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = TestConfig::new()
.timeout(Duration::from_secs(60))
.without_message_tracing()
.max_messages(100)
.with_fail_fast();
assert_eq!(config.timeout, Duration::from_secs(60));
assert!(!config.trace_messages);
assert_eq!(config.max_messages, 100);
assert!(config.fail_fast);
}
#[test]
fn test_role_binding() {
let static_binding = RoleBinding::new("Coordinator");
assert!(static_binding.index.is_none());
let indexed_binding = RoleBinding::indexed("Witness", 2);
assert_eq!(indexed_binding.index, Some(2));
}
#[test]
fn test_protocol_test_builder() {
let test = ProtocolTest::builder("TestProtocol")
.bind_role("Coordinator")
.bind_roles("Witness", 3)
.expect_phases(&["init", "commit"])
.timeout(Duration::from_secs(10))
.build();
assert_eq!(test.protocol_name(), "TestProtocol");
assert!(test.role_bindings().contains_key("Coordinator"));
assert_eq!(test.role_bindings()["Witness"].len(), 3);
assert_eq!(test.expected_phases().len(), 2);
}
#[test]
fn test_result_accessors() {
let mut result = TestResult::success();
result.messages.push(MessageRecord {
from: "A".to_string(),
to: "B".to_string(),
message_type: "Request".to_string(),
size: 100,
timestamp: std::time::Instant::now(),
});
result.phases.push(PhaseRecord {
protocol: "Test".to_string(),
role: "A".to_string(),
phase: "init".to_string(),
completed: true,
duration: Duration::from_millis(50),
});
assert!(result.completed_successfully());
assert_eq!(result.message_count(), 1);
assert_eq!(result.phase_count(), 1);
assert_eq!(result.messages_between("A", "B").len(), 1);
}
}