Skip to main content

bkmr/util/
testing.rs

1// src/util/testing.rs
2
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::sync::OnceLock;
7use tracing::{debug, info, instrument};
8use tracing_subscriber::{
9    filter::filter_fn,
10    fmt::{self, format::FmtSpan},
11    prelude::*,
12    EnvFilter,
13};
14
15use crate::infrastructure::repositories::sqlite::migration;
16use crate::infrastructure::repositories::sqlite::repository::SqliteBookmarkRepository;
17
18/// A struct that holds global test configuration and paths.
19/// Everything is initialized exactly once via OnceLock.
20#[derive(Debug)]
21pub struct TestEnv {
22    /// Path to your test database
23    pub db_path: PathBuf,
24    /// Paths to resource files
25    pub resources: Vec<&'static str>,
26}
27
28impl TestEnv {
29    /// Creates the default test configuration (paths, etc.).
30    fn new() -> Self {
31        Self {
32            db_path: PathBuf::from("../db/bkmr.db"),
33            resources: vec![
34                "tests/resources/schema_v1_migration_test.db",
35                "tests/resources/schema_v2_with_embeddings.db",
36                "tests/resources/schema_v2_no_embeddings.db",
37            ],
38        }
39    }
40}
41
42/// Global OnceLock holding the TestEnv data.
43static TEST_ENV: OnceLock<TestEnv> = OnceLock::new();
44
45/// Initializes the global test environment exactly once.
46/// - Sets up logging
47/// - Updates global AppState
48/// - Sets BKMR_DB_URL to match `TestEnv::db_path`
49/// Returns a reference to the fully-initialized TestEnv.
50pub fn init_test_env() -> &'static TestEnv {
51    // Initialize test environment config, storing it in TEST_ENV exactly once.
52    let env_data = TEST_ENV.get_or_init(|| {
53        let data = TestEnv::new();
54        setup_test_logging(); // set up logger only once
55
56        // Register sqlite-vec extension before any SQLite connections open.
57        crate::infrastructure::repositories::sqlite::register_sqlite_vec();
58
59        info!("Test environment initialized with DummyEmbedding");
60        data
61    });
62    env_data
63}
64
65/// Logging setup only runs once; subsequent calls do nothing if `tracing` is already set.
66fn setup_test_logging() {
67    debug!("Attempting logger init from testing.rs");
68    if tracing::dispatcher::has_been_set() {
69        debug!("Tracing subscriber already set");
70        return;
71    }
72
73    if env::var("RUST_LOG").is_err() {
74        env::set_var("RUST_LOG", "trace");
75    }
76
77    // Silence spammy modules
78    env::set_var("SKIM_LOG", "info");
79    env::set_var("TUIKIT_LOG", "info");
80
81    let noisy_modules = [
82        "skim",
83        "html5ever",
84        "reqwest",
85        "mio",
86        "want",
87        "hyper_util",
88    ];
89    let module_filter = filter_fn(move |metadata| {
90        !noisy_modules
91            .iter()
92            .any(|name| metadata.target().starts_with(name))
93    });
94
95    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug"));
96
97    let subscriber = tracing_subscriber::registry().with(
98        fmt::layer()
99            .with_writer(std::io::stderr)
100            .with_target(true)
101            .with_thread_names(false)
102            .with_span_events(FmtSpan::CLOSE)
103            .with_filter(module_filter)
104            .with_filter(env_filter),
105    );
106
107    subscriber.try_init().unwrap_or_else(|e| {
108        eprintln!("Error: Failed to set up logging: {}", e);
109    });
110}
111
112#[derive(Debug, Clone)]
113pub struct EnvGuard {
114    db_url: Option<String>,
115    fzf_opts: Option<String>,
116}
117
118impl Default for EnvGuard {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl EnvGuard {
125    pub fn new() -> Self {
126        Self {
127            db_url: env::var("BKMR_DB_URL").ok(),
128            fzf_opts: env::var("BKMR_FZF_OPTS").ok(),
129        }
130    }
131}
132
133impl Drop for EnvGuard {
134    #[instrument(level = "trace")]
135    fn drop(&mut self) {
136        env::remove_var("BKMR_DB_URL");
137        env::remove_var("BKMR_FZF_OPTS");
138        if let Some(val) = &self.db_url {
139            env::set_var("BKMR_DB_URL", val);
140        }
141        if let Some(val) = &self.fzf_opts {
142            env::set_var("BKMR_FZF_OPTS", val);
143        }
144    }
145}
146
147/// Creates a new repository with an initialized DB for testing.
148pub fn setup_test_db() -> SqliteBookmarkRepository {
149    let env_data = init_test_env();
150    let repository =
151        SqliteBookmarkRepository::from_url(env_data.db_path.to_string_lossy().as_ref())
152            .expect("Failed to create SqliteBookmarkRepository");
153    let mut conn = repository
154        .get_connection()
155        .expect("Failed to get connection from SqliteBookmarkRepository");
156    migration::init_db(&mut conn).expect("Failed to initialize DB schema");
157    repository
158}
159
160/// Creates a temporary directory and copies test resources into `../db`.
161pub fn setup_temp_dir() -> PathBuf {
162    use fs_extra::dir::CopyOptions;
163    use tempfile::tempdir;
164
165    let env_data = init_test_env(); // ensure global is initialized
166    let tempdir = tempdir().expect("Failed to create temp dir");
167    let options = CopyOptions::new().overwrite(true);
168
169    fs_extra::copy_items(&env_data.resources, "../db", &options)
170        .expect("Failed to copy test resources into ../db");
171
172    tempdir.keep()
173}
174
175/// Removes the temp directory if NO_CLEANUP is not set; otherwise leaves artifacts.
176pub fn teardown_temp_dir(temp_dir: &Path) {
177    if env::var("NO_CLEANUP").is_err() && temp_dir.exists() {
178        let _ = fs::remove_dir_all(temp_dir);
179    } else {
180        info!("Test artifacts left at: {}", temp_dir.display());
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn given_test_env_when_init_then_db_path_exists() {
190        let test_env = init_test_env();
191        let _guard = EnvGuard::new();
192        assert!(test_env.db_path.exists());
193        info!("test logic here");
194    }
195
196    #[test]
197    fn given_test_env_when_setup_test_db_then_returns_working_repository() {
198        let _ = init_test_env();
199        let repo = setup_test_db();
200        assert!(repo.get_connection().is_ok());
201    }
202
203    #[test]
204    fn given_test_env_when_setup_temp_dir_then_creates_directory_with_resources() {
205        let _ = init_test_env();
206        let temp_dir = setup_temp_dir();
207        assert!(temp_dir.exists());
208        teardown_temp_dir(&temp_dir);
209    }
210}