whitaker_common/test_support/mod.rs
1//! Shared test helpers for Whitaker crates.
2//!
3//! The helpers in this module are intended for use from unit and integration
4//! tests so repeated boilerplate (such as locale overrides) can live in one
5//! place with the necessary safety documentation.
6//!
7//! ## Available helpers
8//!
9//! - [`fixtures`]: Copies UI fixtures (source files, `.stderr` expectations and
10//! support directories) into isolated workspaces for dylint UI harnesses.
11//! - [`decomposition`]: Reusable decomposition-advice fixtures for unit and
12//! behaviour tests.
13//! - [`env_test_guard`]: Serializes tests that temporarily mutate process-wide
14//! environment variables.
15//! - [`ui`]: Discovers fixtures, prepares isolated workspaces, and runs dylint
16//! UI tests with consistent panic handling.
17//! - [`LocaleOverride`]: Temporarily mutates `DYLINT_LOCALE` so locale-sensitive
18//! tests can execute without leaking global state between cases.
19
20pub mod decomposition;
21pub mod fixtures;
22pub mod ui;
23
24pub use fixtures::{copy_directory, copy_fixture};
25pub use ui::{
26 FixtureEnvironment, discover_fixtures, prepare_fixture, read_directory_config,
27 read_fixture_config, resolve_fixture_config, run_fixtures_with, run_test_runner,
28};
29
30use std::ffi::OsString;
31use std::sync::{Mutex, MutexGuard, OnceLock};
32
33/// Serializes tests that mutate process-wide environment variables.
34///
35/// Use this guard around helpers such as `temp_env::with_var` or
36/// `temp_env::with_vars_unset` when the test would otherwise race with other
37/// cases changing the same global process state.
38pub fn env_test_guard() -> MutexGuard<'static, ()> {
39 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
40 LOCK.get_or_init(|| Mutex::new(()))
41 .lock()
42 .unwrap_or_else(|error| panic!("expected environment test lock: {error}"))
43}
44
45/// Guard that overrides `DYLINT_LOCALE` for the lifetime of the instance.
46///
47/// The guard captures any existing value and restores it when dropped. The
48/// mutation itself must be executed under a serialized test harness (for
49/// example via the `serial_test::serial` attribute) to ensure the unsafe
50/// environment access remains race-free.
51///
52/// # Examples
53///
54/// ```ignore
55/// use whitaker_common::test_support::LocaleOverride;
56/// use serial_test::serial;
57///
58/// #[test]
59/// #[serial]
60/// fn ui_runs_in_welsh_locale() {
61/// let _guard = LocaleOverride::set("cy");
62/// // Execute lint UI harness here.
63/// }
64/// ```
65pub struct LocaleOverride {
66 previous: Option<OsString>,
67}
68
69impl LocaleOverride {
70 /// Sets `DYLINT_LOCALE` to `locale`, returning a guard that will restore
71 /// the prior value (if any) when dropped.
72 pub fn set(locale: &str) -> Self {
73 let previous = std::env::var_os("DYLINT_LOCALE");
74 // SAFETY: Callers must serialize the surrounding test using a
75 // synchronization primitive such as the `serial_test::serial`
76 // attribute. The guard is thread-local and dropped before another
77 // serialized test begins, so no two threads mutate the environment
78 // concurrently.
79 unsafe {
80 std::env::set_var("DYLINT_LOCALE", locale);
81 }
82 Self { previous }
83 }
84
85 /// Removes `DYLINT_LOCALE`, returning a guard that reinstates the prior
86 /// value (if any) when dropped.
87 ///
88 /// # Examples
89 ///
90 /// ```ignore
91 /// use whitaker_common::test_support::LocaleOverride;
92 /// use serial_test::serial;
93 /// use std::ffi::OsString;
94 ///
95 /// #[test]
96 /// #[serial]
97 /// fn clears_then_restores_locale() {
98 /// unsafe {
99 /// std::env::set_var("DYLINT_LOCALE", "cy");
100 /// }
101 /// {
102 /// let _guard = LocaleOverride::clear();
103 /// assert!(std::env::var_os("DYLINT_LOCALE").is_none());
104 /// }
105 /// assert_eq!(
106 /// std::env::var_os("DYLINT_LOCALE"),
107 /// Some(OsString::from("cy"))
108 /// );
109 /// }
110 /// ```
111 pub fn clear() -> Self {
112 let previous = std::env::var_os("DYLINT_LOCALE");
113 // SAFETY: Callers must serialize the surrounding test using a
114 // synchronization primitive such as the `serial_test::serial`
115 // attribute. Clearing the environment therefore cannot race with other
116 // threads.
117 unsafe {
118 std::env::remove_var("DYLINT_LOCALE");
119 }
120 Self { previous }
121 }
122}
123
124impl Drop for LocaleOverride {
125 fn drop(&mut self) {
126 if let Some(value) = &self.previous {
127 // SAFETY: By construction the guard only lives within a serialized
128 // test, so restoring the prior value cannot race with another
129 // thread mutating the environment.
130 unsafe {
131 std::env::set_var("DYLINT_LOCALE", value);
132 }
133 } else {
134 // SAFETY: Serialized execution also guarantees removal has no
135 // concurrent callers.
136 unsafe {
137 std::env::remove_var("DYLINT_LOCALE");
138 }
139 }
140 }
141}