easy_prefs 3.0.1

The simplest to use API we could think of to persist prefs to disk. Basically wrap a macro around a struct (see syntax), then data is saved when you write to it. Performant, testable, thread safe, easy to migrate, and careful to not corrupt your data.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
//! # easy_prefs
//!
//! A simple, safe, and performant preferences library for Rust applications.
//!
//! Created by Ever Accountable – an app that helps people overcome compulsive porn use
//! and become their best selves. Visit [everaccountable.com](https://everaccountable.com) for more details.
//!
//! This library provides an intuitive API for managing preferences using a struct-like interface.
//! Its key design goals are:
//!
//! - **Ease of Use**: Read/write preferences as easily as struct fields.
//! - **Safety**: Uses temporary files for writes to prevent data corruption.
//! - **Performance**: Optimized for fast operations.
//! - **Testability**: Integrates seamlessly with unit tests.
//! - **Cross-Platform**: Works on native platforms and WebAssembly (WASM).
//!
//! **Limitation**: Not suited for large datasets. All data is held in memory, and the entire file
//! is rewritten on save. For substantial data, use a database instead.
//!
//! ## Single-Instance Constraint
//!
//! The `load()` method enforces that only one instance of a preferences struct exists at a time,
//! using a static atomic flag. This prevents data races in production but can cause issues in
//! parallel test execution. Tests using `load()` are combined into a single test to avoid conflicts.
//!
//! ## Error Handling
//!
//! The library provides two loading methods:
//! - `load()` - Always succeeds by returning defaults on errors (panics in debug mode)
//! - `load_with_error()` - Returns `Result<Self, LoadError>` for explicit error handling
//!
//! ## WASM Support
//!
//! This library supports WebAssembly targets for use in browser extensions and web applications.
//! When compiled to WASM, preferences are stored in localStorage instead of the file system.

pub mod storage;

// Re-export dependencies for convenience
pub use once_cell;
pub use paste; // Macro utilities
pub use toml; // TOML serialization
pub use web_time; // Cross-platform time implementation

/// Errors that can occur when loading preferences.
#[derive(Debug)]
pub enum LoadError {
    /// Another instance is already loaded (due to single-instance constraint).
    InstanceAlreadyLoaded,
    /// Failed to deserialize TOML data.
    DeserializationError(String, toml::de::Error),
    /// Storage operation failed
    StorageError(std::io::Error),
}

impl std::fmt::Display for LoadError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InstanceAlreadyLoaded => {
                write!(f, "another preferences instance is already loaded")
            }
            Self::DeserializationError(location, e) => {
                write!(f, "deserialization error: {e} at {location}")
            }
            Self::StorageError(e) => write!(f, "storage error: {e}"),
        }
    }
}

impl std::error::Error for LoadError {}
/// Macro to define a preferences struct with persistence.
///
/// Generates a struct with methods for loading, saving, and editing preferences.
/// Enforces a single instance (except in test mode) using a static flag.
///
/// # Example
///
/// ```rust
/// use easy_prefs::easy_prefs;
///
/// easy_prefs! {
///     pub struct AppPrefs {
///         pub dark_mode: bool = false => "dark_mode",
///         pub font_size: i32 = 14 => "font_size",
///     },
///     "app-settings"
/// }
/// ```
///
/// # Platform Behavior
///
/// - **Native**: Stores preferences as TOML files in the specified directory
/// - **WASM**: Stores preferences in browser localStorage
#[macro_export]
macro_rules! easy_prefs {
    (
        $(#[$outer:meta])*
        $vis:vis struct $name:ident {
            $(
                $(#[$inner:meta])*
                $field_vis:vis $field:ident: $type:ty = $default:expr => $saved_name:expr,
            )*
        },
        $preferences_filename:expr
    ) => {
        $crate::paste::paste!{
            // Static flag to enforce single instance.
            static [<$name:upper _INSTANCE_EXISTS>]: $crate::once_cell::sync::Lazy<std::sync::atomic::AtomicBool> =
                $crate::once_cell::sync::Lazy::new(|| std::sync::atomic::AtomicBool::new(false));

            // Guard that resets the instance flag on drop.
            #[derive(Debug)]
            struct [<$name InstanceGuard>];
            impl Drop for [<$name InstanceGuard>] {
                fn drop(&mut self) {
                    [<$name:upper _INSTANCE_EXISTS>].store(false, std::sync::atomic::Ordering::Release);
                }
            }

            $(#[$outer])*
            #[derive(serde::Serialize, serde::Deserialize, Debug)]
            #[serde(default)]  // Use defaults for missing fields.
            $vis struct $name {
                $(
                    $(#[$inner])*
                    #[serde(rename = $saved_name)]
                    $field_vis [<_ $field>]: $type,
                )*
                #[serde(skip_serializing, skip_deserializing)]
                storage: Option<Box<dyn $crate::storage::Storage>>,
                #[serde(skip_serializing, skip_deserializing)]
                storage_key: Option<String>,
                #[serde(skip_serializing, skip_deserializing)]
                #[cfg(not(target_arch = "wasm32"))]
                temp_file: Option<tempfile::NamedTempFile>,
                #[serde(skip_serializing, skip_deserializing)]
                _instance_guard: Option<[<$name InstanceGuard>]>,
            }

            impl Default for $name {
                fn default() -> Self {
                    Self {
                        $( [<_ $field>]: $default, )*
                        storage: None,
                        storage_key: None,
                        #[cfg(not(target_arch = "wasm32"))]
                        temp_file: None,
                        _instance_guard: None,
                    }
                }
            }

            impl $name {
                pub const PREFERENCES_FILENAME: &'static str = concat!($preferences_filename, ".toml");

                /// Loads preferences from a file, gracefully handling errors.
                ///
                /// This method provides a simple API that always succeeds:
                /// - In release builds: Returns defaults on errors (except instance conflicts)
                /// - In debug/test builds: Panics on errors to catch issues early
                /// - Always panics if another instance is already loaded
                ///
                /// For explicit error handling, use `load_with_error()` instead.
                ///
                /// # Arguments
                ///
                /// * `directory` - The directory path (native) or app ID (WASM) where preferences are stored.
                ///
                /// # Panics
                ///
                /// - Always panics if another instance is already loaded
                /// - In debug/test builds only: panics on storage or deserialization errors
                pub fn load(directory: &str) -> Self {
                    match Self::load_with_error(directory) {
                        Ok(prefs) => prefs,
                        Err(e) => {
                            // Always panic if another instance exists - this is a programming error
                            if matches!(e, $crate::LoadError::InstanceAlreadyLoaded) {
                                panic!("Failed to load preferences: {}", e);
                            }
                            
                            #[cfg(any(debug_assertions, test))]
                            {
                                // Panic in debug/test to catch issues early
                                panic!("Failed to load preferences: {}", e);
                            }
                            
                            #[cfg(not(any(debug_assertions, test)))]
                            {
                                // In production, log the error and return defaults
                                eprintln!("Failed to load preferences from {}: {}, using defaults", directory, e);
                                
                                // We need to acquire the instance guard for the default instance
                                // First, try to acquire it
                                let was_free = [<$name:upper _INSTANCE_EXISTS>].compare_exchange(
                                    false, true, std::sync::atomic::Ordering::Acquire, std::sync::atomic::Ordering::Relaxed
                                );
                                
                                if was_free.is_err() {
                                    // This should be rare - means load_with_error failed but instance still exists
                                    panic!("Failed to load preferences and instance is still locked: {}", e);
                                }
                                
                                let guard = [<$name InstanceGuard>];
                                let storage = $crate::storage::create_storage(directory);
                                let storage_key = Self::PREFERENCES_FILENAME;
                                
                                let mut cfg = Self::default();
                                cfg.storage = Some(storage);
                                cfg.storage_key = Some(storage_key.to_string());
                                cfg._instance_guard = Some(guard);
                                cfg
                            }
                        }
                    }
                }

                /// Loads preferences from a file with explicit error handling.
                ///
                /// Deserializes from file if it exists; otherwise uses defaults.
                /// Only one instance can exist at a time (tracked by a static flag).
                ///
                /// # Arguments
                ///
                /// * `directory` - The directory path (native) or app ID (WASM) where preferences are stored.
                ///
                /// # Errors
                ///
                /// Returns a `LoadError` if:
                /// - Another instance is already loaded.
                /// - Storage operations fail.
                /// - TOML deserialization fails.
                pub fn load_with_error(directory: &str) -> Result<Self, $crate::LoadError> {

                    {
                        // Runtime duplicate check for field_names. We don't want duplicates!
                        use std::collections::HashSet;
                        let keys = [ $( ($saved_name, stringify!($field) ), )* ];
                        let mut seen = HashSet::new();
                        for (key, field_name) in keys.iter() {
                            if !seen.insert(*key) {
                                panic!("Duplicate saved_name '{}' found for field '{}'", key, field_name);
                            }
                        }
                    }

                    let was_free = [<$name:upper _INSTANCE_EXISTS>].compare_exchange(
                        false, true, std::sync::atomic::Ordering::Acquire, std::sync::atomic::Ordering::Relaxed
                    );
                    if was_free.is_err() {
                        return Err($crate::LoadError::InstanceAlreadyLoaded);
                    }

                    let guard = [<$name InstanceGuard>];
                    let storage = $crate::storage::create_storage(directory);
                    let storage_key = Self::PREFERENCES_FILENAME;

                    let mut cfg = match storage.read(storage_key).map_err($crate::LoadError::StorageError)? {
                        Some(contents) => {
                            $crate::toml::from_str::<Self>(&contents)
                                .map_err(|e| $crate::LoadError::DeserializationError(
                                    storage.get_path(storage_key), e
                                ))?
                        }
                        None => Self::default(),
                    };

                    cfg.storage = Some(storage);
                    cfg.storage_key = Some(storage_key.to_string());
                    cfg._instance_guard = Some(guard);
                    Ok(cfg)
                }

                /// DEPRECATED: This method is no longer supported.
                ///
                /// # Why was this removed?
                ///
                /// `load_default()` bypassed the single-instance constraint, which could lead to:
                /// - Data corruption when multiple instances write to the same file
                /// - Race conditions in concurrent environments
                /// - Inconsistent application state
                ///
                /// # What to use instead?
                ///
                /// Use `load()` which handles errors gracefully:
                /// - In debug/test: Helps catch configuration issues early
                /// - In production: Falls back to defaults when needed
                ///
                /// If you need explicit error handling, use `load_with_error()`.
                ///
                /// # Panics
                ///
                /// Always panics with a deprecation message.
                #[deprecated(
                    since = "3.0.0",
                    note = "Use `load()` instead - it handles errors gracefully without compromising safety"
                )]
                pub fn load_default(_directory_or_app_id: &str) -> Self {
                    panic!(
                        "load_default() has been removed in version 3.0.0 because it bypassed safety constraints. \
                        Use load() instead, which handles errors gracefully while maintaining the single-instance guarantee. \
                        See the documentation for more details."
                    );
                }

                /// Loads preferences into a temporary location for testing (ignores the single-instance constraint).
                #[cfg(not(target_arch = "wasm32"))]
                pub fn load_testing() -> Self {
                    let tmp_file = tempfile::NamedTempFile::with_prefix(Self::PREFERENCES_FILENAME)
                        .expect("Failed to create temporary file for testing preferences");
                    let tmp_dir = tmp_file.path().parent().unwrap().to_str().unwrap();
                    let storage = $crate::storage::create_storage(tmp_dir);
                    let storage_key = tmp_file.path().file_name().unwrap().to_str().unwrap();

                    let mut cfg = Self::default();
                    let serialized = $crate::toml::to_string(&cfg).unwrap();
                    storage.write(storage_key, &serialized)
                        .expect("Failed to write preferences data to temporary file");

                    cfg.storage = Some(storage);
                    cfg.storage_key = Some(storage_key.to_string());
                    cfg.temp_file = Some(tmp_file);
                    cfg
                }

                /// Loads preferences into a temporary location for testing (ignores the single-instance constraint).
                #[cfg(target_arch = "wasm32")]
                pub fn load_testing() -> Self {
                    let test_id = format!("test_{}", $crate::web_time::SystemTime::now()
                        .duration_since($crate::web_time::UNIX_EPOCH)
                        .unwrap()
                        .as_millis());
                    let storage = $crate::storage::create_storage(&test_id);
                    let storage_key = Self::PREFERENCES_FILENAME;

                    let mut cfg = Self::default();
                    cfg.storage = Some(storage);
                    cfg.storage_key = Some(storage_key.to_string());
                    cfg
                }

                /// Serializes preferences to a TOML string.
                pub fn to_string(&self) -> String {
                    $crate::toml::to_string(self).expect("Serialization failed")
                }

                /// Save the preferences data to storage.
                ///
                /// This function serializes the preferences data to TOML format and writes it to storage.
                /// On native platforms, it uses atomic writes via temporary files. On WASM, it writes to localStorage.
                ///
                /// # Errors
                ///
                /// Returns an error if:
                /// - Storage is not initialized
                /// - Serialization fails
                /// - Storage write operation fails
                pub fn save(&self) -> Result<(), std::io::Error> {
                    // Ensure storage is initialized
                    let storage = self.storage.as_ref().ok_or_else(|| std::io::Error::new(
                        std::io::ErrorKind::Other,
                        "storage not initialized"
                    ))?;

                    let storage_key = self.storage_key.as_ref().ok_or_else(|| std::io::Error::new(
                        std::io::ErrorKind::Other,
                        "storage key not set"
                    ))?;

                    // Serialize the preferences data to TOML
                    let serialized = $crate::toml::to_string(self).map_err(|e| std::io::Error::new(
                        std::io::ErrorKind::Other,
                        format!("serialization failed: {}", e)
                    ))?;

                    // Write to storage
                    storage.write(storage_key, &serialized)?;

                    Ok(())
                }

                /// Returns the storage path/key as a string.
                pub fn get_preferences_file_path(&self) -> String {
                    match (&self.storage, &self.storage_key) {
                        (Some(storage), Some(key)) => storage.get_path(key),
                        _ => panic!("storage not initialized"),
                    }
                }

                $(
                    /// Gets the value of the field.
                    pub fn [<get_ $field>](&self) -> &$type {
                        &self.[<_ $field>]
                    }

                    /// Sets the field's value and immediately saves.
                    pub fn [<save_ $field>](&mut self, value: $type) -> Result<(), std::io::Error> {
                        if self.[<_ $field>] != value {
                            self.[<_ $field>] = value;
                            self.save()
                        } else {
                            Ok(())
                        }
                    }
                )*

                /// Creates an edit guard for batching updates (saves on drop).
                pub fn edit(&mut self) -> [<$name EditGuard>] {
                    [<$name EditGuard>] {
                        preferences: self,
                        modified: false,
                        created: $crate::web_time::Instant::now()
                    }
                }
            }

            /// Guard for batch editing; saves changes on drop if any fields were modified.
            $vis struct [<$name EditGuard>]<'a> {
                preferences: &'a mut $name,
                modified: bool,
                created: $crate::web_time::Instant,
            }

            impl<'a> [<$name EditGuard>]<'a> {
                $(
                    /// Sets the field's value (save is deferred until the guard is dropped).
                    pub fn [<set_ $field>](&mut self, value: $type) {
                        if self.preferences.[<_ $field>] != value {
                            self.preferences.[<_ $field>] = value;
                            self.modified = true;
                        }
                    }

                    /// Gets the current value of the field.
                    pub fn [<get_ $field>](&self) -> &$type {
                        &self.preferences.[<_ $field>]
                    }
                )*
            }

            impl<'a> Drop for [<$name EditGuard>]<'a> {
                fn drop(&mut self) {
                    if cfg!(debug_assertions) && !std::thread::panicking() {
                        let duration = self.created.elapsed();
                        // Warn if edit guard is held for more than 1 second in debug mode
                        if duration.as_secs() >= 1 {
                            eprintln!("Warning: Edit guard held for {:?} - consider reducing the scope", duration);
                        }
                    }
                    if self.modified {
                        if let Err(e) = self.preferences.save() {
                            eprintln!("Failed to save: {}", e);
                        }
                    }
                }
            }
        }
    }
}

#[allow(dead_code)]
#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Arc, Barrier, Mutex};
    use std::thread;
    use web_time::Duration;

    #[cfg(debug_assertions)]
    easy_prefs! {
        /// Original test preferences.
        struct TestEasyPreferences {
            pub bool1_default_true: bool = true => "bool1_default_true",
            pub bool2_default_true: bool = true => "bool2_default_true",
            pub bool3_initial_default_false: bool = false => "bool3_initial_default_false",
            pub string1: String = String::new() => "string1",
            pub int1: i32 = 42 => "int1",
        }, "test-easy-prefs"
    }

    #[cfg(debug_assertions)]
    easy_prefs! {
        /// Updated test preferences for schema evolution.
        pub struct TestEasyPreferencesUpdated {
            pub bool2_default_true_renamed: bool = true => "bool2_default_true",
            pub bool3_initial_default_false: bool = true => "bool3_initial_default_false",
            pub bool4_default_true: bool = true => "bool4_default_true",
            pub string1: String = "ea".to_string() => "string1",
            pub string2: String = "new default value".to_string() => "string2",
        }, "test-easy-prefs"
    }

    /// Tests loading and saving using `load_testing()` (ignores the single-instance constraint).
    #[test]
    fn test_load_save_preferences_with_macro() {
        let mut prefs = TestEasyPreferences::load_testing();
        assert_eq!(prefs.get_bool1_default_true(), &true);
        assert_eq!(prefs.get_int1(), &42);

        prefs
            .save_bool1_default_true(false)
            .expect("Failed to save bool1");
        prefs
            .save_string1("hi".to_string())
            .expect("Failed to save string1");

        // Verify the values were saved
        let file_path = prefs.get_preferences_file_path();
        assert!(file_path.contains("test-easy-prefs"));
        // For native platforms, we can verify the file contents
        #[cfg(not(target_arch = "wasm32"))]
        {
            let contents = std::fs::read_to_string(&file_path).expect("Failed to read file");
            assert!(contents.contains("bool1_default_true = false"));
            assert!(contents.contains("string1 = \"hi\""));
        }
    }

    /// Tests the edit guard batching and save-on-drop functionality.
    #[test]
    fn test_edit_guard() {
        let mut prefs = TestEasyPreferences::load_testing();
        {
            let mut guard = prefs.edit();
            guard.set_bool1_default_true(false);
            guard.set_int1(43);
        }
        assert_eq!(prefs.get_bool1_default_true(), &false);
        assert_eq!(prefs.get_int1(), &43);

        // Verify the values were saved
        #[cfg(not(target_arch = "wasm32"))]
        {
            let contents = std::fs::read_to_string(prefs.get_preferences_file_path())
                .expect("Failed to read file");
            assert!(contents.contains("bool1_default_true = false"));
            assert!(contents.contains("int1 = 43"));
        }
    }

    /// Tests multithreading with Arc/Mutex using `load_testing()`.
    #[test]
    fn test_with_arc_mutex() {
        let prefs = Arc::new(Mutex::new(TestEasyPreferences::load_testing()));
        {
            let prefs = prefs.lock().unwrap();
            assert_eq!(prefs.get_int1(), &42);
        }
        {
            let mut prefs = prefs.lock().unwrap();
            prefs.save_int1(100).expect("Failed to save int1");
        }
        {
            let prefs = prefs.lock().unwrap();
            assert_eq!(prefs.get_int1(), &100);
        }
    }

    /// Combined test for real file operations and the single-instance constraint.
    ///
    /// Running these tests sequentially avoids conflicts caused by the single-instance flag.
    #[test]
    fn test_real_preferences_and_single_instance() {
        // --- Part 1: Test persistence and schema upgrades ---
        {
            let path = {
                let prefs = TestEasyPreferences::load("/tmp/tests/");
                prefs.get_preferences_file_path()
            };
            let _ = std::fs::remove_file(&path); // Clean up any previous run

            // Save some values.
            {
                let mut prefs = TestEasyPreferences::load("/tmp/tests/");
                prefs
                    .save_bool1_default_true(false)
                    .expect("Failed to save bool1");
                prefs.edit().set_string1("test1".to_string());
            }
            // Verify persistence.
            {
                let prefs = TestEasyPreferences::load("/tmp/tests/");
                assert_eq!(prefs.get_bool1_default_true(), &false);
                assert_eq!(prefs.get_string1(), "test1");
            }
            // Test schema evolution.
            {
                let prefs = TestEasyPreferencesUpdated::load("/tmp/tests/");
                assert_eq!(prefs.get_bool2_default_true_renamed(), &true); // Default (not saved earlier)
                assert_eq!(prefs.get_string1(), "test1");
                assert_eq!(prefs.get_string2(), "new default value");
            }
        } // All instances from part 1 are now dropped

        // --- Part 2: Test the single-instance constraint ---
        let barrier = Arc::new(Barrier::new(2));
        let barrier_clone = barrier.clone();

        let test_dir = "/tmp/test_instance_conflict/";
        let handle = thread::spawn(move || {
            let prefs = TestEasyPreferences::load_with_error(test_dir).expect("Failed to load");
            barrier_clone.wait(); // Hold instance until main thread tries to load.
            thread::sleep(Duration::from_millis(100));
            drop(prefs); // Release instance.
            true
        });

        barrier.wait(); // Synchronize with spawned thread.
        let result = TestEasyPreferences::load_with_error(test_dir);
        assert!(matches!(result, Err(LoadError::InstanceAlreadyLoaded)));

        handle.join().unwrap(); // Wait for thread to finish.

        // Verify instance can be loaded after release.
        let _prefs = TestEasyPreferences::load(test_dir);

        // Verify that `load_testing()` ignores the single-instance constraint.
        let _test1 = TestEasyPreferences::load_testing();
        let _test2 = TestEasyPreferences::load_testing();
    }

    /// Test that the new load() API panics on errors in debug mode
    #[test]
    #[should_panic(expected = "Failed to load preferences")]
    #[cfg(debug_assertions)]
    fn test_load_panics_on_error_in_debug() {
        let test_dir = "/tmp/tests_panic/";
        
        // First load should succeed
        let _prefs1 = TestEasyPreferences::load(test_dir);
        
        // Second load should panic due to instance already loaded
        let _prefs2 = TestEasyPreferences::load(test_dir);
    }
}