fsci-conformance 0.3.0

Differential conformance testing harness for FrankenSciPy
#![forbid(unsafe_code)]

use fsci_conformance::e2e::{E2eOrchestratorConfig, run_orchestrator};
use std::path::PathBuf;
use std::process::ExitCode;

#[derive(Debug, Clone)]
struct CliArgs {
    artifact_root: PathBuf,
    fixture_root: PathBuf,
    packet_filter: Option<String>,
    scenario_filter: Option<String>,
    run_id: Option<String>,
}

#[derive(Debug, Clone)]
enum CliParseError {
    Help,
    Message(String),
}

fn parse_cli_args(args: &[String]) -> Result<CliArgs, CliParseError> {
    let defaults = E2eOrchestratorConfig::default();
    let mut artifact_root = defaults.artifact_root;
    let mut fixture_root = defaults.fixture_root;
    let mut packet_filter = None;
    let mut scenario_filter = None;
    let mut run_id = None;

    let mut index = 0;
    while index < args.len() {
        match args[index].as_str() {
            "-h" | "--help" => return Err(CliParseError::Help),
            "--artifact-root" => {
                let Some(value) = args.get(index + 1) else {
                    return Err(CliParseError::Message(String::from(
                        "missing value for --artifact-root",
                    )));
                };
                artifact_root = PathBuf::from(value);
                index += 2;
            }
            "--fixture-root" => {
                let Some(value) = args.get(index + 1) else {
                    return Err(CliParseError::Message(String::from(
                        "missing value for --fixture-root",
                    )));
                };
                fixture_root = PathBuf::from(value);
                index += 2;
            }
            "--packet" | "--packet-filter" => {
                let Some(value) = args.get(index + 1) else {
                    return Err(CliParseError::Message(String::from(
                        "missing value for --packet",
                    )));
                };
                packet_filter = Some(value.clone());
                index += 2;
            }
            "--scenario" | "--scenario-filter" => {
                let Some(value) = args.get(index + 1) else {
                    return Err(CliParseError::Message(String::from(
                        "missing value for --scenario",
                    )));
                };
                scenario_filter = Some(value.clone());
                index += 2;
            }
            "--run-id" => {
                let Some(value) = args.get(index + 1) else {
                    return Err(CliParseError::Message(String::from(
                        "missing value for --run-id",
                    )));
                };
                run_id = Some(value.clone());
                index += 2;
            }
            unknown => {
                return Err(CliParseError::Message(format!(
                    "unrecognized argument `{unknown}`"
                )));
            }
        }
    }

    Ok(CliArgs {
        artifact_root,
        fixture_root,
        packet_filter,
        scenario_filter,
        run_id,
    })
}

fn print_usage(program: &str) {
    eprintln!(
        "Usage: {program} [--artifact-root <path>] [--fixture-root <path>] [--packet <id>] [--scenario <id>] [--run-id <id>]"
    );
    eprintln!("  --artifact-root <path>  root containing packet artifact directories");
    eprintln!("  --fixture-root <path>   root containing fixture json files");
    eprintln!("  --packet <id>           run only scenarios for one packet");
    eprintln!("  --scenario <id>         run only one scenario id");
    eprintln!("  --run-id <id>           explicit run id (default uses timestamp)");
}

fn main() -> ExitCode {
    let argv: Vec<String> = std::env::args().collect();
    let program = argv
        .first()
        .cloned()
        .unwrap_or_else(|| String::from("e2e_orchestrator"));

    let args = match parse_cli_args(&argv[1..]) {
        Ok(args) => args,
        Err(CliParseError::Help) => {
            print_usage(&program);
            return ExitCode::SUCCESS;
        }
        Err(CliParseError::Message(message)) => {
            eprintln!("{message}");
            print_usage(&program);
            return ExitCode::from(2);
        }
    };

    let config = E2eOrchestratorConfig {
        artifact_root: args.artifact_root,
        fixture_root: args.fixture_root,
        packet_filter: args.packet_filter,
        scenario_filter: args.scenario_filter,
        run_id: args.run_id,
    };

    match run_orchestrator(&config) {
        Ok(summary) => {
            eprintln!(
                "run_id={} total={} passed={} failed={}",
                summary.run_id,
                summary.total_scenarios,
                summary.passed_scenarios,
                summary.failed_scenarios
            );
            for scenario in &summary.scenario_results {
                let status = if scenario.passed { "PASS" } else { "FAIL" };
                eprintln!(
                    "{status} packet={} scenario={} bundle={}",
                    scenario.packet_id,
                    scenario.scenario_id,
                    scenario.run_bundle_dir.display()
                );
                if !scenario.passed {
                    eprintln!("replay_command={}", scenario.replay_command);
                }
            }
            if summary.failed_scenarios > 0 {
                ExitCode::from(1)
            } else {
                ExitCode::SUCCESS
            }
        }
        Err(error) => {
            eprintln!("orchestrator error: {error}");
            ExitCode::from(2)
        }
    }
}