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}