Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/integration/mod.rs
1use std::{
2 collections::HashSet,
3 env,
4 ffi::OsStr,
5 fs,
6 io::ErrorKind,
7 path::{Path, PathBuf},
8};
9 
10mod action_log;
11mod artifacts;
12pub mod capture_recorder;
13mod driver;
14pub mod overlay;
15mod step;
16pub mod video_recorder;
17pub use action_log::ActionLog;
18pub use artifacts::ARTIFACTS_DIR_ENV_VAR;
19pub use driver::{Builder, SetupFn, TestDriver, RERUN_EXIT_CODE, RUNTIME_TAG_FAILURE_REASON};
20pub use overlay::OverlayLog;
21pub use step::{
22 AssertionCallback, AssertionOutcome, AssertionWithDataCallback, IntegrationTestEvent,
23 PersistedDataMap, StepData, StepDataMap, TestStep,
24};
25pub use video_recorder::{save_captured_frame_as_png, VideoRecorder};
26 
27#[macro_export]
28macro_rules! async_assert {
29 ($left:expr) => {
30 match (&$left) {
31 (left_val) => {
32 if *left_val {
33 $crate::integration::AssertionOutcome::Success
34 } else {
35 let assertion_message = format!("assertion failed: {}", stringify!($left));
36 $crate::integration::AssertionOutcome::failure(assertion_message)
37 }
38 }
39 }
40 };
41 ($left:expr, $($arg:tt)+) => {
42 match (&$left) {
43 (left_val) => {
44 if *left_val {
45 $crate::integration::AssertionOutcome::Success
46 } else {
47 let assertion_message = format!("assertion failed: {}", format_args!($($arg)+));
48 $crate::integration::AssertionOutcome::failure(assertion_message)
49 }
50 }
51 }
52 };
53}
54 
55/// Asserts that the condition is true immediately,
56/// but allows for some on_finish behavior in the app before it panics.
57#[macro_export]
58macro_rules! integration_assert {
59 ($left:expr) => {
60 match (&$left) {
61 (left_val) => {
62 if !*left_val {
63 let assertion_message = format!("assertion failed: {}", stringify!($left));
64 return $crate::integration::AssertionOutcome::immediate_failure(assertion_message);
65 }
66 }
67 }
68 };
69 ($left:expr, $($arg:tt)+) => {
70 match (&$left) {
71 (left_val) => {
72 if !*left_val {
73 let assertion_message = format!("assertion failed: {}", format_args!($($arg)+));
74 return $crate::integration::AssertionOutcome::immediate_failure(assertion_message);
75 }
76 }
77 }
78 };
79}
80 
81#[macro_export]
82macro_rules! async_assert_eq {
83 ($left:expr, $right:expr) => {
84 match (&$left, &$right) {
85 (left_val, right_val) => {
86 if *left_val == *right_val {
87 $crate::integration::AssertionOutcome::Success
88 } else {
89 let assertion_message = format!(
90 "assertion failed: `(left = right)`
91 left: `{:?}`,
92 right: `{:?}`",
93 left_val, right_val
94 );
95 $crate::integration::AssertionOutcome::failure(assertion_message)
96 }
97 }
98 }
99 };
100 ($left:expr, $right:expr, $($arg:tt)+) => {
101 match (&$left, &$right) {
102 (left_val, right_val) => {
103 if *left_val == *right_val {
104 $crate::integration::AssertionOutcome::Success
105 } else {
106 let assertion_message = format!(
107 "assertion failed: `{}`
108 left: `{:?}`,
109 right: `{:?}`",
110 format_args!($($arg)+), left_val, right_val
111 );
112 $crate::integration::AssertionOutcome::failure(assertion_message)
113 }
114 }
115 }
116 };
117}
118 
119pub struct TestSetupUtils {
120 env_vars: HashSet<String>,
121 root_dir: RootDir,
122}
123 
124impl TestSetupUtils {
125 fn new(root_dir: RootDir) -> Self {
126 TestSetupUtils {
127 env_vars: HashSet::new(),
128 root_dir,
129 }
130 }
131 
132 /// Returns the $HOME dir for the test.
133 pub fn test_dir(&self) -> PathBuf {
134 self.root_dir.as_path().to_path_buf()
135 }
136 
137 pub fn set_env<K, V>(&mut self, key: K, value: Option<V>)
138 where
139 K: Into<String>,
140 V: AsRef<OsStr>,
141 {
142 let key = key.into();
143 match value {
144 Some(v) => {
145 println!(
146 "Setting env var {} to {} for test",
147 key,
148 v.as_ref().to_string_lossy()
149 );
150 env::set_var(&key, v);
151 self.env_vars.insert(key);
152 }
153 None => {
154 println!("Clearing env var {key}");
155 env::remove_var(key);
156 }
157 };
158 }
159 
160 pub fn cleanup_env(&mut self) {
161 for key in &self.env_vars {
162 println!("Clearing env var {key}");
163 env::remove_var(key);
164 }
165 self.env_vars = HashSet::new();
166 }
167 
168 // For each test, we create an empty directory in the temp filesystem. This will be the root
169 // for that test's specific resources.
170 fn create_temp_dir_for_test(&self) {
171 let test_dir = self.root_dir.as_path();
172 
173 // Remove anything we failed to remove from previous runs of the test.
174 match fs::remove_dir_all(test_dir) {
175 Ok(_) => (),
176 Err(err) => {
177 // Not found is fine because there's no old data to interfere with this test.
178 if err.kind() != ErrorKind::NotFound {
179 eprintln!("failure cleaning up test temp dir at {test_dir:?}");
180 if let Ok(rd) = test_dir.read_dir() {
181 eprintln!("contents of test temp dir:");
182 for entry in rd.flatten() {
183 eprintln!(" - {:?}", entry.file_name());
184 }
185 }
186 panic!("failed to remove previous run test data: {err}");
187 }
188 }
189 }
190 
191 let res = fs::create_dir_all(test_dir);
192 if let Err(err_code) = res {
193 if err_code.kind() != ErrorKind::AlreadyExists {
194 panic!("Failed to create directory {test_dir:?}");
195 }
196 }
197 }
198 
199 /// Configures the home directory path for the test.
200 fn set_home_dir_for_test(&mut self) {
201 if cfg!(unix) {
202 self.set_env("ORIGINAL_HOME", dirs::home_dir());
203 // Override the home directory path. This helps keep tests more
204 // hermetic by making them not depend on the contents of the user's
205 // home directory (which could be very different on a developer's
206 // machine vs. on cloud CI runners).
207 //
208 // We canonicalize the path to resolve symlinks (e.g. /var ->
209 // /private/var on macOS) so that the shell's resolved $PWD matches
210 // $HOME exactly, which is required for ~ substitution to work.
211 let canonical_test_dir = self
212 .test_dir()
213 .canonicalize()
214 .unwrap_or_else(|_| self.test_dir());
215 self.set_env("HOME", Some(canonical_test_dir));
216 }
217 }
218 
219 pub fn cleanup_dir(&mut self) {
220 if let Err(err) = fs::remove_dir_all(self.root_dir.as_path()) {
221 log::error!("Could not cleanup directory {err:?}");
222 }
223 }
224}
225 
226enum RootDir {
227 /// Uses the provided path as the test's root directory.
228 Path(PathBuf),
229 /// Uses the provided TempDir as the test's root directory.
230 TempDir(tempfile::TempDir),
231}
232 
233impl RootDir {
234 fn as_path(&self) -> &Path {
235 match self {
236 RootDir::Path(path) => path.as_path(),
237 RootDir::TempDir(tempdir) => tempdir.path(),
238 }
239 }
240}
241