rtest 0.2.2

integration test building framework
Documentation
use crate::{error::TestErrorDetails, runner::test_repo::TestRepo};

use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, collections::HashSet, time::Duration};

/// Persistable Info of testcases
#[derive(Serialize, Deserialize, Debug)]
pub struct ExecutionResult {
    pub(crate) status:         ExecutionStatus,
    #[serde(flatten)]
    pub(crate) execution_time: Duration,
    #[cfg(feature = "capture_tracing")]
    pub(crate) tracing_logs:   String,
}
/// for all executions of a TestCase
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum ExecutionStatus {
    Success,
    Error(TestErrorDetails),
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ExecutionRecord {
    pub(crate) test_id: u64,
    #[serde(flatten)]
    pub(crate) result:  ExecutionResult,
}

#[derive(Debug, Default)]
pub struct ExecutionResultMap {
    records:   Vec<ExecutionRecord>,
    executed:  HashSet<u64>, // optimisation
    successes: usize,
    errors:    usize,
}

impl ExecutionResultMap {
    pub fn records(&self) -> &Vec<ExecutionRecord> { &self.records }

    pub fn into_records(self) -> Vec<ExecutionRecord> { self.records }

    pub fn with_records(records: Vec<ExecutionRecord>) -> Self {
        let mut map = ExecutionResultMap::default();
        for record in records {
            map.record(record.test_id, record.result);
        }
        map
    }

    pub fn results_of(&self, test_id: u64) -> Vec<&ExecutionResult> {
        self.records
            .iter()
            .filter(|r| r.test_id == test_id)
            .map(|r| &r.result)
            .collect()
    }

    pub fn did_execute(&self, test_id: u64) -> bool { self.executed.contains(&test_id) }

    /// either because its executed or optional
    pub fn did_complete(&self, repo: &TestRepo) -> bool {
        !repo
            .cases()
            .iter()
            .filter(|(_, case)| !case.info.test_arguments.optional)
            .map(|(key, _)| key)
            .any(|key| !self.did_execute(*key))
    }

    /// return exitcode with highest prio, if there is multiple errors with the
    /// same prio, return highest exitcode
    pub fn exitcode(&self) -> u8 {
        let mut exitcode_prio = None;
        for r in &self.records {
            if let ExecutionStatus::Error(e) = &r.result.status {
                match exitcode_prio {
                    None => exitcode_prio = Some(e.exitcode_prio),
                    Some(sp) => {
                        if (e.exitcode_prio.1 > sp.1) || (e.exitcode_prio.1 == sp.1 && e.exitcode_prio.0 > sp.0) {
                            exitcode_prio = Some(e.exitcode_prio);
                        }
                    },
                }
            }
        }

        exitcode_prio.map(|sp| sp.0).unwrap_or(0)
    }

    pub fn record(&mut self, test_id: u64, result: ExecutionResult) {
        match result.status {
            ExecutionStatus::Success => self.successes += 1,
            ExecutionStatus::Error(_) => self.errors += 1,
        }
        self.records.push(ExecutionRecord { test_id, result });
        self.executed.insert(test_id);
    }

    pub fn successes(&self) -> usize { self.successes }

    pub fn errors(&self) -> usize { self.errors }
}

impl ExecutionResult {
    pub fn avg_runtime(results: &[&ExecutionResult]) -> Option<Duration> {
        if results.is_empty() {
            None
        } else {
            Some(results.iter().map(|r| r.execution_time).sum::<Duration>() / results.len() as u32)
        }
    }
}

impl ExecutionStatus {
    pub fn successful(statuses: &[&ExecutionStatus]) -> bool {
        if statuses.is_empty() {
            return false;
        }
        for status in statuses {
            if let ExecutionStatus::Error(e) = &status {
                if e.fails {
                    return false;
                }
            }
        }
        true
    }
}

impl Ord for ExecutionStatus {
    fn cmp(&self, other: &Self) -> Ordering {
        match (self, other) {
            (ExecutionStatus::Success, ExecutionStatus::Success) => Ordering::Equal,
            (ExecutionStatus::Error(_), ExecutionStatus::Success) => Ordering::Greater,
            (ExecutionStatus::Success, ExecutionStatus::Error(_)) => Ordering::Less,
            (ExecutionStatus::Error(_), ExecutionStatus::Error(_)) => Ordering::Equal, // TODO pri
        }
    }
}

impl PartialOrd for ExecutionStatus {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}

#[cfg(test)]
mod tests {
    use crate::{runner::panic::PanicError, TestError};

    use super::*;

    #[test]
    fn successfuls() {
        let detail = TestErrorDetails::new(Box::new(PanicError { details: None }));
        let detail1 = TestErrorDetails::new(Box::new(PanicError { details: None }));
        let detail2 = TestErrorDetails::new(Box::new(PanicError { details: None }));
        assert!(!ExecutionStatus::successful(&[
            &ExecutionStatus::Success,
            &ExecutionStatus::Error(detail)
        ]));
        assert!(!ExecutionStatus::successful(&[
            &ExecutionStatus::Error(detail1),
            &ExecutionStatus::Success,
            &ExecutionStatus::Error(detail2)
        ]));
        assert!(ExecutionStatus::successful(&[
            &ExecutionStatus::Success,
            &ExecutionStatus::Success
        ]));
        assert!(ExecutionStatus::successful(&[&ExecutionStatus::Success]));
        assert!(!ExecutionStatus::successful(&[]));
    }

    #[test]
    fn sort_test_case_status() {
        let detail = TestErrorDetails::new(Box::new(PanicError { details: None }));
        let detail1 = TestErrorDetails::new(Box::new(PanicError { details: None }));
        let mut statuses = vec![
            Some(ExecutionStatus::Success),
            None,
            Some(ExecutionStatus::Error(detail)),
        ];
        statuses.sort();
        assert_eq!(statuses, vec![
            None,
            Some(ExecutionStatus::Success),
            Some(ExecutionStatus::Error(detail1))
        ]);
    }

    fn fake_result(status: ExecutionStatus) -> ExecutionResult {
        ExecutionResult {
            execution_time: Duration::from_secs(1),
            tracing_logs: "".to_string(),
            status,
        }
    }

    #[test]
    fn exitcode_success() {
        let mut map = ExecutionResultMap::default();
        map.record(0, fake_result(ExecutionStatus::Success));
        map.record(1, fake_result(ExecutionStatus::Success));
        map.record(2, fake_result(ExecutionStatus::Success));
        assert_eq!(map.exitcode(), 0);
    }

    #[test]
    fn exitcode_fail() {
        let mut map = ExecutionResultMap::default();
        let detail = TestErrorDetails::new(Box::new(PanicError { details: None }));
        map.record(0, fake_result(ExecutionStatus::Success));
        map.record(1, fake_result(ExecutionStatus::Error(detail)));
        map.record(2, fake_result(ExecutionStatus::Success));
        assert_eq!(map.exitcode(), 1);
    }

    #[derive(Debug)]
    struct E1 {}
    #[derive(Debug)]
    struct E2 {}

    impl std::fmt::Display for E1 {
        fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) }
    }

    impl std::fmt::Display for E2 {
        fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) }
    }

    impl std::error::Error for E1 {}
    impl TestError for E1 {
        fn exitcode(&self) -> (u8, u64) { (3, 100) }
    }
    impl std::error::Error for E2 {}
    impl TestError for E2 {
        fn exitcode(&self) -> (u8, u64) { (4, 100) }
    }

    #[test]
    fn exitcode_fail_higher() {
        let mut map = ExecutionResultMap::default();
        let detail1 = TestErrorDetails::new(Box::new(E1 {}));
        let detail2 = TestErrorDetails::new(Box::new(E2 {}));
        map.record(0, fake_result(ExecutionStatus::Success));
        map.record(1, fake_result(ExecutionStatus::Error(detail1)));
        map.record(2, fake_result(ExecutionStatus::Error(detail2)));
        map.record(3, fake_result(ExecutionStatus::Success));
        assert_eq!(map.exitcode(), 4);
    }
}