1use crate::determinism::{replay_consistent, DeterminismMode};
7use crate::effect::{EffectHandler, RecordingEffectHandler};
8use crate::engine::{ProtocolMachine, ProtocolMachineError};
9use serde::{Deserialize, Serialize};
10use std::io::Cursor;
11
12fn encode_snapshot(machine: &ProtocolMachine) -> Result<Vec<u8>, ProtocolMachineError> {
13 let mut bytes = Vec::new();
14 ciborium::into_writer(machine, &mut bytes).map_err(|e| {
15 ProtocolMachineError::PersistenceError(format!("integration snapshot encode failed: {e}"))
16 })?;
17 Ok(bytes)
18}
19
20fn decode_snapshot(bytes: &[u8]) -> Result<ProtocolMachine, ProtocolMachineError> {
21 ciborium::from_reader(Cursor::new(bytes)).map_err(|e| {
22 ProtocolMachineError::PersistenceError(format!("integration snapshot decode failed: {e}"))
23 })
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub struct LoadedProtocolMachineReplayConformance {
29 pub determinism_mode: DeterminismMode,
31 pub replay_consistent: bool,
33 pub config_mode_consistent: bool,
35 pub exact_trace_match: bool,
37 pub exact_effect_trace_match: bool,
39 pub recorded_effect_count: usize,
41 pub baseline_event_count: usize,
43 pub replay_event_count: usize,
45}
46
47pub fn run_loaded_protocol_machine_record_replay_conformance(
57 machine: &mut ProtocolMachine,
58 handler: &dyn EffectHandler,
59 max_steps: usize,
60) -> Result<LoadedProtocolMachineReplayConformance, ProtocolMachineError> {
61 let snapshot = encode_snapshot(machine)?;
62
63 let recording = RecordingEffectHandler::new(handler);
64 machine.run(&recording, max_steps)?;
65
66 let recorded_effects = recording.effect_trace();
67 let baseline_trace = machine.trace().to_vec();
68 let baseline_effect_trace = machine.effect_trace().to_vec();
69 let determinism_mode = machine.config().determinism_mode;
70
71 let mut replay_vm = decode_snapshot(&snapshot)?;
72 replay_vm.run_replay(handler, &recorded_effects, max_steps)?;
73
74 let replay_trace = replay_vm.trace().to_vec();
75 let replay_effect_trace = replay_vm.effect_trace().to_vec();
76 let replay_mode_consistent = replay_consistent(
77 DeterminismMode::Replay,
78 &baseline_trace,
79 &replay_trace,
80 &baseline_effect_trace,
81 &replay_effect_trace,
82 );
83 let config_mode_consistent = replay_consistent(
84 determinism_mode,
85 &baseline_trace,
86 &replay_trace,
87 &baseline_effect_trace,
88 &replay_effect_trace,
89 );
90
91 Ok(LoadedProtocolMachineReplayConformance {
92 determinism_mode,
93 replay_consistent: replay_mode_consistent,
94 config_mode_consistent,
95 exact_trace_match: baseline_trace == replay_trace,
96 exact_effect_trace_match: baseline_effect_trace == replay_effect_trace,
97 recorded_effect_count: recorded_effects.len(),
98 baseline_event_count: baseline_trace.len(),
99 replay_event_count: replay_trace.len(),
100 })
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::coroutine::Value;
107 use crate::durable::WalSyncRequest;
108 use crate::effect::{
109 EffectFailure, EffectResult, SendDecision, SendDecisionInput, TopologyPerturbation,
110 };
111 use crate::engine::ProtocolMachineConfig;
112 use crate::loader::CodeImage;
113 use crate::output_condition::OutputConditionHint;
114 use std::collections::BTreeMap;
115 use telltale_types::{GlobalType, Label, LocalTypeR};
116
117 struct DeterministicHandler;
118
119 impl EffectHandler for DeterministicHandler {
120 fn handle_send(
121 &self,
122 _role: &str,
123 _partner: &str,
124 _label: &str,
125 _state: &[Value],
126 ) -> EffectResult<Value> {
127 EffectResult::success(Value::Nat(1))
128 }
129
130 fn send_decision(&self, input: SendDecisionInput<'_>) -> EffectResult<SendDecision> {
131 EffectResult::success(SendDecision::Deliver(input.payload.unwrap_or(Value::Unit)))
132 }
133
134 fn handle_recv(
135 &self,
136 _role: &str,
137 _partner: &str,
138 _label: &str,
139 _state: &mut Vec<Value>,
140 _payload: &Value,
141 ) -> EffectResult<()> {
142 EffectResult::success(())
143 }
144
145 fn handle_choose(
146 &self,
147 _role: &str,
148 _partner: &str,
149 labels: &[String],
150 _state: &[Value],
151 ) -> EffectResult<String> {
152 match labels.first().cloned() {
153 Some(label) => EffectResult::success(label),
154 None => EffectResult::failure(EffectFailure::invalid_input("no labels available")),
155 }
156 }
157
158 fn step(&self, _role: &str, _state: &mut Vec<Value>) -> EffectResult<()> {
159 EffectResult::success(())
160 }
161 }
162
163 struct DeterministicInternalEffectHandler;
164
165 impl EffectHandler for DeterministicInternalEffectHandler {
166 fn handle_send(
167 &self,
168 _role: &str,
169 _partner: &str,
170 _label: &str,
171 _state: &[Value],
172 ) -> EffectResult<Value> {
173 EffectResult::success(Value::Nat(1))
174 }
175
176 fn send_decision(&self, input: SendDecisionInput<'_>) -> EffectResult<SendDecision> {
177 EffectResult::success(SendDecision::Deliver(input.payload.unwrap_or(Value::Unit)))
178 }
179
180 fn handle_recv(
181 &self,
182 _role: &str,
183 _partner: &str,
184 _label: &str,
185 _state: &mut Vec<Value>,
186 _payload: &Value,
187 ) -> EffectResult<()> {
188 EffectResult::success(())
189 }
190
191 fn handle_choose(
192 &self,
193 _role: &str,
194 _partner: &str,
195 labels: &[String],
196 _state: &[Value],
197 ) -> EffectResult<String> {
198 match labels.first().cloned() {
199 Some(label) => EffectResult::success(label),
200 None => EffectResult::failure(EffectFailure::invalid_input("no labels available")),
201 }
202 }
203
204 fn step(&self, _role: &str, _state: &mut Vec<Value>) -> EffectResult<()> {
205 EffectResult::success(())
206 }
207
208 fn topology_events(&self, tick: u64) -> EffectResult<Vec<TopologyPerturbation>> {
209 let events = match tick {
210 1 => vec![TopologyPerturbation::Partition {
211 from: "A".to_string(),
212 to: "B".to_string(),
213 }],
214 2 => vec![TopologyPerturbation::Heal {
215 from: "A".to_string(),
216 to: "B".to_string(),
217 }],
218 _ => Vec::new(),
219 };
220 EffectResult::success(events)
221 }
222
223 fn output_condition_hint(
224 &self,
225 sid: usize,
226 role: &str,
227 _state: &[Value],
228 ) -> Option<OutputConditionHint> {
229 Some(OutputConditionHint {
230 predicate_ref: "machine.integration.internal_effects".to_string(),
231 witness_ref: Some(format!("sid:{sid}:role:{role}")),
232 })
233 }
234
235 fn supports_wal_sync(&self) -> bool {
236 true
237 }
238
239 fn wal_sync(&self, _sync: &WalSyncRequest) -> EffectResult<()> {
240 EffectResult::success(())
241 }
242 }
243
244 fn simple_send_recv_image() -> CodeImage {
245 let mut local_types = BTreeMap::new();
246 local_types.insert(
247 "A".to_string(),
248 LocalTypeR::Send {
249 partner: "B".into(),
250 branches: vec![(Label::new("msg"), None, LocalTypeR::End)],
251 },
252 );
253 local_types.insert(
254 "B".to_string(),
255 LocalTypeR::Recv {
256 partner: "A".into(),
257 branches: vec![(Label::new("msg"), None, LocalTypeR::End)],
258 },
259 );
260
261 let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
262 CodeImage::from_local_types(&local_types, &global)
263 }
264
265 #[test]
266 fn loaded_protocol_machine_harness_reports_replay_conformance() {
267 let image = simple_send_recv_image();
268 let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
269 machine
270 .load_choreography(&image)
271 .expect("load choreography");
272
273 let report = run_loaded_protocol_machine_record_replay_conformance(
274 &mut machine,
275 &DeterministicHandler,
276 100,
277 )
278 .expect("harness run should succeed");
279
280 assert!(report.replay_consistent);
281 assert!(report.config_mode_consistent);
282 assert!(report.exact_trace_match);
283 assert!(report.exact_effect_trace_match);
284 assert!(report.recorded_effect_count > 0);
285 assert!(report.baseline_event_count > 0);
286 }
287
288 #[test]
289 fn loaded_protocol_machine_harness_preserves_internal_effect_replay_exactness() {
290 let image = simple_send_recv_image();
291 let mut machine = ProtocolMachine::new(ProtocolMachineConfig::default());
292 machine
293 .load_choreography(&image)
294 .expect("load choreography");
295
296 let report = run_loaded_protocol_machine_record_replay_conformance(
297 &mut machine,
298 &DeterministicInternalEffectHandler,
299 100,
300 )
301 .expect("harness run should succeed");
302
303 assert!(report.replay_consistent);
304 assert!(report.config_mode_consistent);
305 assert!(report.exact_trace_match);
306 assert!(report.exact_effect_trace_match);
307 assert!(report.recorded_effect_count > 0);
308 }
309}