grpctestify 1.6.1

gRPC testing utility written in Rust
Documentation
use crate::state::{TestResult, TestResults};
use anyhow::Result;
use serde_json::json;
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};

use super::Reporter;

pub struct StreamingJsonReporter {
    suite_started: AtomicBool,
    test_count: usize,
}

impl StreamingJsonReporter {
    pub fn new(test_count: usize) -> Self {
        Self {
            suite_started: AtomicBool::new(false),
            test_count,
        }
    }

    fn emit(&self, event: &serde_json::Value) {
        let mut stdout = io::stdout().lock();
        match serde_json::to_string(event) {
            Ok(s) => {
                if let Err(e) = writeln!(stdout, "{}", s) {
                    tracing::warn!("Failed to write streaming JSON to stdout: {e}");
                }
                if let Err(e) = stdout.flush() {
                    tracing::warn!("Failed to flush stdout: {e}");
                }
            }
            Err(e) => {
                tracing::warn!("Failed to serialize streaming event: {e}");
            }
        }
    }
}

impl Reporter for StreamingJsonReporter {
    fn on_test_start(&self, test_name: &str) {
        if !self.suite_started.swap(true, Ordering::SeqCst) {
            self.emit(&json!({
                "event": "suite_start",
                "testCount": self.test_count,
                "timestamp": crate::time::now_rfc3339()
            }));
        }

        self.emit(&json!({
            "event": "test_start",
            "testId": test_name,
            "timestamp": crate::time::now_rfc3339()
        }));
    }

    fn on_test_end(&self, test_name: &str, result: &TestResult) {
        let event_type = match result.status {
            crate::state::TestStatus::Pass => "test_pass",
            crate::state::TestStatus::Fail => "test_fail",
            crate::state::TestStatus::Skip => "test_skip",
        };

        let mut event = json!({
            "event": event_type,
            "testId": test_name,
            "duration": result.duration_ms,
            "timestamp": crate::time::now_rfc3339()
        });

        if let Some(msg) = &result.error_message {
            event["message"] = json!(msg);
        }

        if let Some(grpc_ms) = result.grpc_duration_ms {
            event["grpcDuration"] = json!(grpc_ms);
        }

        self.emit(&event);
    }

    fn on_suite_end(&self, results: &TestResults) -> Result<()> {
        self.emit(&json!({
            "event": "suite_end",
            "summary": {
                "total": results.total(),
                "passed": results.passed(),
                "failed": results.failed(),
                "skipped": results.skipped(),
                "duration": results.metrics.total_duration_ms
            },
            "timestamp": crate::time::now_rfc3339()
        }));

        Ok(())
    }
}