1use std::fs;
4use std::path::Path;
5
6use aura_core::AuraFault;
7use aura_core::AuraVmDeterminismProfileV1;
8use serde::{Deserialize, Serialize};
9use telltale_machine::{
10 canonical_effect_trace, EffectTraceCaptureMode, EffectTraceEntry, ProtocolMachineConfig,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum AuraEffectTraceEncoding {
16 #[default]
18 Json,
19 Cbor,
21}
22
23impl AuraEffectTraceEncoding {
24 #[must_use]
26 pub fn parse(raw: &str) -> Option<Self> {
27 match raw.trim().to_ascii_lowercase().as_str() {
28 "json" => Some(Self::Json),
29 "cbor" => Some(Self::Cbor),
30 _ => None,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum AuraEffectTraceGranularity {
38 #[default]
40 Full,
41 TopologyOnly,
43 Disabled,
45}
46
47impl AuraEffectTraceGranularity {
48 #[must_use]
50 pub fn to_vm_mode(self) -> EffectTraceCaptureMode {
51 match self {
52 Self::Full => EffectTraceCaptureMode::Full,
53 Self::TopologyOnly => EffectTraceCaptureMode::TopologyOnly,
54 Self::Disabled => EffectTraceCaptureMode::Disabled,
55 }
56 }
57
58 #[must_use]
60 pub fn from_vm_mode(mode: EffectTraceCaptureMode) -> Self {
61 match mode {
62 EffectTraceCaptureMode::Full => Self::Full,
63 EffectTraceCaptureMode::TopologyOnly => Self::TopologyOnly,
64 EffectTraceCaptureMode::Disabled => Self::Disabled,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct EffectTraceCaptureOptions {
72 pub encoding: AuraEffectTraceEncoding,
74 pub granularity: AuraEffectTraceGranularity,
76 pub canonicalize: bool,
78}
79
80impl Default for EffectTraceCaptureOptions {
81 fn default() -> Self {
82 Self {
83 encoding: AuraEffectTraceEncoding::Json,
84 granularity: AuraEffectTraceGranularity::Full,
85 canonicalize: true,
86 }
87 }
88}
89
90#[derive(Debug, thiserror::Error)]
92pub enum EffectTraceCaptureError {
93 #[error("effect trace IO error at {path}: {source}")]
95 Io {
96 path: String,
98 source: std::io::Error,
100 },
101 #[error("effect trace JSON serialization failed: {source}")]
103 Json {
104 source: serde_json::Error,
106 },
107 #[error("effect trace CBOR serialization failed: {source}")]
109 Cbor {
110 source: serde_cbor::Error,
112 },
113}
114
115#[derive(Debug, Clone, Copy)]
117pub struct EffectTraceCapture {
118 options: EffectTraceCaptureOptions,
119}
120
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub struct EffectTraceBundle {
124 pub entries: Vec<EffectTraceEntry>,
126 #[serde(default)]
128 pub faults: Vec<AuraFault>,
129 #[serde(default)]
131 pub vm_determinism_profile: Option<AuraVmDeterminismProfileV1>,
132}
133
134impl Default for EffectTraceCapture {
135 fn default() -> Self {
136 Self::new(EffectTraceCaptureOptions::default())
137 }
138}
139
140impl EffectTraceCapture {
141 #[must_use]
143 pub fn new(options: EffectTraceCaptureOptions) -> Self {
144 Self { options }
145 }
146
147 #[must_use]
149 pub fn options(&self) -> EffectTraceCaptureOptions {
150 self.options
151 }
152
153 pub fn apply_to_vm_config(&self, config: &mut ProtocolMachineConfig) {
155 config.effect_trace_capture_mode = self.options.granularity.to_vm_mode();
156 }
157
158 #[must_use]
160 pub fn from_vm_config(config: &ProtocolMachineConfig) -> Self {
161 Self::new(EffectTraceCaptureOptions {
162 encoding: AuraEffectTraceEncoding::Json,
163 granularity: AuraEffectTraceGranularity::from_vm_mode(config.effect_trace_capture_mode),
164 canonicalize: true,
165 })
166 }
167
168 #[must_use]
170 pub fn capture_entries(&self, trace: &[EffectTraceEntry]) -> Vec<EffectTraceEntry> {
171 let mut entries = if self.options.canonicalize {
172 canonical_effect_trace(trace)
173 } else {
174 trace.to_vec()
175 };
176
177 match self.options.granularity {
178 AuraEffectTraceGranularity::Full => {}
179 AuraEffectTraceGranularity::TopologyOnly => {
180 entries.retain(|entry| entry.effect_kind == "topology_event");
181 }
182 AuraEffectTraceGranularity::Disabled => {
183 entries.clear();
184 }
185 }
186
187 entries
188 }
189
190 pub fn serialize_entries(
196 &self,
197 entries: &[EffectTraceEntry],
198 ) -> Result<Vec<u8>, EffectTraceCaptureError> {
199 match self.options.encoding {
200 AuraEffectTraceEncoding::Json => serde_json::to_vec(entries)
201 .map_err(|source| EffectTraceCaptureError::Json { source }),
202 AuraEffectTraceEncoding::Cbor => serde_cbor::to_vec(&entries.to_vec())
203 .map_err(|source| EffectTraceCaptureError::Cbor { source }),
204 }
205 }
206
207 pub fn deserialize_entries(
213 &self,
214 payload: &[u8],
215 ) -> Result<Vec<EffectTraceEntry>, EffectTraceCaptureError> {
216 match self.options.encoding {
217 AuraEffectTraceEncoding::Json => serde_json::from_slice(payload)
218 .map_err(|source| EffectTraceCaptureError::Json { source }),
219 AuraEffectTraceEncoding::Cbor => serde_cbor::from_slice(payload)
220 .map_err(|source| EffectTraceCaptureError::Cbor { source }),
221 }
222 }
223
224 pub fn serialize_trace(
230 &self,
231 trace: &[EffectTraceEntry],
232 ) -> Result<Vec<u8>, EffectTraceCaptureError> {
233 let captured = self.capture_entries(trace);
234 self.serialize_entries(&captured)
235 }
236
237 pub fn serialize_bundle(
243 &self,
244 trace: &[EffectTraceEntry],
245 faults: &[AuraFault],
246 vm_determinism_profile: Option<AuraVmDeterminismProfileV1>,
247 ) -> Result<Vec<u8>, EffectTraceCaptureError> {
248 let bundle = EffectTraceBundle {
249 entries: self.capture_entries(trace),
250 faults: faults.to_vec(),
251 vm_determinism_profile,
252 };
253 match self.options.encoding {
254 AuraEffectTraceEncoding::Json => serde_json::to_vec(&bundle)
255 .map_err(|source| EffectTraceCaptureError::Json { source }),
256 AuraEffectTraceEncoding::Cbor => serde_cbor::to_vec(&bundle)
257 .map_err(|source| EffectTraceCaptureError::Cbor { source }),
258 }
259 }
260
261 pub fn deserialize_bundle(
267 &self,
268 payload: &[u8],
269 ) -> Result<EffectTraceBundle, EffectTraceCaptureError> {
270 match self.options.encoding {
271 AuraEffectTraceEncoding::Json => serde_json::from_slice(payload)
272 .map_err(|source| EffectTraceCaptureError::Json { source }),
273 AuraEffectTraceEncoding::Cbor => serde_cbor::from_slice(payload)
274 .map_err(|source| EffectTraceCaptureError::Cbor { source }),
275 }
276 }
277
278 pub fn write_trace_file(
284 &self,
285 path: impl AsRef<Path>,
286 trace: &[EffectTraceEntry],
287 ) -> Result<(), EffectTraceCaptureError> {
288 let path_ref = path.as_ref();
289 let payload = self.serialize_trace(trace)?;
290 fs::write(path_ref, payload).map_err(|source| EffectTraceCaptureError::Io {
291 path: path_ref.display().to_string(),
292 source,
293 })
294 }
295
296 pub fn read_trace_file(
302 &self,
303 path: impl AsRef<Path>,
304 ) -> Result<Vec<EffectTraceEntry>, EffectTraceCaptureError> {
305 let path_ref = path.as_ref();
306 let payload = fs::read(path_ref).map_err(|source| EffectTraceCaptureError::Io {
307 path: path_ref.display().to_string(),
308 source,
309 })?;
310 self.deserialize_entries(&payload)
311 }
312
313 pub fn write_bundle_file(
319 &self,
320 path: impl AsRef<Path>,
321 trace: &[EffectTraceEntry],
322 faults: &[AuraFault],
323 vm_determinism_profile: Option<AuraVmDeterminismProfileV1>,
324 ) -> Result<(), EffectTraceCaptureError> {
325 let path_ref = path.as_ref();
326 let payload = self.serialize_bundle(trace, faults, vm_determinism_profile)?;
327 fs::write(path_ref, payload).map_err(|source| EffectTraceCaptureError::Io {
328 path: path_ref.display().to_string(),
329 source,
330 })
331 }
332
333 pub fn read_bundle_file(
339 &self,
340 path: impl AsRef<Path>,
341 ) -> Result<EffectTraceBundle, EffectTraceCaptureError> {
342 let path_ref = path.as_ref();
343 let payload = fs::read(path_ref).map_err(|source| EffectTraceCaptureError::Io {
344 path: path_ref.display().to_string(),
345 source,
346 })?;
347 self.deserialize_bundle(&payload)
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use serde_json::json;
355
356 fn sample_entry(effect_id: u64, effect_kind: &str) -> EffectTraceEntry {
357 EffectTraceEntry {
358 effect_id,
359 effect_kind: effect_kind.to_string(),
360 inputs: json!({"in": effect_id}),
361 outputs: json!({"out": effect_id}),
362 handler_identity: "handler".to_string(),
363 effect_interface: Some("AuraTest".to_string()),
364 effect_operation: Some(effect_kind.to_string()),
365 ordering_key: effect_id,
366 topology: None,
367 }
368 }
369
370 #[test]
371 fn topology_only_filters_non_topology_entries() {
372 let capture = EffectTraceCapture::new(EffectTraceCaptureOptions {
373 granularity: AuraEffectTraceGranularity::TopologyOnly,
374 canonicalize: false,
375 ..EffectTraceCaptureOptions::default()
376 });
377 let trace = vec![
378 sample_entry(0, "send_decision"),
379 sample_entry(1, "topology_event"),
380 ];
381
382 let captured = capture.capture_entries(&trace);
383 assert_eq!(captured.len(), 1);
384 assert_eq!(captured[0].effect_kind, "topology_event");
385 }
386
387 #[test]
388 fn disabled_capture_returns_empty_trace() {
389 let capture = EffectTraceCapture::new(EffectTraceCaptureOptions {
390 granularity: AuraEffectTraceGranularity::Disabled,
391 canonicalize: false,
392 ..EffectTraceCaptureOptions::default()
393 });
394 let trace = vec![sample_entry(0, "send_decision")];
395 assert!(capture.capture_entries(&trace).is_empty());
396 }
397
398 #[test]
399 fn cbor_roundtrip_succeeds() {
400 let capture = EffectTraceCapture::new(EffectTraceCaptureOptions {
401 encoding: AuraEffectTraceEncoding::Cbor,
402 canonicalize: false,
403 ..EffectTraceCaptureOptions::default()
404 });
405 let trace = vec![
406 sample_entry(0, "send_decision"),
407 sample_entry(1, "handle_recv"),
408 ];
409
410 let payload = capture.serialize_entries(&trace).expect("serialize cbor");
411 let decoded = capture
412 .deserialize_entries(&payload)
413 .expect("deserialize cbor");
414 assert_eq!(decoded, trace);
415 }
416
417 #[test]
418 fn json_bundle_roundtrip_preserves_faults() {
419 let capture = EffectTraceCapture::new(EffectTraceCaptureOptions {
420 encoding: AuraEffectTraceEncoding::Json,
421 canonicalize: false,
422 ..EffectTraceCaptureOptions::default()
423 });
424 let trace = vec![sample_entry(0, "send_decision")];
425 let faults = vec![AuraFault::new(aura_core::AuraFaultKind::Legacy {
426 fault_type: "network_partition".to_string(),
427 detail: Some("group=2".to_string()),
428 })];
429
430 let payload = capture
431 .serialize_bundle(&trace, &faults, None)
432 .expect("serialize bundle");
433 let decoded = capture
434 .deserialize_bundle(&payload)
435 .expect("deserialize bundle");
436 assert_eq!(decoded.entries, trace);
437 assert_eq!(decoded.faults, faults);
438 assert_eq!(decoded.vm_determinism_profile, None);
439 }
440
441 #[test]
442 fn bundle_roundtrip_preserves_determinism_profile() {
443 let capture = EffectTraceCapture::default();
444 let trace = vec![sample_entry(1, "handle_recv")];
445 let payload = capture
446 .serialize_bundle(
447 &trace,
448 &[],
449 Some(AuraVmDeterminismProfileV1 {
450 policy_ref: "aura.vm.recovery_grant.prod".to_string(),
451 protocol_class: "aura.recovery.grant".to_string(),
452 runtime_mode: "cooperative".to_string(),
453 scheduler_envelope_class: "exact".to_string(),
454 declared_wave_width_bound: Some(1),
455 determinism_mode: "full".to_string(),
456 effect_determinism_tier: "strict_deterministic".to_string(),
457 communication_replay_mode: "off".to_string(),
458 }),
459 )
460 .expect("serialize bundle");
461 let decoded = capture
462 .deserialize_bundle(&payload)
463 .expect("deserialize bundle");
464
465 assert_eq!(
466 decoded
467 .vm_determinism_profile
468 .as_ref()
469 .map(|profile| profile.policy_ref.as_str()),
470 Some("aura.vm.recovery_grant.prod")
471 );
472 }
473}