rea_rs_test/
lib.rs

1//! # rea-rs-test
2//!
3//! Makes testing of REAPER extension plugins easy.
4//!
5//! This integration test suite was originally written by Benjamin Klum
6//! <benjamin.klum@helgoboss.org> for `reaper-rs`. But it was dependent on the
7//! `reaper-high` crate, which was not and would not be soon published. And,
8//! also, it was deeply integrated into the library.
9//!
10//! This version incapsulates as much as possible, leaving simple interface to
11//! making tests.
12//!
13//! For testing reaper extension, which itself is of type `cdylib`,
14//! you need transform the project folder to workspace. So, basically,
15//! project tree would look similar to this:
16//!
17//! ```bash
18//! workspace_directory
19//! ├── Cargo.toml
20//! ├── README.md
21//! ├—— my_lib
22//! ├   ├—— src
23//! │      └── lib.rs
24//! └── test
25//!     ├── Cargo.toml
26//!     ├── src
27//!     │   └── lib.rs
28//!     └── tests
29//!         └── integration_test.rs
30//! ```
31//!
32//! `test` crate will not be delivered to the end-user, but will be used for
33//! testing your library. Since there is a need for patching of reaper-low and
34//! reaper-medium, contents of `test/Cargo.toml`:
35//!
36//! ```toml
37//! [package]
38//! edition = "2021"
39//! name = "reaper-test-extension-plugin"
40//! publish = false
41//! version = "0.1.0"
42//!
43//! [dependencies]
44//! rea-rs = "0.1.1"
45//! rea-rs-macros = "0.1.0"
46//! rea-rs-test = "0.1.0"
47//! my_lib = {path = "../my_lib"}
48//!
49//! [lib]
50//! crate-type = ["cdylib"]
51//! name = "reaper_test_extension_plugin"
52//! ```
53//!
54//! contents of `test/tests/integration_test.rs`:
55//!
56//! ```no_run
57//! use rea_rs_test::{run_integration_test, ReaperVersion};
58//! #[test]
59//! fn test() {
60//!     run_integration_test(ReaperVersion::latest());
61//! }
62//! ```
63//!
64//! `test/src/lib.rs` is the file your integration tests are placed in.
65//!
66//! ```no_run
67//! use rea_rs_macros::reaper_extension_plugin;
68//! use rea_rs_test::*;
69//! use rea_rs::{Reaper, PluginContext};
70//! use std::error::Error;
71//! fn hello_world(reaper: &mut Reaper) -> TestStepResult {
72//!     reaper.show_console_msg("Hello world!");
73//!     Ok(())
74//! }
75//! #[reaper_extension_plugin]
76//! fn test_extension(context: PluginContext) -> Result<(), Box<dyn Error>> {
77//!     // setup test global environment
78//!     let test = ReaperTest::setup(context, "test_action");
79//!     // Push single test step.
80//!     test.push_test_step(TestStep::new("Hello World!", hello_world));
81//!     Ok(())
82//! }
83//! ```
84//!
85//! to run integration tests, go to the test folder and type:
86//! `cargo build --workspace; cargo test`
87//!
88//! ## Hint
89//!
90//! Use crates `log` and `env_logger` for printing to stdio. integration test
91//! turns env logger on by itself.
92
93use rea_rs::{PluginContext, Reaper, Timer};
94use rea_rs_low::register_plugin_destroy_hook;
95use std::{error::Error, fmt::Debug, panic, process};
96
97pub mod integration_test;
98pub use integration_test::*;
99
100static mut INSTANCE: Option<ReaperTest> = None;
101
102pub type TestStepResult = Result<(), Box<dyn Error>>;
103pub type TestCallback = dyn Fn(&'static mut Reaper) -> TestStepResult;
104
105pub struct TestStep {
106    name: String,
107    operation: Box<TestCallback>,
108}
109impl TestStep {
110    pub fn new(
111        name: impl Into<String>,
112        operation: impl Fn(&'static mut Reaper) -> Result<(), Box<dyn Error>>
113            + 'static,
114    ) -> Self {
115        Self {
116            name: name.into(),
117            operation: Box::new(operation),
118        }
119    }
120}
121impl Debug for TestStep {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        write!(f, "{}", self.name)
124    }
125}
126
127fn test(_flag: i32) -> Result<(), Box<dyn Error>> {
128    ReaperTest::get_mut().test();
129    Ok(())
130}
131
132struct IntegrationTimer {}
133impl Timer for IntegrationTimer {
134    fn run(&mut self) -> Result<(), Box<dyn Error>> {
135        test(0)?;
136        self.stop();
137        Ok(())
138    }
139
140    fn id_string(&self) -> String {
141        "integration_timer".to_string()
142    }
143}
144
145pub struct ReaperTest {
146    steps: Vec<TestStep>,
147    is_integration_test: bool,
148}
149impl ReaperTest {
150    fn make_available_globally(r_test: ReaperTest) {
151        static INIT_INSTANCE: std::sync::Once = std::sync::Once::new();
152        unsafe {
153            INIT_INSTANCE.call_once(|| {
154                INSTANCE = Some(r_test);
155                register_plugin_destroy_hook(|| INSTANCE = None);
156            });
157        }
158    }
159    pub fn setup(
160        context: PluginContext,
161        action_name: &'static str,
162    ) -> &'static mut Self {
163        let reaper = Reaper::init_global(context);
164        let instance = Self {
165            steps: Vec::new(),
166            is_integration_test: std::env::var("RUN_REAPER_INTEGRATION_TEST")
167                .is_ok(),
168        };
169        let integration = instance.is_integration_test;
170        reaper
171            .register_action(action_name, action_name, test, None)
172            .expect("Can not reigister test action");
173        Self::make_available_globally(instance);
174        if integration {
175            reaper.register_timer(Box::new(IntegrationTimer {}))
176        }
177        ReaperTest::get_mut()
178    }
179
180    /// Gives access to the instance which you made available globally before.
181    ///
182    /// # Panics
183    ///
184    /// This panics if [`make_available_globally()`] has not been called
185    /// before.
186    ///
187    /// [`make_available_globally()`]: fn.make_available_globally.html
188    pub fn get() -> &'static ReaperTest {
189        unsafe {
190            INSTANCE
191                .as_ref()
192                .expect("call `load(context)` before using `get()`")
193        }
194    }
195    pub fn get_mut() -> &'static mut ReaperTest {
196        unsafe {
197            INSTANCE
198                .as_mut()
199                .expect("call `load(context)` before using `get()`")
200        }
201    }
202
203    fn test(&mut self) {
204        println!("# Testing reaper-rs\n");
205        let result = panic::catch_unwind(|| -> TestStepResult {
206            // let r_test = ReaperTest::get_mut();
207            // let rpr = &mut r_test.reaper;
208            // for step in r_test.steps.iter() {
209            //     println!("Testing step: {}", step.name);
210            //     (step.operation)(rpr)?;
211            // }
212            ReaperTest::get()
213                .steps
214                .iter()
215                .map(|step| -> Result<(), Box<dyn Error>> {
216                    println!("Testing step: {}", step.name);
217                    (step.operation)(Reaper::get_mut())?;
218                    Ok(())
219                })
220                .count();
221            Ok(())
222        });
223        let final_result = match result.is_err() {
224            false => result.unwrap(),
225            true => Err("Reaper panicked!".into()),
226        };
227        match final_result {
228            Ok(_) => {
229                println!("From REAPER: reaper-rs integration test executed successfully");
230                if self.is_integration_test {
231                    process::exit(0)
232                }
233            }
234            Err(reason) => {
235                // We use a particular exit code to distinguish test
236                // failure from other possible
237                // exit paths.
238                match self.is_integration_test {
239                    true => {
240                        eprintln!("From REAPER: reaper-rs integration test failed: {}", reason);
241                        process::exit(172)
242                    }
243                    false => panic!(
244                        "From REAPER: reaper-rs integration test failed: {}",
245                        reason
246                    ),
247                }
248            }
249        }
250    }
251
252    pub fn push_test_step(&mut self, step: TestStep) {
253        self.steps.push(step);
254    }
255}