1use opentelemetry::trace::{Span, SpanKind, Status, Tracer};
24use opentelemetry::{global, KeyValue};
25use std::time::{Duration, SystemTime};
26use tracing::{error, info};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum TestResult {
31 Pass,
33 Fail,
35 Error,
37}
38
39impl TestResult {
40 pub fn as_str(&self) -> &'static str {
42 match self {
43 TestResult::Pass => "pass",
44 TestResult::Fail => "fail",
45 TestResult::Error => "error",
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct ContainerInfo {
53 pub id: String,
55 pub image_name: String,
57 pub image_tag: Option<String>,
59 pub exit_code: Option<i32>,
61}
62
63impl ContainerInfo {
64 pub fn new(id: String, image: String) -> Self {
66 let (name, tag) = if let Some(pos) = image.rfind(':') {
68 let (n, t) = image.split_at(pos);
69 (n.to_string(), Some(t[1..].to_string()))
70 } else {
71 (image, Some("latest".to_string()))
72 };
73
74 Self {
75 id,
76 image_name: name,
77 image_tag: tag,
78 exit_code: None,
79 }
80 }
81
82 pub fn with_exit_code(mut self, code: i32) -> Self {
84 self.exit_code = Some(code);
85 self
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct TestExecutionContext {
92 pub test_name: String,
94 pub test_suite: String,
96 pub test_isolated: bool,
98 pub test_result: TestResult,
100 pub test_duration_ms: f64,
102 pub test_start_timestamp: i64,
104 pub test_end_timestamp: i64,
106 pub container_info: Option<ContainerInfo>,
108 pub error_type: Option<String>,
110 pub error_message: Option<String>,
112 pub assertion_count: Option<u32>,
114 pub cleanup_performed: bool,
116 pub plugin_execution_time_ms: Option<f64>,
118}
119
120impl TestExecutionContext {
121 pub fn new(test_name: String, test_suite: String) -> Self {
123 Self {
124 test_name,
125 test_suite,
126 test_isolated: true, test_result: TestResult::Pass,
128 test_duration_ms: 0.0,
129 test_start_timestamp: Self::now_unix_ms(),
130 test_end_timestamp: 0,
131 container_info: None,
132 error_type: None,
133 error_message: None,
134 assertion_count: None,
135 cleanup_performed: false,
136 plugin_execution_time_ms: None,
137 }
138 }
139
140 fn now_unix_ms() -> i64 {
142 SystemTime::now()
143 .duration_since(std::time::UNIX_EPOCH)
144 .unwrap_or_default()
145 .as_millis() as i64
146 }
147
148 pub fn with_container(mut self, container: ContainerInfo) -> Self {
150 self.container_info = Some(container);
151 self
152 }
153
154 pub fn with_result(mut self, result: TestResult, duration: Duration) -> Self {
156 self.test_result = result;
157 self.test_duration_ms = duration.as_secs_f64() * 1000.0;
158 self.test_end_timestamp = Self::now_unix_ms();
159 self
160 }
161
162 pub fn with_error(mut self, error_type: String, error_message: String) -> Self {
164 self.error_type = Some(error_type);
165 self.error_message = Some(error_message);
166 self
167 }
168
169 pub fn with_cleanup(mut self, performed: bool) -> Self {
171 self.cleanup_performed = performed;
172 self
173 }
174
175 pub fn with_assertions(mut self, count: u32) -> Self {
177 self.assertion_count = Some(count);
178 self
179 }
180
181 pub fn with_plugin_time(mut self, time_ms: f64) -> Self {
183 self.plugin_execution_time_ms = Some(time_ms);
184 self
185 }
186
187 pub fn emit_span(&self) {
193 info!(
194 "🔍 Emitting test execution span: {} (result={}, duration={:.2}ms)",
195 self.test_name,
196 self.test_result.as_str(),
197 self.test_duration_ms
198 );
199
200 let tracer = global::tracer("clnrm");
201 let mut span = tracer
202 .span_builder("clnrm.test_execution")
203 .with_kind(SpanKind::Internal)
204 .start(&tracer);
205
206 span.set_attribute(KeyValue::new("test.name", self.test_name.clone()));
208 span.set_attribute(KeyValue::new("test.suite", self.test_suite.clone()));
209 span.set_attribute(KeyValue::new("test.isolated", self.test_isolated));
210 span.set_attribute(KeyValue::new("test.result", self.test_result.as_str()));
211 span.set_attribute(KeyValue::new("test.duration_ms", self.test_duration_ms));
212 span.set_attribute(KeyValue::new(
213 "test.start_timestamp",
214 self.test_start_timestamp,
215 ));
216 span.set_attribute(KeyValue::new("test.end_timestamp", self.test_end_timestamp));
217 span.set_attribute(KeyValue::new(
218 "test.cleanup_performed",
219 self.cleanup_performed,
220 ));
221
222 if let Some(ref container) = self.container_info {
224 span.set_attribute(KeyValue::new("container.id", container.id.clone()));
225 span.set_attribute(KeyValue::new(
226 "container.image.name",
227 container.image_name.clone(),
228 ));
229
230 if let Some(ref tag) = container.image_tag {
231 span.set_attribute(KeyValue::new("container.image.tag", tag.clone()));
232 }
233
234 if let Some(exit_code) = container.exit_code {
235 span.set_attribute(KeyValue::new("container.exit_code", exit_code as i64));
236 }
237 } else {
238 error!(
239 "⚠️ Test '{}' missing container.id - VALIDATION WILL FAIL",
240 self.test_name
241 );
242 }
243
244 if let Some(ref error_type) = self.error_type {
246 span.set_attribute(KeyValue::new("error.type", error_type.clone()));
247 }
248
249 if let Some(ref error_message) = self.error_message {
250 span.set_attribute(KeyValue::new("error.message", error_message.clone()));
251 }
252
253 if let Some(count) = self.assertion_count {
255 span.set_attribute(KeyValue::new("test.assertion_count", count as i64));
256 }
257
258 if let Some(time) = self.plugin_execution_time_ms {
259 span.set_attribute(KeyValue::new("plugin.execution_time_ms", time));
260 }
261
262 match self.test_result {
264 TestResult::Pass => {
265 span.set_status(Status::Ok);
266 }
267 TestResult::Fail | TestResult::Error => {
268 let msg = self
269 .error_message
270 .clone()
271 .unwrap_or_else(|| "Test failed".to_string());
272 span.set_status(Status::error(msg));
273 }
274 }
275
276 span.end();
277
278 info!(
279 "✅ Test execution span emitted: {} attributes ({}% complete)",
280 if self.container_info.is_some() {
281 "9/9 required"
282 } else {
283 "8/9 required"
284 },
285 if self.container_info.is_some() {
286 100
287 } else {
288 89
289 }
290 );
291 }
292
293 pub fn validate(&self) -> Result<(), String> {
298 let mut errors = Vec::new();
299
300 if self.test_name.is_empty() {
302 errors.push("test.name is empty");
303 }
304 if self.test_suite.is_empty() {
305 errors.push("test.suite is empty");
306 }
307
308 if self.test_duration_ms <= 0.0 {
310 errors.push("test.duration_ms must be > 0 (proves actual execution)");
311 }
312
313 if self.test_end_timestamp == 0 {
315 errors.push("test.end_timestamp must be set (proves completion)");
316 }
317
318 if self.container_info.is_none() {
320 errors.push("container.id is missing (CRITICAL PROOF attribute)");
321 }
322
323 match self.test_result {
325 TestResult::Error => {
326 if self.error_type.is_none() {
327 errors.push("error.type required when test.result is 'error'");
328 }
329 if self.error_message.is_none() {
330 errors.push("error.message required when test.result is 'error'");
331 }
332 }
333 TestResult::Fail => {
334 if self.error_message.is_none() {
335 errors.push("error.message required when test.result is 'fail'");
336 }
337 }
338 TestResult::Pass => {}
339 }
340
341 if errors.is_empty() {
342 Ok(())
343 } else {
344 Err(format!(
345 "Invalid test execution context: {}",
346 errors.join(", ")
347 ))
348 }
349 }
350}
351
352pub struct TestExecutionBuilder {
354 context: TestExecutionContext,
355 start_time: std::time::Instant,
356}
357
358impl TestExecutionBuilder {
359 pub fn new(test_name: String, test_suite: String) -> Self {
361 Self {
362 context: TestExecutionContext::new(test_name, test_suite),
363 start_time: std::time::Instant::now(),
364 }
365 }
366
367 pub fn container(mut self, container: ContainerInfo) -> Self {
369 self.context = self.context.with_container(container);
370 self
371 }
372
373 pub fn error(mut self, error_type: String, error_message: String) -> Self {
375 self.context = self.context.with_error(error_type, error_message);
376 self
377 }
378
379 pub fn assertions(mut self, count: u32) -> Self {
381 self.context = self.context.with_assertions(count);
382 self
383 }
384
385 pub fn plugin_time(mut self, time_ms: f64) -> Self {
387 self.context = self.context.with_plugin_time(time_ms);
388 self
389 }
390
391 pub fn cleanup_done(mut self) -> Self {
393 self.context = self.context.with_cleanup(true);
394 self
395 }
396
397 pub fn finish(mut self, result: TestResult) -> TestExecutionContext {
399 let duration = self.start_time.elapsed();
400 self.context = self.context.with_result(result, duration);
401
402 if let Err(e) = self.context.validate() {
404 error!("⚠️ Test execution context invalid: {}", e);
405 error!("⚠️ Span will be emitted but may fail validation");
406 }
407
408 self.context.emit_span();
410
411 self.context
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_result_as_str() {
421 assert_eq!(TestResult::Pass.as_str(), "pass");
422 assert_eq!(TestResult::Fail.as_str(), "fail");
423 assert_eq!(TestResult::Error.as_str(), "error");
424 }
425
426 #[test]
427 fn test_container_info_parsing() {
428 let container = ContainerInfo::new("abc123".to_string(), "postgres:15".to_string());
429 assert_eq!(container.id, "abc123");
430 assert_eq!(container.image_name, "postgres");
431 assert_eq!(container.image_tag, Some("15".to_string()));
432
433 let container2 = ContainerInfo::new("def456".to_string(), "alpine".to_string());
434 assert_eq!(container2.image_name, "alpine");
435 assert_eq!(container2.image_tag, Some("latest".to_string()));
436 }
437
438 #[test]
439 fn test_context_validation_pass() {
440 let mut context = TestExecutionContext::new("test_1".to_string(), "suite_1".to_string());
441 context.test_duration_ms = 100.0;
442 context.test_end_timestamp = 1730250000000; context.container_info = Some(ContainerInfo::new(
444 "container123".to_string(),
445 "alpine:latest".to_string(),
446 ));
447
448 assert!(context.validate().is_ok());
449 }
450
451 #[test]
452 fn test_context_validation_missing_container() {
453 let mut context = TestExecutionContext::new("test_1".to_string(), "suite_1".to_string());
454 context.test_duration_ms = 100.0;
455 context.test_end_timestamp = 1730250000000;
456 let result = context.validate();
459 assert!(result.is_err());
460 assert!(result.unwrap_err().contains("container.id"));
461 }
462
463 #[test]
464 fn test_context_validation_error_requires_error_type() {
465 let mut context = TestExecutionContext::new("test_1".to_string(), "suite_1".to_string());
466 context.test_duration_ms = 100.0;
467 context.test_end_timestamp = 1730250000000;
468 context.container_info = Some(ContainerInfo::new(
469 "container123".to_string(),
470 "alpine:latest".to_string(),
471 ));
472 context.test_result = TestResult::Error;
473 let result = context.validate();
476 assert!(result.is_err());
477 let err_msg = result.unwrap_err();
478 assert!(err_msg.contains("error.type"));
479 assert!(err_msg.contains("error.message"));
480 }
481
482 #[test]
483 fn test_builder_fluent_api() {
484 let container = ContainerInfo::new("test123".to_string(), "alpine:3.18".to_string());
485 let builder =
486 TestExecutionBuilder::new("test_example".to_string(), "integration".to_string())
487 .container(container)
488 .assertions(5)
489 .plugin_time(45.2)
490 .cleanup_done();
491
492 assert_eq!(builder.context.test_name, "test_example");
494 assert_eq!(builder.context.assertion_count, Some(5));
495 assert_eq!(builder.context.plugin_execution_time_ms, Some(45.2));
496 assert!(builder.context.cleanup_performed);
497 }
498}