grpctestify 1.5.3

gRPC testing utility written in Rust
Documentation
// Check command - validate GCTF files

use crate::cli::args::HasFormat;
use anyhow::Result;
use std::path::PathBuf;
use tracing::info;

use crate::cli::args::CheckArgs;
use crate::parser;
use crate::report::{CheckReport, CheckSummary, Diagnostic, DiagnosticSeverity};
use crate::semantics;
use crate::utils::FileUtils;

fn sort_and_dedup_files(files: &mut Vec<PathBuf>) {
    files.sort();
    files.dedup();
}

fn sort_diagnostics(diagnostics: &mut [Diagnostic]) {
    diagnostics.sort_by(|a, b| {
        (
            a.file.as_str(),
            a.range.start.line,
            a.code.as_str(),
            a.message.as_str(),
        )
            .cmp(&(
                b.file.as_str(),
                b.range.start.line,
                b.code.as_str(),
                b.message.as_str(),
            ))
    });
}

fn build_summary(
    diagnostics: &[Diagnostic],
    total_files: usize,
    files_with_errors: usize,
) -> CheckSummary {
    let total_errors = diagnostics
        .iter()
        .filter(|d| matches!(d.severity, DiagnosticSeverity::Error))
        .count();
    let total_warnings = diagnostics
        .iter()
        .filter(|d| matches!(d.severity, DiagnosticSeverity::Warning))
        .count();
    CheckSummary {
        total_files,
        files_with_errors,
        total_errors,
        total_warnings,
    }
}

fn print_text_diagnostics(diagnostics: &[Diagnostic]) {
    for d in diagnostics {
        println!(
            "{}:{}: [{}] {}",
            d.file, d.range.start.line, d.code, d.message
        );
        if let Some(hint) = &d.hint {
            println!("  hint: {}", hint);
        }
    }
}

pub async fn handle_check(args: &CheckArgs) -> Result<()> {
    let mut files = Vec::new();
    let mut diagnostics: Vec<Diagnostic> = Vec::new();
    let mut files_with_errors = 0;

    for path in &args.files {
        if path.is_dir() {
            files.extend(FileUtils::collect_test_files(path, &[]));
        } else if path.is_file() {
            files.push(path.clone());
        } else {
            diagnostics.push(Diagnostic::error(
                &path.to_string_lossy(),
                "FILE_NOT_FOUND",
                "Path not found",
                1,
            ));
            files_with_errors += 1;
        }
    }

    sort_and_dedup_files(&mut files);
    sort_diagnostics(&mut diagnostics);

    if files.is_empty() {
        if args.is_json() {
            let summary = build_summary(&diagnostics, files.len(), files_with_errors);
            let report = CheckReport {
                diagnostics,
                summary,
            };
            println!("{}", serde_json::to_string_pretty(&report)?);
        } else {
            print_text_diagnostics(&diagnostics);
        }

        if files_with_errors > 0 {
            std::process::exit(1);
        }
        return Ok(());
    }

    info!("Checking {} file(s)...", files.len());

    for file in &files {
        let file_str = file.to_string_lossy().to_string();
        let mut file_has_error = false;
        match parser::parse_gctf(file) {
            Ok(doc) => {
                // Check for deprecated HEADERS using AST section types
                for section in &doc.sections {
                    if doc.section_uses_deprecated_headers_alias(section) {
                        diagnostics.push(
                            Diagnostic::warning(
                                &file_str,
                                "DEPRECATED_SECTION",
                                "HEADERS section is deprecated, use REQUEST_HEADERS instead",
                                section.start_line + 1,
                            )
                            .with_hint("Replace --- HEADERS --- with --- REQUEST_HEADERS ---"),
                        );
                    }
                }

                if let Err(e) = parser::validate_document(&doc) {
                    diagnostics.push(Diagnostic::error(
                        &file_str,
                        "VALIDATION_ERROR",
                        &e.to_string(),
                        1,
                    ));
                    file_has_error = true;
                }

                let semantic_mismatches = semantics::collect_assertion_type_mismatches(&doc);
                for mismatch in semantic_mismatches {
                    diagnostics.push(
                        Diagnostic::error(
                            &file_str,
                            &mismatch.rule_id,
                            &mismatch.message,
                            mismatch.line,
                        )
                        .with_hint(&format!(
                            "Type contract violation in assertion: {}",
                            mismatch.expression
                        )),
                    );
                    file_has_error = true;
                }

                let unknown_plugins = semantics::collect_unknown_plugin_calls(&doc);
                for unknown in unknown_plugins {
                    diagnostics.push(
                        Diagnostic::error(
                            &file_str,
                            &unknown.rule_id,
                            &unknown.message,
                            unknown.line,
                        )
                        .with_hint(&format!("Assertion: {}", unknown.expression)),
                    );
                    file_has_error = true;
                }

                if !args.is_json() && !file_has_error {
                    println!("{} ... OK", file.display());
                }
            }
            Err(e) => {
                diagnostics.push(Diagnostic::error(
                    &file_str,
                    "PARSE_ERROR",
                    &e.to_string(),
                    1,
                ));
                file_has_error = true;
            }
        }

        if file_has_error {
            files_with_errors += 1;
        }
    }

    if !args.is_json() {
        sort_diagnostics(&mut diagnostics);
        print_text_diagnostics(&diagnostics);
    }

    if args.is_json() {
        sort_diagnostics(&mut diagnostics);
        let summary = build_summary(&diagnostics, files.len(), files_with_errors);
        let report = CheckReport {
            diagnostics,
            summary,
        };
        println!("{}", serde_json::to_string_pretty(&report)?);
    }

    if files_with_errors > 0 {
        std::process::exit(1);
    }
    Ok(())
}