rtest 0.2.2

integration test building framework
Documentation
pub mod backtrack;
pub mod builder;
pub mod environment;

use super::ExecutionStrategy;
use crate::{
    context::ResourceId,
    runner::{result_map::ExecutionResultMap, test_repo::TestRepo, Runner},
    TestCase,
};

pub use builder::*;
pub use environment::*;

/// a process transforms inputs into outputs if it succeeds.
/// It needs tools to be able to run, tools are not consumed.
pub trait Process {
    type I: IntoIterator<Item = ResourceId>;
    fn tools(&self) -> Self::I;
    fn inputs(&self) -> Self::I;
    fn outputs(&self) -> Self::I;
    /// evalute the cost of this process
    fn cost(&self) -> u64;
    fn id(&self) -> ProcessId;
    fn name(&self) -> &str { "" }
}

pub type ProcessId = u64;

pub trait Resource {
    fn id(&self) -> ResourceId;
    fn name(&self) -> &str { "" }
}

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

struct TestResource {
    name: ResourceId,
}

impl Resource for TestResource {
    fn id(&self) -> ResourceId { self.name.clone() }

    fn name(&self) -> &str { &self.name }
}

struct TestProcess<'a>(&'a TestCase);

impl<'a> Process for TestProcess<'a> {
    type I = Vec<ResourceId>;

    fn tools(&self) -> Self::I { self.0.info.reference_resources.clone() }

    fn inputs(&self) -> Self::I { self.0.info.input_resources.clone() }

    fn outputs(&self) -> Self::I { self.0.info.output_resources.clone() }

    fn cost(&self) -> u64 { 0 }

    fn id(&self) -> ProcessId { self.0.info.test_id }

    fn name(&self) -> &str { &self.0.info.display_name }
}

impl ExecutionStrategy for DepdencyStrategy {
    fn run(&self, repo: &mut TestRepo, initial: &[ResourceId]) -> ExecutionResultMap {
        let mut result_map = ExecutionResultMap::default();

        // prepare env
        let mut env = EnvironmentBuilder::default();
        let mut resources = repo.cases().values().fold(Vec::new(), |mut data, c: &TestCase| {
            data.append(&mut c.info.reference_resources.clone());
            data.append(&mut c.info.input_resources.clone());
            data.append(&mut c.info.output_resources.clone());
            data
        });
        resources.sort();
        resources.dedup();
        let resources = resources.into_iter().map(|r| TestResource { name: r });
        for r in resources {
            env.add_resource(r);
        }

        let mut processes: Vec<_> = repo.cases().values().collect();
        processes.sort_by_key(|p| p.info.test_id);
        for c in processes {
            env.add_process(TestProcess(c))
        }
        for r in initial {
            env.add_initial(r.clone())
        }

        let env = env.build();
        let params = repo.runner_params();
        let mut path = Path::default();

        'outer: for rand in 0usize..1000 {
            let (_reason, mut nexts) = backtrack::next(&env, &path);
            if nexts.is_empty() {
                break;
            }
            let i = rand.rem_euclid(nexts.len());

            let step = nexts.remove(i);
            let tc_id = step.process;
            let tc = repo.cases().get(&tc_id).unwrap();

            if let Ok(res) = Runner::exec_testcase(tc, &params) {
                // println!("Run {}/{}: {:?}", &tc.info.display_name, nexts.len(), _reason);
                result_map.record(tc_id, res);
                path.push(step);

                if result_map.did_complete(repo) {
                    break 'outer;
                }
            }
        }
        result_map
    }
}

impl DepdencyStrategy {}

#[cfg(test)]
mod tests {
    use crate::{
        algos::ExecutionStrategy, call_handler, Context, HandlerParams, Resource, ResourceId, RunConfig, TestError,
        TestRepo,
    };
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum ExampleError {}

    impl TestError for ExampleError {}

    struct A {}
    struct B {}

    impl Resource for A {
        type Context = Context;

        fn from_context(context: &Self::Context) -> Option<Self> { context.extract::<A>() }

        fn into_context(context: &Self::Context, resource: A) { context.inject(resource) }

        fn get_resource_id() -> ResourceId { "A".to_string() }
    }

    impl Resource for B {
        type Context = Context;

        fn from_context(context: &Self::Context) -> Option<Self> { context.extract::<B>() }

        fn into_context(context: &Self::Context, resource: B) { context.inject(resource) }

        fn get_resource_id() -> ResourceId { "B".to_string() }
    }

    fn test_1() -> std::result::Result<A, ExampleError> { Ok(A {}) }
    fn test_2(_: A) -> std::result::Result<B, ExampleError> { Ok(B {}) }
    fn test_3(_: B) -> std::result::Result<(), ExampleError> { Ok(()) }

    #[test]
    fn dependency_with_2_resources() {
        let mut repo = TestRepo::default();
        let runconfig = RunConfig {
            context: Context::default(),
            ..Default::default()
        };
        let context = runconfig.context.clone();
        let handler_params = HandlerParams::from(&runconfig);
        repo.add(
            0,
            "test_1",
            Box::new(move || call_handler(context.clone(), &mut test_1, &handler_params)),
            Default::default(),
            vec![],
            vec![],
            vec!["A".to_string()],
        );
        let context = runconfig.context.clone();
        let handler_params = HandlerParams::from(&runconfig);
        repo.add(
            1,
            "test_2",
            Box::new(move || call_handler(context.clone(), &mut test_2, &handler_params)),
            Default::default(),
            vec![],
            vec!["A".to_string()],
            vec!["B".to_string()],
        );
        let context = runconfig.context.clone();
        let handler_params = HandlerParams::from(&runconfig);
        repo.add(
            2,
            "test_3",
            Box::new(move || call_handler(context.clone(), &mut test_3, &handler_params)),
            Default::default(),
            vec![],
            vec!["B".to_string()],
            vec![],
        );

        let strategy = super::DepdencyStrategy::default();
        let result = strategy.run(&mut repo, &[]);

        assert_eq!(result.successes(), 3);
        assert_eq!(result.errors(), 0);

        let records = result.into_records();
        assert_eq!(records.len(), 3);
        assert_eq!(records[0].test_id, 0);
        assert_eq!(records[1].test_id, 1);
        assert_eq!(records[2].test_id, 2);
    }
}