1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#![doc = include_str!("../README.md")]
use serde::{Deserialize, Serialize};
use tarantool::test::test_cases;
use tester::{TestDescAndFn, TestFn, TestOpts};

use anyhow::bail;
pub use tarantool::proc as test_entrypoint;
pub use tarantool::test;

pub const DEFAULT_TEST_ENTRYPOINT_NAME: &str = "default_test_entrypoint";

/// Implementors provide custom test suite logic.
pub trait TestSuite {
    fn before_all() {}
    fn after_all() {}

    fn before_each() {}
    fn after_each() {}
}

// () is the simplest test suite - does nothing in `before_all`, `after_all`, etc.
impl TestSuite for () {}

#[derive(Clone, Deserialize, Serialize, Debug, Default)]
pub struct TestsConfig {
    #[serde(default)]
    pub filter: Option<String>,
}

/// Generates entrypoint function for the test suite.
///
/// Generated entrypoints are functions that could be used from tarantool runtime - in other words, stored procedures.
#[macro_export]
macro_rules! bind_test_suite {
    () => {
        bind_test_suite!(@default_entrypoint, ());
    };
    ($suite:ty) => {
        bind_test_suite!(@default_entrypoint, $suite);
    };
    ($entrypoint:ident) => {
        bind_test_suite($entrypoint, ());
    };
    // helper branch that inserts `default_test_entrypoint` as an entrypoint
    (@default_entrypoint, $suite:ty) => {
        bind_test_suite!(default_test_entrypoint, $suite);
    };
    ($entrypoint:ident, $suite:ty) => {
        #[$crate::test_entrypoint]
        fn $entrypoint(input: String) -> Result<(), Box<dyn std::error::Error>> {
            $crate::handle_entrypoint::<$suite>(input)?;
            Ok(())
        }
    };
}

/// Collects tests from the tarantool tester with given suite setup.
pub fn collect_tests<S: TestSuite>(cfg: TestsConfig) -> Vec<TestDescAndFn> {
    let tests = test_cases();

    tests
        .iter()
        .filter(|case| {
            cfg.filter
                .as_ref()
                .map(|must_contains| case.name().contains(must_contains))
                .unwrap_or(true)
        })
        .map(|case| {
            let test_fn = move || {
                S::before_each();
                case.run();
                S::after_each();
            };
            let mut tester = case.to_tester();
            tester.testfn = TestFn::DynTestFn(Box::new(test_fn));
            tester
        })
        .collect()
}

/// Executes tests with given config and user-provided suite.
pub fn execute_tests<S: TestSuite>(mut cfg: TestsConfig) -> anyhow::Result<()> {
    if cfg.filter == Some("".to_string()) {
        cfg.filter = None
    }

    let opts = &TestOpts {
        list: false,
        filter: None,
        filter_exact: false,
        force_run_in_process: false,
        exclude_should_panic: false,
        run_ignored: tester::RunIgnored::No,
        run_tests: true,
        bench_benchmarks: false,
        logfile: None,
        nocapture: false,
        color: tester::ColorConfig::AutoColor,
        format: tester::OutputFormat::Pretty,
        test_threads: Some(1),
        skip: vec![],
        time_options: None,
        options: tester::Options::new(),
    };

    S::before_all();
    let result = tester::run_tests_console(opts, collect_tests::<S>(cfg));
    S::after_all();

    match result {
        Ok(true) => Ok(()),
        failure => {
            bail!("failed to successfully pass the tests due to failure: {failure:?}");
        }
    }
}

/// Process entrypoint with given input and specific `TestSuite`(given as generic argument).
///
/// Normally you wouldn't like to call this function yourself - it exists for `bind_test_suite` macro expansion.
pub fn handle_entrypoint<S: TestSuite>(input: String) -> anyhow::Result<()> {
    let input: TestsConfig = serde_json::from_str(&input)?;
    execute_tests::<S>(input)?;
    Ok(())
}