rtest 0.2.2

integration test building framework
Documentation
pub(crate) mod handler;
#[cfg(feature = "capture_tracing")]
pub(crate) mod logger;
pub(crate) mod panic;
pub(crate) mod result_map;
pub(crate) mod test_repo;

use crate::{
    algos::ExecutionStrategy,
    config::{Commands, RunArgs},
    context::ResourceId,
    error::TestErrorDetails,
    runner::{result_map::ExecutionResultMap, test_repo::TestRepoInfo},
    Context, DepdencyStrategy, Persister, Printer, RerunStrategy,
};
use backtrace::Backtrace;
use core::cell::RefCell;
use handler::HandlerError;
#[cfg(feature = "capture_tracing")]
use logger::default_subscriber;
use panic::PanicDetails;
use result_map::ExecutionResult;
#[cfg(feature = "capture_tracing")]
use std::sync::Arc;
use std::{collections::HashSet, time::Instant};
use test_repo::{RunnerParams, TestCase, TestRepo};

use self::{panic::PanicError, result_map::ExecutionStatus};

thread_local! {
    //TODO: wont work with async
    pub(crate) static CURRENT_EXECUTABLE_FUNCTION: RefCell<Option<(u64, String)>> = const { RefCell::new(None) };
    static BACKTRACE: RefCell<Vec<PanicDetails>> = const { RefCell::new(Vec::new()) };
}

#[derive(Default)]
pub struct Runner {}

/// executes a single or all tests, using an ExecutionStrategy
impl Runner {
    #[allow(private_bounds)]
    pub fn test_all<T: ExecutionStrategy>(
        strategy: T,
        mut repo: TestRepo,
        initial: Vec<ResourceId>,
    ) -> (ExecutionResultMap, TestRepoInfo) {
        // TODO: capture println! with std::io::set_output_capture once it's stable or
        // uses gag::BufferRedirect::stdout() once it works for multithreading
        let old_panic_hook = std::panic::take_hook();

        std::panic::set_hook(Box::new(|pi| {
            let executable_function = CURRENT_EXECUTABLE_FUNCTION.with(move |b| b.borrow_mut().take());
            let trace = Backtrace::new();
            if let Some(executable_function) = executable_function {
                const MESSAGE_START: &str = "message: Some(";
                const MESSAGE_END: &str = "), location: Location { file: \"";
                BACKTRACE.with(move |b| {
                    b.borrow_mut().push(PanicDetails {
                        backtrace: trace,
                        payload: None,
                        message: format!("{pi:?}")
                            .split(MESSAGE_START)
                            .nth(1)
                            .and_then(|m| m.split(MESSAGE_END).next())
                            .map(|s| s.to_string()),
                        location: pi.location().map(|l| (l.file().to_string(), l.line(), l.column())),
                        backtrace_function_name: executable_function.1,
                        test_id: executable_function.0,
                    })
                });
            } else {
                tracing::warn!(?trace, "Could not find test info while panic. cannot catch panic!");
            }
        }));

        let model = strategy.run(&mut repo, &initial);

        std::panic::set_hook(old_panic_hook);

        (model, repo.repo_info())
    }

    /// Ok when the test started
    /// Err when the test could not be started
    pub(crate) fn exec_testcase(
        tc: &TestCase,
        #[allow(unused_variables)] params: &RunnerParams,
    ) -> Result<ExecutionResult, ResourceId> {
        let test_func = &tc.test_func;
        let test_func_name = tc.display_name().to_string();
        let test_id = tc.test_id();
        CURRENT_EXECUTABLE_FUNCTION.with(move |b| {
            *b.borrow_mut() = Some((test_id, test_func_name));
        });
        let test_id = tc.test_id();

        #[cfg(feature = "capture_tracing")]
        let (subscriber, tracing_bytes) = default_subscriber(params.log_level);

        let start_time = Instant::now();
        #[cfg(feature = "capture_tracing")]
        let res = tracing::subscriber::with_default(subscriber, test_func);
        #[cfg(not(feature = "capture_tracing"))]
        let res = (test_func)();
        let execution_time = start_time.elapsed();

        #[cfg(feature = "capture_tracing")]
        let tracing_logs = {
            let data = Arc::try_unwrap(tracing_bytes.0)
                .ok()
                .and_then(|mutex| mutex.into_inner().ok().map(|wrtier| wrtier.into_inner().freeze()));
            if let Some(data) = data {
                String::from_utf8_lossy(&data).to_string()
            } else {
                tracing::warn!("tracing_logs seem to be locked");
                String::new()
            }
        };

        match res {
            Ok(Ok(())) => Ok(ExecutionResult {
                status: ExecutionStatus::Success,
                execution_time,
                #[cfg(feature = "capture_tracing")]
                tracing_logs,
            }),
            Ok(Err(e)) => Ok(ExecutionResult {
                status: ExecutionStatus::Error(TestErrorDetails::new(e)),
                execution_time,
                #[cfg(feature = "capture_tracing")]
                tracing_logs,
            }),
            Err(HandlerError::NotInContext(missing)) => Err(missing),
            Err(HandlerError::Panic(payload)) => {
                let panic_details = BACKTRACE.with(move |b| {
                    let mut bt = b.borrow_mut();
                    if let Some(i) = bt.iter().position(|b| b.test_id == test_id) {
                        let mut pd = bt.remove(i);
                        pd.payload = Some(payload);
                        Some(pd)
                    } else {
                        tracing::warn!("expected backtrace, but couln't fine one");
                        None
                    }
                });
                Ok(ExecutionResult {
                    status: ExecutionStatus::Error(TestErrorDetails::new(Box::new(PanicError::new(panic_details)))),
                    execution_time,
                    #[cfg(feature = "capture_tracing")]
                    tracing_logs,
                })
            },
        }
    }
}

/// To be called by run!()
pub fn main_run(repo: TestRepo, run_config: crate::config::RunConfig<Context>) -> std::process::ExitCode {
    use clap::Parser;
    let args = crate::config::Args::parse();

    let exit_code = match args.command {
        Commands::Run { args, output } => {
            let (results, info) = run_strategy(repo, run_config, DepdencyStrategy::default(), &args);
            let exit_code = results.exitcode();
            Printer::print_to_stdout(&results, &info);
            if let Some(location) = output {
                if let Err(e) = Persister::persist_to_file(results, info, args, &location) {
                    tracing::error!(?e, "could not store to file");
                }
            }
            exit_code
        },
        Commands::ReRun { file } => {
            let (results, _, args) = Persister::load_from_file(&file).unwrap();

            let (results, info) = run_strategy(repo, run_config, RerunStrategy::new(results), &args);
            let exit_code = results.exitcode();
            Printer::print_to_stdout(&results, &info);
            exit_code
        },
        Commands::Display { file } => {
            let (results, info, _) = Persister::load_from_file(&file).unwrap();
            Printer::print_to_stdout(&results, &info);
            results.exitcode()
        },
    };
    std::process::ExitCode::from(exit_code)
}

fn run_strategy<T: ExecutionStrategy>(
    mut repo: TestRepo,
    run_config: crate::config::RunConfig<Context>,
    strategy: T,
    args: &RunArgs,
) -> (ExecutionResultMap, TestRepoInfo) {
    let optional_tests: HashSet<_> = args.optional_tests.iter().collect();
    for c in repo.cases_mut().values_mut() {
        if optional_tests.contains(&c.info.display_name) {
            c.info.test_arguments.optional = true;
        }
    }

    repo.set_runner_params(RunnerParams {
        #[cfg(feature = "capture_tracing")]
        log_level:                                     args.test_tracing_log_level,
    });
    let initial_resources = run_config.context.get_resources();

    Runner::test_all(strategy, repo, initial_resources)
}