deflake-rs 0.1.0

cargo-deflake is a command that detects flaky tests based on what tests fail and what code has changed
use clap::{Parser, ValueEnum};
use glob::glob;
use serde::Deserialize;
use std::{collections::HashMap, fs::File, io::BufReader, time::Duration};
use test_interface::{TestId, TestResult, TestResultOutput, TestResults};

mod ast;
mod cli;
mod git;
mod test_interface;

#[derive(Parser, ValueEnum, Default, Debug, Clone)]
pub enum ResultType {
    #[default]
    Deflaker,
    Nodeflake,
    Cargo,
}

impl ToString for ResultType {
    fn to_string(&self) -> String {
        match self {
            ResultType::Deflaker => "deflaker",
            ResultType::Nodeflake => "nodeflake",
            ResultType::Cargo => "vanilla",
        }
        .to_string()
    }
}

#[derive(Deserialize, Debug)]
struct VanillaResult {
    success: bool,
    time: f32,
}

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Cli {
    #[arg(
        short = 't',
        long = "type",
        help = "type of results to process",
        value_enum,
        default_value_t = ResultType::Deflaker,
    )]
    pub results_type: ResultType,

    #[arg(short = 'p', long = "project", help = "the project to analyse")]
    pub project: String,
}

fn main() {
    let args = Cli::parse();
    println!("Analysing");

    // take all results (vanilla or deflaker), and return info about it
    // Results expected to be in format PROJECT-(type)-(time)

    let glob_pattern = &format!(
        "./uploads/{}-{}-*",
        args.project,
        args.results_type.to_string()
    );
    dbg!(glob_pattern);
    match args.results_type {
        ResultType::Cargo => {
            let mut times = vec![];
            let mut success = 0;
            let mut fail = 0;
            for item in glob(glob_pattern.as_str()).expect("Failed to get files") {
                if let Ok(path) = item {
                    let file = File::open(path).unwrap();
                    let reader = BufReader::new(file);
                    let result: VanillaResult =
                        serde_json::from_reader(reader).expect("Invalid results file");
                    times.push(result.time);

                    let counter = match result.success {
                        true => &mut success,
                        false => &mut fail,
                    };
                    *counter += 1;
                }
            }
            let sum: f32 = times.iter().sum();
            let average = sum / times.len() as f32;

            println!("Average exec time: {}", average);
            println!(
                "{} runs. {} success, {} failed.",
                success + fail,
                success,
                fail
            );
        }
        ResultType::Deflaker | ResultType::Nodeflake => {
            let mut test_results: HashMap<TestId, TestResult> = HashMap::new();
            let mut test_timings = vec![];
            let mut full_timings = vec![]; // includes deflaker
            let mut num_tests = None;
            let mut flaky = HashMap::new();
            let mut deflaker_found = vec![];
            for item in glob(glob_pattern.as_str()).expect("Failed to get files") {
                if let Ok(path) = item {
                    let file = File::open(path).unwrap();
                    let reader = BufReader::new(file);
                    let results: TestResultOutput =
                        serde_json::from_reader(reader).expect("Invalid results file");

                    if num_tests.is_none() {
                        num_tests = Some(results.meta.tests);
                    } else {
                        if num_tests.unwrap() != results.meta.tests {
                            println!("WARN: number of tests does not match between runs");
                        }
                    }

                    test_timings.push(results.meta.execution_time);
                    if let Some(full_time) = results.meta.deflake_time {
                        full_timings.push(full_time);
                    }

                    results.tests.into_iter().for_each(|(id, res)| {
                        // Skip result if marked as Flaky already
                        if res == TestResult::Flaky {
                            deflaker_found.push(id);
                            return;
                        }

                        match test_results.get(&id) {
                            Some(current_res) => {
                                if *current_res != res {
                                    println!("Test {} has varying results", id);
                                    match flaky.get(&id) {
                                        Some(v) => flaky.insert(id, v + 1),
                                        None => flaky.insert(id, 1),
                                    };
                                }
                            }
                            None => {
                                test_results.insert(id, res);
                            }
                        };
                    })
                }
            }

            println!("Processing {} results", test_timings.len());
            let sum: u128 = test_timings.iter().sum();
            let average = sum / test_timings.len() as u128;

            // So for some reason Duration::as_millis returns u128 but Duration::from_millis
            // requires u64???
            println!(
                "Average test exec time: {}",
                Duration::from_millis(average as u64).as_secs_f64()
            );

            if full_timings.len() > 0 {
                let sum: u128 = full_timings.iter().sum();
                let average = sum / full_timings.len() as u128;
                println!(
                    "Average exec time incl deflaker: {}",
                    Duration::from_millis(average as u64).as_secs_f64()
                );
            } else {
                println!("Deflaker not enabled for runs");
            }

            println!("{} Flaky ", flaky.len());
            for flake in &flaky {
                println!(" - {} ({} times)", flake.0, flake.1);
            }

            println!("{} Deflaker found ", deflaker_found.len());
            for test in &deflaker_found {
                println!(" - {} ", test);
            }
        }
    }
}