1use crate::scenario_orchestrator::{OrchestratedScenario, ScenarioStep};
7use chrono::{DateTime, Utc};
8use parking_lot::RwLock;
9use reqwest::Client;
10use serde::{Deserialize, Deserializer, Serialize};
11use serde_json::Value as JsonValue;
12use std::collections::HashMap;
13use std::sync::Arc;
14use std::time::Duration;
15use thiserror::Error;
16
17fn deserialize_body_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
19where
20 D: Deserializer<'de>,
21{
22 let value: Option<JsonValue> = Option::deserialize(deserializer)?;
23 match value {
24 None => Ok(None),
25 Some(JsonValue::String(s)) => Ok(Some(s)),
26 Some(json_obj) => {
27 serde_json::to_string(&json_obj).map_err(serde::de::Error::custom).map(Some)
29 }
30 }
31}
32
33#[derive(Error, Debug)]
35pub enum OrchestrationError {
36 #[error("Assertion failed: {0}")]
37 AssertionFailed(String),
38
39 #[error("Hook execution failed: {0}")]
40 HookFailed(String),
41
42 #[error("Variable not found: {0}")]
43 VariableNotFound(String),
44
45 #[error("Condition evaluation failed: {0}")]
46 ConditionFailed(String),
47
48 #[error("Serialization error: {0}")]
49 SerializationError(String),
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(tag = "type", rename_all = "snake_case")]
55pub enum Condition {
56 Equals { variable: String, value: JsonValue },
58 NotEquals { variable: String, value: JsonValue },
60 GreaterThan { variable: String, value: f64 },
62 LessThan { variable: String, value: f64 },
64 GreaterThanOrEqual { variable: String, value: f64 },
66 LessThanOrEqual { variable: String, value: f64 },
68 Exists { variable: String },
70 And { conditions: Vec<Condition> },
72 Or { conditions: Vec<Condition> },
74 Not { condition: Box<Condition> },
76 PreviousStepSucceeded,
78 PreviousStepFailed,
80 MetricThreshold {
82 metric_name: String,
83 operator: ComparisonOperator,
84 threshold: f64,
85 },
86}
87
88#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum ComparisonOperator {
92 Equals,
93 NotEquals,
94 GreaterThan,
95 LessThan,
96 GreaterThanOrEqual,
97 LessThanOrEqual,
98}
99
100impl Condition {
101 pub fn evaluate(&self, context: &ExecutionContext) -> Result<bool, OrchestrationError> {
103 match self {
104 Condition::Equals { variable, value } => {
105 let var_value = context.get_variable(variable)?;
106 Ok(var_value == value)
107 }
108 Condition::NotEquals { variable, value } => {
109 let var_value = context.get_variable(variable)?;
110 Ok(var_value != value)
111 }
112 Condition::GreaterThan { variable, value } => {
113 let var_value = context.get_variable(variable)?;
114 if let Some(num) = var_value.as_f64() {
115 Ok(num > *value)
116 } else {
117 Err(OrchestrationError::ConditionFailed(format!(
118 "Variable {} is not a number",
119 variable
120 )))
121 }
122 }
123 Condition::LessThan { variable, value } => {
124 let var_value = context.get_variable(variable)?;
125 if let Some(num) = var_value.as_f64() {
126 Ok(num < *value)
127 } else {
128 Err(OrchestrationError::ConditionFailed(format!(
129 "Variable {} is not a number",
130 variable
131 )))
132 }
133 }
134 Condition::GreaterThanOrEqual { variable, value } => {
135 let var_value = context.get_variable(variable)?;
136 if let Some(num) = var_value.as_f64() {
137 Ok(num >= *value)
138 } else {
139 Err(OrchestrationError::ConditionFailed(format!(
140 "Variable {} is not a number",
141 variable
142 )))
143 }
144 }
145 Condition::LessThanOrEqual { variable, value } => {
146 let var_value = context.get_variable(variable)?;
147 if let Some(num) = var_value.as_f64() {
148 Ok(num <= *value)
149 } else {
150 Err(OrchestrationError::ConditionFailed(format!(
151 "Variable {} is not a number",
152 variable
153 )))
154 }
155 }
156 Condition::Exists { variable } => Ok(context.variables.contains_key(variable)),
157 Condition::And { conditions } => {
158 for cond in conditions {
159 if !cond.evaluate(context)? {
160 return Ok(false);
161 }
162 }
163 Ok(true)
164 }
165 Condition::Or { conditions } => {
166 for cond in conditions {
167 if cond.evaluate(context)? {
168 return Ok(true);
169 }
170 }
171 Ok(false)
172 }
173 Condition::Not { condition } => Ok(!condition.evaluate(context)?),
174 Condition::PreviousStepSucceeded => Ok(context.last_step_success),
175 Condition::PreviousStepFailed => Ok(!context.last_step_success),
176 Condition::MetricThreshold {
177 metric_name,
178 operator,
179 threshold,
180 } => {
181 if let Some(value) = context.metrics.get(metric_name) {
182 Ok(match operator {
183 ComparisonOperator::Equals => (value - threshold).abs() < f64::EPSILON,
184 ComparisonOperator::NotEquals => (value - threshold).abs() >= f64::EPSILON,
185 ComparisonOperator::GreaterThan => value > threshold,
186 ComparisonOperator::LessThan => value < threshold,
187 ComparisonOperator::GreaterThanOrEqual => value >= threshold,
188 ComparisonOperator::LessThanOrEqual => value <= threshold,
189 })
190 } else {
191 Err(OrchestrationError::ConditionFailed(format!(
192 "Metric {} not found",
193 metric_name
194 )))
195 }
196 }
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ConditionalStep {
204 pub name: String,
206 pub condition: Condition,
208 pub then_steps: Vec<AdvancedScenarioStep>,
210 pub else_steps: Vec<AdvancedScenarioStep>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum HookType {
218 PreStep,
220 PostStep,
222 PreOrchestration,
224 PostOrchestration,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(tag = "type", rename_all = "snake_case")]
231pub enum HookAction {
232 SetVariable { name: String, value: JsonValue },
234 Log { message: String, level: LogLevel },
236 HttpRequest {
238 url: String,
239 method: String,
240 #[serde(default, deserialize_with = "deserialize_body_string")]
243 body: Option<String>,
244 },
245 Command { command: String, args: Vec<String> },
247 RecordMetric { name: String, value: f64 },
249}
250
251#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
253#[serde(rename_all = "lowercase")]
254pub enum LogLevel {
255 Trace,
256 Debug,
257 Info,
258 Warn,
259 Error,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct Hook {
265 pub name: String,
267 pub hook_type: HookType,
269 pub actions: Vec<HookAction>,
271 pub condition: Option<Condition>,
273}
274
275impl Hook {
276 pub async fn execute(&self, context: &mut ExecutionContext) -> Result<(), OrchestrationError> {
278 if let Some(condition) = &self.condition {
280 if !condition.evaluate(context)? {
281 return Ok(());
282 }
283 }
284
285 for action in &self.actions {
286 self.execute_action(action, context).await?;
287 }
288
289 Ok(())
290 }
291
292 async fn execute_action(
294 &self,
295 action: &HookAction,
296 context: &mut ExecutionContext,
297 ) -> Result<(), OrchestrationError> {
298 match action {
299 HookAction::SetVariable { name, value } => {
300 context.set_variable(name.clone(), value.clone());
301 Ok(())
302 }
303 HookAction::Log { message, level } => {
304 use tracing::{debug, error, info, trace, warn};
305 match level {
306 LogLevel::Trace => trace!("[Hook: {}] {}", self.name, message),
307 LogLevel::Debug => debug!("[Hook: {}] {}", self.name, message),
308 LogLevel::Info => info!("[Hook: {}] {}", self.name, message),
309 LogLevel::Warn => warn!("[Hook: {}] {}", self.name, message),
310 LogLevel::Error => error!("[Hook: {}] {}", self.name, message),
311 }
312 Ok(())
313 }
314 HookAction::HttpRequest { url, method, body } => {
315 let client =
317 Client::builder().timeout(Duration::from_secs(30)).build().map_err(|e| {
318 OrchestrationError::HookFailed(format!(
319 "Failed to create HTTP client: {}",
320 e
321 ))
322 })?;
323
324 let http_method = method.parse::<reqwest::Method>().map_err(|e| {
325 OrchestrationError::HookFailed(format!(
326 "Invalid HTTP method '{}': {}",
327 method, e
328 ))
329 })?;
330
331 let mut request_builder = client.request(http_method.clone(), url);
332
333 if let Some(body_value) = body {
335 request_builder = request_builder
336 .header("Content-Type", "application/json")
337 .body(body_value.clone());
338 }
339
340 match request_builder.send().await {
342 Ok(response) => {
343 let status = response.status();
344 let response_body = response.text().await.unwrap_or_default();
345 tracing::info!(
346 "[Hook: {}] HTTP {} {} → {} (body: {})",
347 self.name,
348 http_method,
349 url,
350 status,
351 if response_body.len() > 100 {
352 format!("{}...", &response_body[..100])
353 } else {
354 response_body
355 }
356 );
357
358 Ok(())
361 }
362 Err(e) => {
363 tracing::warn!(
364 "[Hook: {}] HTTP {} {} failed: {}",
365 self.name,
366 http_method,
367 url,
368 e
369 );
370 Ok(())
373 }
374 }
375 }
376 HookAction::Command { command, args } => {
377 tracing::info!("[Hook: {}] Execute: {} {:?}", self.name, command, args);
380 Ok(())
381 }
382 HookAction::RecordMetric { name, value } => {
383 context.record_metric(name.clone(), *value);
384 Ok(())
385 }
386 }
387 }
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392#[serde(tag = "type", rename_all = "snake_case")]
393pub enum Assertion {
394 VariableEquals {
396 variable: String,
397 expected: JsonValue,
398 },
399 MetricInRange { metric: String, min: f64, max: f64 },
401 StepSucceeded { step_name: String },
403 StepFailed { step_name: String },
405 Condition { condition: Condition },
407}
408
409impl Assertion {
410 pub fn validate(&self, context: &ExecutionContext) -> Result<bool, OrchestrationError> {
412 match self {
413 Assertion::VariableEquals { variable, expected } => {
414 let value = context.get_variable(variable)?;
415 Ok(value == expected)
416 }
417 Assertion::MetricInRange { metric, min, max } => {
418 if let Some(value) = context.metrics.get(metric) {
419 Ok(*value >= *min && *value <= *max)
420 } else {
421 Ok(false)
422 }
423 }
424 Assertion::StepSucceeded { step_name } => {
425 if let Some(result) = context.step_results.get(step_name) {
426 Ok(result.success)
427 } else {
428 Ok(false)
429 }
430 }
431 Assertion::StepFailed { step_name } => {
432 if let Some(result) = context.step_results.get(step_name) {
433 Ok(!result.success)
434 } else {
435 Ok(false)
436 }
437 }
438 Assertion::Condition { condition } => condition.evaluate(context),
439 }
440 }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct AdvancedScenarioStep {
446 #[serde(flatten)]
448 pub base: ScenarioStep,
449
450 pub condition: Option<Condition>,
452
453 pub pre_hooks: Vec<Hook>,
455
456 pub post_hooks: Vec<Hook>,
458
459 pub assertions: Vec<Assertion>,
461
462 pub variables: HashMap<String, JsonValue>,
464
465 pub timeout_seconds: Option<u64>,
467
468 pub retry: Option<RetryConfig>,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct RetryConfig {
475 pub max_attempts: usize,
477 pub delay_seconds: u64,
479 pub exponential_backoff: bool,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct StepResult {
486 pub step_name: String,
488 pub success: bool,
490 pub start_time: DateTime<Utc>,
492 pub end_time: DateTime<Utc>,
494 pub duration_seconds: f64,
496 pub error: Option<String>,
498 pub assertion_results: Vec<AssertionResult>,
500 pub metrics: HashMap<String, f64>,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct AssertionResult {
507 pub description: String,
509 pub passed: bool,
511 pub error: Option<String>,
513}
514
515#[derive(Debug, Clone)]
517pub struct ExecutionContext {
518 pub variables: HashMap<String, JsonValue>,
520 pub metrics: HashMap<String, f64>,
522 pub step_results: HashMap<String, StepResult>,
524 pub last_step_success: bool,
526 pub iteration: usize,
528}
529
530impl ExecutionContext {
531 pub fn new() -> Self {
533 Self {
534 variables: HashMap::new(),
535 metrics: HashMap::new(),
536 step_results: HashMap::new(),
537 last_step_success: true,
538 iteration: 0,
539 }
540 }
541
542 pub fn set_variable(&mut self, name: String, value: JsonValue) {
544 self.variables.insert(name, value);
545 }
546
547 pub fn get_variable(&self, name: &str) -> Result<&JsonValue, OrchestrationError> {
549 self.variables
550 .get(name)
551 .ok_or_else(|| OrchestrationError::VariableNotFound(name.to_string()))
552 }
553
554 pub fn record_metric(&mut self, name: String, value: f64) {
556 self.metrics.insert(name, value);
557 }
558
559 pub fn record_step_result(&mut self, result: StepResult) {
561 self.last_step_success = result.success;
562 self.step_results.insert(result.step_name.clone(), result);
563 }
564}
565
566impl Default for ExecutionContext {
567 fn default() -> Self {
568 Self::new()
569 }
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct AdvancedOrchestratedScenario {
575 #[serde(flatten)]
577 pub base: OrchestratedScenario,
578
579 pub advanced_steps: Vec<AdvancedScenarioStep>,
581
582 pub conditional_steps: Vec<ConditionalStep>,
584
585 pub hooks: Vec<Hook>,
587
588 pub assertions: Vec<Assertion>,
590
591 pub variables: HashMap<String, JsonValue>,
593
594 pub enable_reporting: bool,
596
597 pub report_path: Option<String>,
599}
600
601impl AdvancedOrchestratedScenario {
602 pub fn from_base(base: OrchestratedScenario) -> Self {
604 Self {
605 base,
606 advanced_steps: Vec::new(),
607 conditional_steps: Vec::new(),
608 hooks: Vec::new(),
609 assertions: Vec::new(),
610 variables: HashMap::new(),
611 enable_reporting: false,
612 report_path: None,
613 }
614 }
615
616 pub fn with_variable(mut self, name: String, value: JsonValue) -> Self {
618 self.variables.insert(name, value);
619 self
620 }
621
622 pub fn with_hook(mut self, hook: Hook) -> Self {
624 self.hooks.push(hook);
625 self
626 }
627
628 pub fn with_assertion(mut self, assertion: Assertion) -> Self {
630 self.assertions.push(assertion);
631 self
632 }
633
634 pub fn with_reporting(mut self, path: Option<String>) -> Self {
636 self.enable_reporting = true;
637 self.report_path = path;
638 self
639 }
640
641 pub fn to_json(&self) -> Result<String, OrchestrationError> {
643 serde_json::to_string_pretty(self)
644 .map_err(|e| OrchestrationError::SerializationError(e.to_string()))
645 }
646
647 pub fn to_yaml(&self) -> Result<String, OrchestrationError> {
649 serde_yaml::to_string(self)
650 .map_err(|e| OrchestrationError::SerializationError(e.to_string()))
651 }
652
653 pub fn from_json(json: &str) -> Result<Self, OrchestrationError> {
655 serde_json::from_str(json)
656 .map_err(|e| OrchestrationError::SerializationError(e.to_string()))
657 }
658
659 pub fn from_yaml(yaml: &str) -> Result<Self, OrchestrationError> {
661 serde_yaml::from_str(yaml)
662 .map_err(|e| OrchestrationError::SerializationError(e.to_string()))
663 }
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize)]
668pub struct ExecutionReport {
669 pub orchestration_name: String,
671 pub start_time: DateTime<Utc>,
673 pub end_time: DateTime<Utc>,
675 pub total_duration_seconds: f64,
677 pub success: bool,
679 pub step_results: Vec<StepResult>,
681 pub assertion_results: Vec<AssertionResult>,
683 pub final_variables: HashMap<String, JsonValue>,
685 pub final_metrics: HashMap<String, f64>,
687 pub errors: Vec<String>,
689}
690
691impl ExecutionReport {
692 pub fn new(orchestration_name: String, start_time: DateTime<Utc>) -> Self {
694 Self {
695 orchestration_name,
696 start_time,
697 end_time: Utc::now(),
698 total_duration_seconds: 0.0,
699 success: true,
700 step_results: Vec::new(),
701 assertion_results: Vec::new(),
702 final_variables: HashMap::new(),
703 final_metrics: HashMap::new(),
704 errors: Vec::new(),
705 }
706 }
707
708 pub fn finalize(mut self, context: &ExecutionContext) -> Self {
710 self.end_time = Utc::now();
711 self.total_duration_seconds =
712 (self.end_time - self.start_time).num_milliseconds() as f64 / 1000.0;
713 self.final_variables = context.variables.clone();
714 self.final_metrics = context.metrics.clone();
715 self.step_results = context.step_results.values().cloned().collect();
716 self.success = self.step_results.iter().all(|r| r.success) && self.errors.is_empty();
717 self
718 }
719
720 pub fn to_json(&self) -> Result<String, OrchestrationError> {
722 serde_json::to_string_pretty(self)
723 .map_err(|e| OrchestrationError::SerializationError(e.to_string()))
724 }
725
726 pub fn to_html(&self) -> String {
728 format!(
729 r#"<!DOCTYPE html>
730<html>
731<head>
732 <title>Chaos Orchestration Report: {}</title>
733 <style>
734 body {{ font-family: Arial, sans-serif; margin: 20px; }}
735 .header {{ background: #f5f5f5; padding: 20px; border-radius: 5px; }}
736 .success {{ color: green; }}
737 .failure {{ color: red; }}
738 table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
739 th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
740 th {{ background: #f5f5f5; }}
741 </style>
742</head>
743<body>
744 <div class="header">
745 <h1>Chaos Orchestration Report</h1>
746 <h2>{}</h2>
747 <p><strong>Status:</strong> <span class="{}">{}</span></p>
748 <p><strong>Duration:</strong> {:.2} seconds</p>
749 <p><strong>Start Time:</strong> {}</p>
750 <p><strong>End Time:</strong> {}</p>
751 </div>
752
753 <h2>Step Results</h2>
754 <table>
755 <tr>
756 <th>Step</th>
757 <th>Status</th>
758 <th>Duration (s)</th>
759 <th>Assertions</th>
760 </tr>
761 {}
762 </table>
763
764 <h2>Metrics</h2>
765 <table>
766 <tr>
767 <th>Metric</th>
768 <th>Value</th>
769 </tr>
770 {}
771 </table>
772</body>
773</html>"#,
774 self.orchestration_name,
775 self.orchestration_name,
776 if self.success { "success" } else { "failure" },
777 if self.success { "SUCCESS" } else { "FAILURE" },
778 self.total_duration_seconds,
779 self.start_time,
780 self.end_time,
781 self.step_results
782 .iter()
783 .map(|r| format!(
784 "<tr><td>{}</td><td class=\"{}\">{}</td><td>{:.2}</td><td>{}/{}</td></tr>",
785 r.step_name,
786 if r.success { "success" } else { "failure" },
787 if r.success { "SUCCESS" } else { "FAILURE" },
788 r.duration_seconds,
789 r.assertion_results.iter().filter(|a| a.passed).count(),
790 r.assertion_results.len()
791 ))
792 .collect::<Vec<_>>()
793 .join("\n "),
794 self.final_metrics
795 .iter()
796 .map(|(k, v)| format!("<tr><td>{}</td><td>{:.2}</td></tr>", k, v))
797 .collect::<Vec<_>>()
798 .join("\n ")
799 )
800 }
801}
802
803#[derive(Debug, Clone)]
805pub struct OrchestrationLibrary {
806 orchestrations: Arc<RwLock<HashMap<String, AdvancedOrchestratedScenario>>>,
808}
809
810impl OrchestrationLibrary {
811 pub fn new() -> Self {
813 Self {
814 orchestrations: Arc::new(RwLock::new(HashMap::new())),
815 }
816 }
817
818 pub fn store(&self, name: String, orchestration: AdvancedOrchestratedScenario) {
820 let mut orch = self.orchestrations.write();
821 orch.insert(name, orchestration);
822 }
823
824 pub fn retrieve(&self, name: &str) -> Option<AdvancedOrchestratedScenario> {
826 let orch = self.orchestrations.read();
827 orch.get(name).cloned()
828 }
829
830 pub fn list(&self) -> Vec<String> {
832 let orch = self.orchestrations.read();
833 orch.keys().cloned().collect()
834 }
835
836 pub fn delete(&self, name: &str) -> bool {
838 let mut orch = self.orchestrations.write();
839 orch.remove(name).is_some()
840 }
841
842 pub fn import_from_directory(&self, _path: &str) -> Result<usize, OrchestrationError> {
844 Ok(0)
847 }
848
849 pub fn export_to_directory(&self, _path: &str) -> Result<usize, OrchestrationError> {
851 let orch = self.orchestrations.read();
854 Ok(orch.len())
855 }
856}
857
858impl Default for OrchestrationLibrary {
859 fn default() -> Self {
860 Self::new()
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867
868 #[test]
869 fn test_condition_equals() {
870 let mut context = ExecutionContext::new();
871 context.set_variable("test".to_string(), JsonValue::String("value".to_string()));
872
873 let condition = Condition::Equals {
874 variable: "test".to_string(),
875 value: JsonValue::String("value".to_string()),
876 };
877
878 assert!(condition.evaluate(&context).unwrap());
879 }
880
881 #[test]
882 fn test_condition_and() {
883 let mut context = ExecutionContext::new();
884 context.set_variable("a".to_string(), JsonValue::Number(5.into()));
885 context.set_variable("b".to_string(), JsonValue::Number(10.into()));
886
887 let condition = Condition::And {
888 conditions: vec![
889 Condition::GreaterThan {
890 variable: "a".to_string(),
891 value: 3.0,
892 },
893 Condition::LessThan {
894 variable: "b".to_string(),
895 value: 15.0,
896 },
897 ],
898 };
899
900 assert!(condition.evaluate(&context).unwrap());
901 }
902
903 #[test]
904 fn test_execution_context() {
905 let mut context = ExecutionContext::new();
906 context.set_variable("test".to_string(), JsonValue::String("value".to_string()));
907 context.record_metric("latency".to_string(), 100.0);
908
909 assert_eq!(context.get_variable("test").unwrap(), &JsonValue::String("value".to_string()));
910 assert_eq!(*context.metrics.get("latency").unwrap(), 100.0);
911 }
912
913 #[test]
914 fn test_orchestration_library() {
915 let library = OrchestrationLibrary::new();
916
917 let orch = AdvancedOrchestratedScenario::from_base(OrchestratedScenario::new("test"));
918
919 library.store("test".to_string(), orch.clone());
920
921 let retrieved = library.retrieve("test");
922 assert!(retrieved.is_some());
923
924 let list = library.list();
925 assert_eq!(list.len(), 1);
926
927 let deleted = library.delete("test");
928 assert!(deleted);
929
930 let list = library.list();
931 assert_eq!(list.len(), 0);
932 }
933
934 #[test]
935 fn test_execution_report() {
936 let report = ExecutionReport::new("test".to_string(), Utc::now());
937 let context = ExecutionContext::new();
938
939 let final_report = report.finalize(&context);
940 assert_eq!(final_report.orchestration_name, "test");
941 assert!(final_report.total_duration_seconds >= 0.0);
942 }
943}