siderite_testkit/
database.rs1use siderite_migrations::{
4 MigrationError, MigrationGraph, Migrator, ProjectState, diff, load_dir, schema_editor,
5};
6use siderite_orm::signals::Signals;
7use siderite_orm::{BackendError, Db, ModelMeta, OrmError};
8use std::convert::Infallible;
9use std::future::Future;
10use std::path::Path;
11use thiserror::Error;
12
13#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum TestDatabaseError {
17 #[error(transparent)]
19 Backend(#[from] BackendError),
20 #[error(transparent)]
22 Orm(#[from] OrmError),
23 #[error(transparent)]
25 Migration(#[from] MigrationError),
26}
27
28#[derive(Debug, Clone)]
43pub struct TestDatabase {
44 db: Db,
45}
46
47enum Rollback<T> {
50 Done(T),
51 Failed(OrmError),
52}
53
54impl<T> From<OrmError> for Rollback<T> {
55 fn from(err: OrmError) -> Self {
56 Self::Failed(err)
57 }
58}
59
60impl TestDatabase {
61 pub fn from_db(db: Db) -> Self {
63 Self { db }
64 }
65
66 #[cfg(feature = "sqlite")]
71 pub async fn sqlite_memory() -> Result<Self, TestDatabaseError> {
72 let backend = siderite_backends::sqlite::SqliteBackend::connect("sqlite::memory:").await?;
73 Ok(Self::from_db(Db::new(backend)))
74 }
75
76 pub fn db(&self) -> &Db {
78 &self.db
79 }
80
81 pub fn into_db(self) -> Db {
83 self.db
84 }
85
86 #[must_use]
90 pub fn with_signals(self, signals: Signals) -> Self {
91 Self {
92 db: self.db.with_signals(signals),
93 }
94 }
95
96 pub async fn with_models(self, models: &[&ModelMeta]) -> Result<Self, TestDatabaseError> {
104 let target = ProjectState::from_metas(models);
105 let operations = diff(&ProjectState::new(), &target)?;
106 let kind = self.db.capabilities().kind;
107 for sql in schema_editor::statements(kind, &ProjectState::new(), &operations)? {
108 self.db.execute_script(&sql).await?;
109 }
110 Ok(self)
111 }
112
113 pub async fn with_migrations(self, dir: impl AsRef<Path>) -> Result<Self, TestDatabaseError> {
119 let graph = MigrationGraph::build(load_dir(dir.as_ref())?)?;
120 Migrator::new(&self.db, &graph).migrate(None, false).await?;
121 Ok(self)
122 }
123
124 pub async fn isolated<F, Fut, T>(&self, f: F) -> Result<T, TestDatabaseError>
132 where
133 F: FnOnce(Db) -> Fut,
134 Fut: Future<Output = T>,
135 {
136 let outcome = self
137 .db
138 .transaction(|tx| async move { Err::<Infallible, _>(Rollback::Done(f(tx).await)) })
139 .await;
140 match outcome {
141 Err(Rollback::Done(value)) => Ok(value),
142 Err(Rollback::Failed(err)) => Err(err.into()),
143 Ok(never) => match never {},
144 }
145 }
146}
147
148#[cfg(all(test, feature = "sqlite"))]
149mod tests {
150 use super::*;
151 use siderite_migrations::{FieldState, ModelState, SqlType};
152 use siderite_migrations::{Migration, Operation, write_migration};
153 use std::time::{SystemTime, UNIX_EPOCH};
154
155 fn widget_model() -> ModelState {
156 let mut id = FieldState::new("id", "id", SqlType::BigInt);
157 id.primary_key = true;
158 id.auto = true;
159 ModelState {
160 name: "Widget".into(),
161 table: "widgets".into(),
162 fields: vec![id, FieldState::new("label", "label", SqlType::Text)],
163 indexes: Vec::new(),
164 constraints: Vec::new(),
165 }
166 }
167
168 #[tokio::test]
169 async fn with_migrations_applies_files_from_a_directory() {
170 let nanos = SystemTime::now()
171 .duration_since(UNIX_EPOCH)
172 .map_or(0, |d| d.as_nanos());
173 let dir =
174 std::env::temp_dir().join(format!("siderite-testkit-{}-{nanos}", std::process::id()));
175 let migration = Migration::new(
176 "0001_initial",
177 Vec::new(),
178 vec![Operation::CreateModel {
179 model: widget_model(),
180 }],
181 true,
182 Vec::new(),
183 )
184 .unwrap();
185 write_migration(&dir, &migration).unwrap();
186
187 let test = TestDatabase::sqlite_memory()
188 .await
189 .unwrap()
190 .with_migrations(&dir)
191 .await
192 .unwrap();
193 std::fs::remove_dir_all(&dir).unwrap();
194
195 test.db()
196 .raw_execute("INSERT INTO widgets (label) VALUES ('a')", vec![])
197 .await
198 .unwrap();
199 let rows = test
200 .db()
201 .raw_sql("SELECT id FROM widgets", vec![])
202 .await
203 .unwrap();
204 assert_eq!(rows.rows.len(), 1);
205 }
206
207 #[tokio::test]
208 async fn with_migrations_rejects_an_unreadable_directory_entry() {
209 let nanos = SystemTime::now()
210 .duration_since(UNIX_EPOCH)
211 .map_or(0, |d| d.as_nanos());
212 let dir = std::env::temp_dir().join(format!(
213 "siderite-testkit-bad-{}-{nanos}",
214 std::process::id()
215 ));
216 std::fs::create_dir_all(&dir).unwrap();
217 std::fs::write(dir.join("0001_bad.json"), "not json").unwrap();
218 let result = TestDatabase::sqlite_memory()
219 .await
220 .unwrap()
221 .with_migrations(&dir)
222 .await;
223 std::fs::remove_dir_all(&dir).unwrap();
224 assert!(matches!(result, Err(TestDatabaseError::Migration(_))));
225 }
226
227 #[tokio::test]
228 async fn memory_database_is_shared_between_clones() {
229 let test = TestDatabase::sqlite_memory().await.unwrap();
230 test.db()
231 .execute_script("CREATE TABLE t (id INTEGER PRIMARY KEY)")
232 .await
233 .unwrap();
234 let clone = test.clone().into_db();
235 clone
236 .raw_execute("INSERT INTO t (id) VALUES (1)", vec![])
237 .await
238 .unwrap();
239 let rows = test.db().raw_sql("SELECT id FROM t", vec![]).await.unwrap();
240 assert_eq!(rows.rows.len(), 1);
241 }
242
243 #[tokio::test]
244 async fn isolated_rolls_back_and_returns_value() {
245 let test = TestDatabase::sqlite_memory().await.unwrap();
246 test.db()
247 .execute_script("CREATE TABLE t (id INTEGER PRIMARY KEY)")
248 .await
249 .unwrap();
250 let inside = test
251 .isolated(|db| async move {
252 db.raw_execute("INSERT INTO t (id) VALUES (1)", vec![])
253 .await
254 .unwrap();
255 assert!(db.in_transaction());
256 db.raw_sql("SELECT id FROM t", vec![])
257 .await
258 .unwrap()
259 .rows
260 .len()
261 })
262 .await
263 .unwrap();
264 assert_eq!(inside, 1);
265 let after = test.db().raw_sql("SELECT id FROM t", vec![]).await.unwrap();
266 assert!(after.rows.is_empty());
267 test.isolated(|_| async {}).await.unwrap();
269 }
270}