grpctestify 1.8.6

gRPC testing utility written in Rust
Documentation
use crate::execution::{ExecutionPlan, Workflow};
use crate::parser::ast::{GctfDocument, SectionType};
use crate::state::{TestResult, TestStatus};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelReportContext {
    pub tool: String,
    pub version: String,
    pub generated_at: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelPhase {
    pub kind: String,
    pub details: String,
    pub status: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelCall {
    pub call_index: usize,
    pub doc_index: usize,
    pub endpoint: String,
    pub package: Option<String>,
    pub service: Option<String>,
    pub method: Option<String>,
    pub request_count: usize,
    pub expectation_kind: String,
    pub rpc_mode: String,
    pub display_name: String,
    pub status: String,
    pub phases: Vec<KernelPhase>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct KernelGrpcLabels {
    pub endpoints: Vec<String>,
    pub packages: Vec<String>,
    pub services: Vec<String>,
    pub methods: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelFailure {
    pub call_index: usize,
    pub phase: String,
    pub category: String,
    pub message: String,
    pub grpc_code: Option<i32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelTestCase {
    pub case_index: usize,
    pub test_path: String,
    pub display_name: String,
    pub status: String,
    pub duration_ms: Option<u64>,
    pub calls: Vec<KernelCall>,
    pub failures: Vec<KernelFailure>,
    pub grpc_labels: KernelGrpcLabels,
}

fn call_display_name(doc: &GctfDocument, call_index: usize, endpoint: &str) -> String {
    let named = doc
        .sections
        .iter()
        .find_map(|s| s.get_attribute("name").map(|a| a.value.trim().to_string()))
        .filter(|s| !s.is_empty());

    match named {
        Some(name) => format!("#{} {} ({})", call_index, name, endpoint),
        None => format!("#{} {}", call_index, endpoint),
    }
}

fn call_phases(doc: &GctfDocument, call_status: &str) -> Vec<KernelPhase> {
    let plan = ExecutionPlan::from_document(doc);
    let mut phases = Vec::new();

    let request_count = plan.summary.total_requests;
    if request_count > 0 {
        phases.push(KernelPhase {
            kind: "request".to_string(),
            details: format!("messages={}", request_count),
            status: "passed".to_string(),
        });
    }

    let response_sections = doc.sections_by_type(SectionType::Response);
    if !response_sections.is_empty() {
        phases.push(KernelPhase {
            kind: "response".to_string(),
            details: format!(
                "sections={}, total_messages={}",
                response_sections.len(),
                plan.summary.total_responses
            ),
            status: if call_status == "failed" {
                "failed".to_string()
            } else {
                "passed".to_string()
            },
        });
    }

    if doc.first_section(SectionType::Error).is_some() {
        phases.push(KernelPhase {
            kind: "error".to_string(),
            details: "expected error validation".to_string(),
            status: if call_status == "failed" {
                "failed".to_string()
            } else {
                "passed".to_string()
            },
        });
    }

    let assert_blocks = plan.summary.assertion_blocks;
    if assert_blocks > 0 {
        phases.push(KernelPhase {
            kind: "asserts".to_string(),
            details: format!("blocks={}", assert_blocks),
            status: if call_status == "failed" {
                "failed".to_string()
            } else {
                "passed".to_string()
            },
        });
    }

    let extract_blocks = plan.summary.variable_extractions;
    if extract_blocks > 0 {
        phases.push(KernelPhase {
            kind: "extract".to_string(),
            details: format!("blocks={}", extract_blocks),
            status: if call_status == "failed" {
                "failed".to_string()
            } else {
                "passed".to_string()
            },
        });
    }

    phases
}

pub fn build_kernel_calls(test_path: &str, result: &TestResult) -> Option<Vec<KernelCall>> {
    let doc = crate::parser::parse_gctf(std::path::Path::new(test_path)).ok()?;
    let chain: Vec<&GctfDocument> = doc.iter_chain().collect();
    if chain.is_empty() {
        return None;
    }

    let failed_index = if result.status == TestStatus::Fail {
        Some(chain.len().saturating_sub(1))
    } else {
        None
    };

    let mut calls = Vec::with_capacity(chain.len());
    for (idx, d) in chain.iter().enumerate() {
        let endpoint = d
            .get_endpoint()
            .unwrap_or_else(|| "<missing endpoint>".to_string());
        let parsed = d.parse_endpoint();
        let plan = ExecutionPlan::from_document(d);
        let workflow = Workflow::from_document_with_analysis(d);
        let request_count = plan.summary.total_requests;

        let has_error_expectation = d.first_section(SectionType::Error).is_some();
        let has_response_expectation = d.first_section(SectionType::Response).is_some();
        let expectation_kind = if has_error_expectation {
            "ERROR"
        } else if has_response_expectation {
            "RESPONSE"
        } else {
            "ASSERTS"
        }
        .to_string();

        let status = if failed_index == Some(idx) {
            "failed"
        } else {
            "passed"
        }
        .to_string();

        let (package, service, method) = match parsed {
            Some((pkg, svc, mtd)) => {
                let pkg = if pkg.is_empty() { None } else { Some(pkg) };
                (pkg, Some(svc), Some(mtd))
            }
            None => (None, None, None),
        };

        calls.push(KernelCall {
            call_index: idx + 1,
            doc_index: idx + 1,
            endpoint: endpoint.clone(),
            package,
            service,
            method,
            request_count,
            expectation_kind,
            rpc_mode: workflow.rpc_mode_name().to_string(),
            display_name: call_display_name(d, idx + 1, &endpoint),
            phases: call_phases(d, &status),
            status,
        });
    }

    Some(calls)
}

pub fn collect_grpc_labels(test_path: &str) -> KernelGrpcLabels {
    let doc = match crate::parser::parse_gctf(std::path::Path::new(test_path)) {
        Ok(d) => d,
        Err(_) => return KernelGrpcLabels::default(),
    };

    let mut endpoints = BTreeSet::new();
    let mut services = BTreeSet::new();
    let mut methods = BTreeSet::new();
    let mut packages = BTreeSet::new();

    for d in doc.iter_chain() {
        if let Some(endpoint) = d.get_endpoint() {
            endpoints.insert(endpoint);
        }
        if let Some((pkg, service, method)) = d.parse_endpoint() {
            if !pkg.is_empty() {
                packages.insert(pkg);
            }
            services.insert(service);
            methods.insert(method);
        }
    }

    KernelGrpcLabels {
        endpoints: endpoints.into_iter().collect(),
        packages: packages.into_iter().collect(),
        services: services.into_iter().collect(),
        methods: methods.into_iter().collect(),
    }
}

pub fn report_context() -> KernelReportContext {
    KernelReportContext {
        tool: "grpctestify".to_string(),
        version: env!("CARGO_PKG_VERSION").to_string(),
        generated_at: crate::polyfill::runtime::now_timestamp(),
    }
}

pub fn runtime_properties(test_path: &str) -> Vec<(String, String)> {
    let mut props = vec![(
        "grpctestify.version".to_string(),
        env!("CARGO_PKG_VERSION").to_string(),
    )];

    let doc = match crate::parser::parse_gctf(std::path::Path::new(test_path)) {
        Ok(d) => d,
        Err(_) => return props,
    };

    props.push((
        "documents.count".to_string(),
        doc.document_count().to_string(),
    ));

    if let Ok(runtime) = crate::execution::runner_helpers::resolve_effective_runtime_options(
        &doc,
        crate::execution::runner_helpers::CliRuntimeDefaults {
            timeout_seconds: 30,
            retry: 0,
            retry_delay_seconds: 1.0,
            no_retry: false,
        },
    ) {
        props.push((
            "runtime.timeout".to_string(),
            runtime.timeout_seconds.value.to_string(),
        ));
        props.push(("runtime.retry".to_string(), runtime.retry.value.to_string()));
        props.push((
            "runtime.retry_delay".to_string(),
            runtime.retry_delay_seconds.value.to_string(),
        ));
        props.push((
            "runtime.no_retry".to_string(),
            runtime.no_retry.value.to_string(),
        ));
        props.push(("runtime.compression".to_string(), runtime.compression.value));
    }

    props
}

pub fn build_kernel_test_case(test_path: &str, result: &TestResult) -> Option<KernelTestCase> {
    let doc = crate::parser::parse_gctf(std::path::Path::new(test_path)).ok()?;
    let calls = build_kernel_calls(test_path, result)?;
    let grpc_labels = collect_grpc_labels(test_path);
    let failures = extract_failures(test_path, result);

    let display_name = doc
        .sections
        .iter()
        .find_map(|s| s.get_attribute("name").map(|a| a.value.trim().to_string()))
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| {
            std::path::Path::new(test_path)
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_else(|| test_path.to_string())
        });

    Some(KernelTestCase {
        case_index: 0,
        test_path: test_path.to_string(),
        display_name,
        status: match result.status {
            TestStatus::Pass => "passed".to_string(),
            TestStatus::Fail => "failed".to_string(),
            TestStatus::Skip => "skipped".to_string(),
        },
        duration_ms: Some(result.duration_ms),
        calls,
        failures,
        grpc_labels,
    })
}

fn extract_failures(test_path: &str, result: &TestResult) -> Vec<KernelFailure> {
    let mut failures = Vec::new();
    if result.status != TestStatus::Fail {
        return failures;
    }

    let doc = match crate::parser::parse_gctf(std::path::Path::new(test_path)) {
        Ok(d) => d,
        Err(_) => return failures,
    };

    let chain: Vec<&GctfDocument> = doc.iter_chain().collect();
    let failed_doc_idx = chain.len().saturating_sub(1);

    failures.push(KernelFailure {
        call_index: failed_doc_idx + 1,
        phase: "execution".to_string(),
        category: categorize_failure(&result.error_message),
        message: result.error_message.clone().unwrap_or_default(),
        grpc_code: extract_grpc_code(&result.error_message),
    });

    failures
}

fn categorize_failure(error_msg: &Option<String>) -> String {
    let msg = match error_msg {
        Some(m) => m.to_lowercase(),
        None => return "unknown".to_string(),
    };

    if msg.contains("timeout") {
        "timeout".to_string()
    } else if msg.contains("connection") || msg.contains("network") {
        "connection_error".to_string()
    } else if msg.contains("assertion") || msg.contains("diff") || msg.contains("expected") {
        "assertion_failure".to_string()
    } else if msg.contains("parse") || msg.contains("syntax") {
        "parse_error".to_string()
    } else if msg.contains("validation") {
        "validation_error".to_string()
    } else {
        "execution_error".to_string()
    }
}

fn extract_grpc_code(error_msg: &Option<String>) -> Option<i32> {
    let msg = error_msg.as_ref()?;

    if let Some(pos) = msg.find("gRPC code ") {
        let rest = &msg[pos + 10..];
        if let Some(end) = rest.find([' ', ',', ')']) {
            let code_str = &rest[..end];
            return code_str.parse().ok();
        }
    }
    None
}