Skip to main content

uv_preview/
lib.rs

1use std::sync::{Mutex, OnceLock};
2use std::{
3    fmt::{Debug, Display, Formatter},
4    ops::BitOr,
5    str::FromStr,
6};
7
8use enumflags2::{BitFlags, bitflags};
9use thiserror::Error;
10use uv_warnings::warn_user_once;
11
12/// Indicates if the preview state has been finalized yet or not.
13enum PreviewState {
14    Provisional(Preview),
15    Final(Preview),
16}
17
18/// Indicates how the preview was initialised, to distinguish between normal
19/// code and unit tests.
20enum PreviewMode {
21    /// Initialised by a call to [`init`].
22    Normal(Mutex<PreviewState>),
23    /// Initialised by a call to [`test::with_features`].
24    #[cfg(feature = "testing")]
25    Test(std::sync::RwLock<Option<Preview>>),
26}
27
28static PREVIEW: OnceLock<PreviewMode> = OnceLock::new();
29
30/// Error type for global preview state initialization related errors
31#[derive(Debug, Error)]
32pub enum PreviewError {
33    /// Returned when [`set`] or [`finalize`] are called on a finalized state.
34    #[error("The preview configuration has already been finalized")]
35    AlreadyFinalized,
36
37    /// Returned when [`finalize`] is called on an uninitialized state.
38    #[error("The preview configuration has not been initialized yet")]
39    NotInitialized,
40
41    /// Returned when [`set`] or [`finalize`] are called on a test state.
42    #[cfg(feature = "testing")]
43    #[error("The preview configuration is in test mode and {}::{} cannot be used", module_path!(), .0)]
44    InTest(&'static str),
45}
46
47/// Initialize the global preview configuration.
48///
49/// This should be called once at startup with the resolved preview settings.
50pub fn set(preview: Preview) -> Result<(), PreviewError> {
51    let mode = PREVIEW.get_or_init(|| {
52        PreviewMode::Normal(Mutex::new(PreviewState::Provisional(Preview::default())))
53    });
54    match mode {
55        PreviewMode::Normal(mutex) => {
56            // Calling `set` in a test context is already disallowed, so a panic if
57            // the mutex is poisoned is fine.
58            let mut state = mutex.lock().unwrap();
59            match &*state {
60                PreviewState::Provisional(_) => {
61                    *state = PreviewState::Provisional(preview);
62                    Ok(())
63                }
64                PreviewState::Final(_) => Err(PreviewError::AlreadyFinalized),
65            }
66        }
67        #[cfg(feature = "testing")]
68        PreviewMode::Test(_) => Err(PreviewError::InTest("set")),
69    }
70}
71
72pub fn finalize() -> Result<(), PreviewError> {
73    match PREVIEW.get().ok_or(PreviewError::NotInitialized)? {
74        PreviewMode::Normal(mutex) => {
75            // Calling `set` in a test context is already disallowed, so a panic if
76            // the mutex is poisoned is fine.
77            let mut state = mutex.lock().unwrap();
78            match &*state {
79                PreviewState::Provisional(preview) => {
80                    *state = PreviewState::Final(*preview);
81                    Ok(())
82                }
83                PreviewState::Final(_) => Err(PreviewError::AlreadyFinalized),
84            }
85        }
86        #[cfg(feature = "testing")]
87        PreviewMode::Test(_) => Err(PreviewError::InTest("finalize")),
88    }
89}
90
91/// Get the current global preview configuration.
92///
93/// # Panics
94///
95/// When called before [`init`] or (with the `testing` feature) when the
96/// current thread does not hold a [`test::with_features`] guard.
97pub fn get() -> Preview {
98    match PREVIEW.get() {
99        Some(PreviewMode::Normal(mutex)) => match *mutex.lock().unwrap() {
100            PreviewState::Provisional(preview) => preview,
101            PreviewState::Final(preview) => preview,
102        },
103        #[cfg(feature = "testing")]
104        Some(PreviewMode::Test(rwlock)) => {
105            assert!(
106                test::HELD.get(),
107                "The preview configuration is in test mode but the current thread does not hold a `FeaturesGuard`\nHint: Use `{}::test::with_features` to get a `FeaturesGuard` and hold it when testing functions which rely on the global preview state",
108                module_path!()
109            );
110            // The unwrap may panic only if the current thread had panicked
111            // while attempting to write the value and then recovered with
112            // `catch_unwind`. This seems unlikely.
113            rwlock
114                .read()
115                .unwrap()
116                .expect("FeaturesGuard is held but preview value is not set")
117        }
118        #[cfg(feature = "testing")]
119        None => panic!(
120            "The preview configuration has not been initialized\nHint: Use `{}::init` or `{}::test::with_features` to initialize it",
121            module_path!(),
122            module_path!()
123        ),
124        #[cfg(not(feature = "testing"))]
125        None => panic!("The preview configuration has not been initialized"),
126    }
127}
128
129/// Check if a specific preview feature is enabled globally.
130pub fn is_enabled(flag: PreviewFeature) -> bool {
131    get().is_enabled(flag)
132}
133
134/// Functions for unit tests, do not use from normal code!
135#[cfg(feature = "testing")]
136pub mod test {
137    use super::{PREVIEW, Preview, PreviewMode};
138    use std::cell::Cell;
139    use std::sync::{Mutex, MutexGuard, RwLock};
140
141    /// The global preview state test mutex. It does not guard any data but is
142    /// simply used to ensure tests which rely on the global preview state are
143    /// ran serially.
144    static MUTEX: Mutex<()> = Mutex::new(());
145
146    thread_local! {
147        /// Whether the current thread holds the global mutex.
148        ///
149        /// This is used to catch situations where a test forgets to set the
150        /// global test state but happens to work anyway because of another test
151        /// setting the state.
152        pub(crate) static HELD: Cell<bool> = const { Cell::new(false) };
153    }
154
155    /// A scope guard which ensures that the global preview state is configured
156    /// and consistent for the duration of its lifetime.
157    #[derive(Debug)]
158    #[expect(unused)]
159    pub struct FeaturesGuard(MutexGuard<'static, ()>);
160
161    /// Temporarily set the state of preview features for the duration of the
162    /// lifetime of the returned guard.
163    ///
164    /// Calls cannot be nested, and this function must be used to set the global
165    /// preview features when testing functionality which uses it, otherwise
166    /// that functionality will panic.
167    ///
168    /// The preview state will only be valid for the thread which calls this
169    /// function, it will not be valid for any other thread. This is a
170    /// consequence of how `HELD` is used to check for tests which are missing
171    /// the guard.
172    pub fn with_features(features: &[super::PreviewFeature]) -> FeaturesGuard {
173        assert!(
174            !HELD.get(),
175            "Additional calls to `{}::with_features` are not allowed while holding a `FeaturesGuard`",
176            module_path!()
177        );
178
179        let guard = match MUTEX.lock() {
180            Ok(guard) => guard,
181            // This is okay because the mutex isn't guarding any data, so when
182            // it gets poisoned, it just means a test thread died while holding
183            // it, so it's safe to just re-grab it from the PoisonError, there's
184            // no chance of any corruption.
185            Err(err) => err.into_inner(),
186        };
187
188        HELD.set(true);
189
190        let state = PREVIEW.get_or_init(|| PreviewMode::Test(RwLock::new(None)));
191        match state {
192            PreviewMode::Test(rwlock) => {
193                *rwlock.write().unwrap() = Some(Preview::new(features));
194            }
195            PreviewMode::Normal(_) => {
196                panic!(
197                    "Cannot use `{}::with_features` after `uv_preview::init` has been called",
198                    module_path!()
199                );
200            }
201        }
202        FeaturesGuard(guard)
203    }
204
205    impl Drop for FeaturesGuard {
206        fn drop(&mut self) {
207            HELD.set(false);
208
209            match PREVIEW.get().unwrap() {
210                PreviewMode::Test(rwlock) => {
211                    *rwlock.write().unwrap() = None;
212                }
213                PreviewMode::Normal(_) => {
214                    unreachable!("FeaturesGuard should not exist when in Normal mode");
215                }
216            }
217        }
218    }
219}
220
221#[bitflags]
222#[repr(u32)]
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum PreviewFeature {
225    PythonInstallDefault = 1 << 0,
226    PythonUpgrade = 1 << 1,
227    JsonOutput = 1 << 2,
228    Pylock = 1 << 3,
229    AddBounds = 1 << 4,
230    PackageConflicts = 1 << 5,
231    ExtraBuildDependencies = 1 << 6,
232    DetectModuleConflicts = 1 << 7,
233    Format = 1 << 8,
234    NativeAuth = 1 << 9,
235    S3Endpoint = 1 << 10,
236    CacheSize = 1 << 11,
237    InitProjectFlag = 1 << 12,
238    WorkspaceMetadata = 1 << 13,
239    WorkspaceDir = 1 << 14,
240    WorkspaceList = 1 << 15,
241    SbomExport = 1 << 16,
242    AuthHelper = 1 << 17,
243    DirectPublish = 1 << 18,
244    TargetWorkspaceDiscovery = 1 << 19,
245    MetadataJson = 1 << 20,
246    GcsEndpoint = 1 << 21,
247    AdjustUlimit = 1 << 22,
248    SpecialCondaEnvNames = 1 << 23,
249    RelocatableEnvsDefault = 1 << 24,
250    PublishRequireNormalized = 1 << 25,
251    Audit = 1 << 26,
252    ProjectDirectoryMustExist = 1 << 27,
253    IndexExcludeNewer = 1 << 28,
254    AzureEndpoint = 1 << 29,
255    TomlBackwardsCompatibility = 1 << 30,
256    MalwareCheck = 1 << 31,
257}
258
259impl PreviewFeature {
260    /// Returns the string representation of a single preview feature flag.
261    fn as_str(self) -> &'static str {
262        match self {
263            Self::PythonInstallDefault => "python-install-default",
264            Self::PythonUpgrade => "python-upgrade",
265            Self::JsonOutput => "json-output",
266            Self::Pylock => "pylock",
267            Self::AddBounds => "add-bounds",
268            Self::PackageConflicts => "package-conflicts",
269            Self::ExtraBuildDependencies => "extra-build-dependencies",
270            Self::DetectModuleConflicts => "detect-module-conflicts",
271            Self::Format => "format",
272            Self::NativeAuth => "native-auth",
273            Self::S3Endpoint => "s3-endpoint",
274            Self::CacheSize => "cache-size",
275            Self::InitProjectFlag => "init-project-flag",
276            Self::WorkspaceMetadata => "workspace-metadata",
277            Self::WorkspaceDir => "workspace-dir",
278            Self::WorkspaceList => "workspace-list",
279            Self::SbomExport => "sbom-export",
280            Self::AuthHelper => "auth-helper",
281            Self::DirectPublish => "direct-publish",
282            Self::TargetWorkspaceDiscovery => "target-workspace-discovery",
283            Self::MetadataJson => "metadata-json",
284            Self::GcsEndpoint => "gcs-endpoint",
285            Self::AdjustUlimit => "adjust-ulimit",
286            Self::SpecialCondaEnvNames => "special-conda-env-names",
287            Self::RelocatableEnvsDefault => "relocatable-envs-default",
288            Self::PublishRequireNormalized => "publish-require-normalized",
289            Self::Audit => "audit",
290            Self::ProjectDirectoryMustExist => "project-directory-must-exist",
291            Self::IndexExcludeNewer => "index-exclude-newer",
292            Self::AzureEndpoint => "azure-endpoint",
293            Self::TomlBackwardsCompatibility => "toml-backwards-compatibility",
294            Self::MalwareCheck => "malware-check",
295        }
296    }
297}
298
299impl Display for PreviewFeature {
300    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
301        write!(f, "{}", self.as_str())
302    }
303}
304
305#[derive(Debug, Error, Clone)]
306#[error("Unknown feature flag")]
307pub struct PreviewFeatureParseError;
308
309impl FromStr for PreviewFeature {
310    type Err = PreviewFeatureParseError;
311
312    fn from_str(s: &str) -> Result<Self, Self::Err> {
313        Ok(match s {
314            "python-install-default" => Self::PythonInstallDefault,
315            "python-upgrade" => Self::PythonUpgrade,
316            "json-output" => Self::JsonOutput,
317            "pylock" => Self::Pylock,
318            "add-bounds" => Self::AddBounds,
319            "package-conflicts" => Self::PackageConflicts,
320            "extra-build-dependencies" => Self::ExtraBuildDependencies,
321            "detect-module-conflicts" => Self::DetectModuleConflicts,
322            "format" => Self::Format,
323            "native-auth" => Self::NativeAuth,
324            "s3-endpoint" => Self::S3Endpoint,
325            "gcs-endpoint" => Self::GcsEndpoint,
326            "cache-size" => Self::CacheSize,
327            "init-project-flag" => Self::InitProjectFlag,
328            "workspace-metadata" => Self::WorkspaceMetadata,
329            "workspace-dir" => Self::WorkspaceDir,
330            "workspace-list" => Self::WorkspaceList,
331            "sbom-export" => Self::SbomExport,
332            "auth-helper" => Self::AuthHelper,
333            "direct-publish" => Self::DirectPublish,
334            "target-workspace-discovery" => Self::TargetWorkspaceDiscovery,
335            "metadata-json" => Self::MetadataJson,
336            "adjust-ulimit" => Self::AdjustUlimit,
337            "special-conda-env-names" => Self::SpecialCondaEnvNames,
338            "relocatable-envs-default" => Self::RelocatableEnvsDefault,
339            "publish-require-normalized" => Self::PublishRequireNormalized,
340            "audit" => Self::Audit,
341            "project-directory-must-exist" => Self::ProjectDirectoryMustExist,
342            "index-exclude-newer" => Self::IndexExcludeNewer,
343            "azure-endpoint" => Self::AzureEndpoint,
344            "toml-backwards-compatibility" => Self::TomlBackwardsCompatibility,
345            "malware-check" => Self::MalwareCheck,
346            _ => return Err(PreviewFeatureParseError),
347        })
348    }
349}
350
351#[derive(Clone, Copy, PartialEq, Eq, Default)]
352pub struct Preview {
353    flags: BitFlags<PreviewFeature>,
354}
355
356impl Debug for Preview {
357    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
358        let flags: Vec<_> = self.flags.iter().collect();
359        f.debug_struct("Preview").field("flags", &flags).finish()
360    }
361}
362
363impl Preview {
364    pub fn new(flags: &[PreviewFeature]) -> Self {
365        Self {
366            flags: flags.iter().copied().fold(BitFlags::empty(), BitOr::bitor),
367        }
368    }
369
370    pub fn all() -> Self {
371        Self {
372            flags: BitFlags::all(),
373        }
374    }
375
376    pub fn from_args(preview: bool, no_preview: bool, preview_features: &[PreviewFeature]) -> Self {
377        if no_preview {
378            return Self::default();
379        }
380
381        if preview {
382            return Self::all();
383        }
384
385        Self::new(preview_features)
386    }
387
388    /// Check if a single feature is enabled.
389    pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
390        self.flags.contains(flag)
391    }
392
393    /// Check if all preview feature rae enabled.
394    pub fn all_enabled(&self) -> bool {
395        self.flags.is_all()
396    }
397
398    /// Check if any preview feature is enabled.
399    pub fn any_enabled(&self) -> bool {
400        !self.flags.is_empty()
401    }
402}
403
404impl Display for Preview {
405    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
406        if self.flags.is_empty() {
407            write!(f, "disabled")
408        } else if self.flags.is_all() {
409            write!(f, "enabled")
410        } else {
411            write!(
412                f,
413                "{}",
414                itertools::join(self.flags.iter().map(PreviewFeature::as_str), ",")
415            )
416        }
417    }
418}
419
420#[derive(Debug, Error, Clone)]
421pub enum PreviewParseError {
422    #[error("Empty string in preview features: {0}")]
423    Empty(String),
424}
425
426impl FromStr for Preview {
427    type Err = PreviewParseError;
428
429    fn from_str(s: &str) -> Result<Self, Self::Err> {
430        let mut flags = BitFlags::empty();
431
432        for part in s.split(',') {
433            let part = part.trim();
434            if part.is_empty() {
435                return Err(PreviewParseError::Empty(
436                    "Empty string in preview features".to_string(),
437                ));
438            }
439
440            match PreviewFeature::from_str(part) {
441                Ok(flag) => flags |= flag,
442                Err(_) => {
443                    warn_user_once!("Unknown preview feature: `{part}`");
444                }
445            }
446        }
447
448        Ok(Self { flags })
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_preview_feature_from_str() {
458        let features = PreviewFeature::from_str("python-install-default").unwrap();
459        assert_eq!(features, PreviewFeature::PythonInstallDefault);
460    }
461
462    #[test]
463    fn test_preview_from_str() {
464        // Test single feature
465        let preview = Preview::from_str("python-install-default").unwrap();
466        assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault);
467
468        // Test multiple features
469        let preview = Preview::from_str("python-upgrade,json-output").unwrap();
470        assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
471        assert!(preview.is_enabled(PreviewFeature::JsonOutput));
472        assert_eq!(preview.flags.bits().count_ones(), 2);
473
474        // Test with whitespace
475        let preview = Preview::from_str("pylock , add-bounds").unwrap();
476        assert!(preview.is_enabled(PreviewFeature::Pylock));
477        assert!(preview.is_enabled(PreviewFeature::AddBounds));
478
479        // Test empty string error
480        assert!(Preview::from_str("").is_err());
481        assert!(Preview::from_str("pylock,").is_err());
482        assert!(Preview::from_str(",pylock").is_err());
483
484        // Test unknown feature (should be ignored with warning)
485        let preview = Preview::from_str("unknown-feature,pylock").unwrap();
486        assert!(preview.is_enabled(PreviewFeature::Pylock));
487        assert_eq!(preview.flags.bits().count_ones(), 1);
488    }
489
490    #[test]
491    fn test_preview_display() {
492        // Test disabled
493        let preview = Preview::default();
494        assert_eq!(preview.to_string(), "disabled");
495        let preview = Preview::new(&[]);
496        assert_eq!(preview.to_string(), "disabled");
497
498        // Test enabled (all features)
499        let preview = Preview::all();
500        assert_eq!(preview.to_string(), "enabled");
501
502        // Test single feature
503        let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
504        assert_eq!(preview.to_string(), "python-install-default");
505
506        // Test multiple features
507        let preview = Preview::new(&[PreviewFeature::PythonUpgrade, PreviewFeature::Pylock]);
508        assert_eq!(preview.to_string(), "python-upgrade,pylock");
509    }
510
511    #[test]
512    fn test_preview_from_args() {
513        // Test no preview and no no_preview, and no features
514        let preview = Preview::from_args(false, false, &[]);
515        assert_eq!(preview.to_string(), "disabled");
516
517        // Test no_preview
518        let preview = Preview::from_args(true, true, &[]);
519        assert_eq!(preview.to_string(), "disabled");
520
521        // Test preview (all features)
522        let preview = Preview::from_args(true, false, &[]);
523        assert_eq!(preview.to_string(), "enabled");
524
525        // Test specific features
526        let features = vec![PreviewFeature::PythonUpgrade, PreviewFeature::JsonOutput];
527        let preview = Preview::from_args(false, false, &features);
528        assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
529        assert!(preview.is_enabled(PreviewFeature::JsonOutput));
530        assert!(!preview.is_enabled(PreviewFeature::Pylock));
531    }
532
533    #[test]
534    fn test_preview_feature_as_str() {
535        assert_eq!(
536            PreviewFeature::PythonInstallDefault.as_str(),
537            "python-install-default"
538        );
539        assert_eq!(PreviewFeature::PythonUpgrade.as_str(), "python-upgrade");
540        assert_eq!(PreviewFeature::JsonOutput.as_str(), "json-output");
541        assert_eq!(PreviewFeature::Pylock.as_str(), "pylock");
542        assert_eq!(PreviewFeature::AddBounds.as_str(), "add-bounds");
543        assert_eq!(
544            PreviewFeature::PackageConflicts.as_str(),
545            "package-conflicts"
546        );
547        assert_eq!(
548            PreviewFeature::ExtraBuildDependencies.as_str(),
549            "extra-build-dependencies"
550        );
551        assert_eq!(
552            PreviewFeature::DetectModuleConflicts.as_str(),
553            "detect-module-conflicts"
554        );
555        assert_eq!(PreviewFeature::Format.as_str(), "format");
556        assert_eq!(PreviewFeature::NativeAuth.as_str(), "native-auth");
557        assert_eq!(PreviewFeature::S3Endpoint.as_str(), "s3-endpoint");
558        assert_eq!(PreviewFeature::CacheSize.as_str(), "cache-size");
559        assert_eq!(
560            PreviewFeature::InitProjectFlag.as_str(),
561            "init-project-flag"
562        );
563        assert_eq!(
564            PreviewFeature::WorkspaceMetadata.as_str(),
565            "workspace-metadata"
566        );
567        assert_eq!(PreviewFeature::WorkspaceDir.as_str(), "workspace-dir");
568        assert_eq!(PreviewFeature::WorkspaceList.as_str(), "workspace-list");
569        assert_eq!(PreviewFeature::SbomExport.as_str(), "sbom-export");
570        assert_eq!(PreviewFeature::AuthHelper.as_str(), "auth-helper");
571        assert_eq!(PreviewFeature::DirectPublish.as_str(), "direct-publish");
572        assert_eq!(
573            PreviewFeature::TargetWorkspaceDiscovery.as_str(),
574            "target-workspace-discovery"
575        );
576        assert_eq!(PreviewFeature::MetadataJson.as_str(), "metadata-json");
577        assert_eq!(PreviewFeature::GcsEndpoint.as_str(), "gcs-endpoint");
578        assert_eq!(PreviewFeature::AdjustUlimit.as_str(), "adjust-ulimit");
579        assert_eq!(
580            PreviewFeature::SpecialCondaEnvNames.as_str(),
581            "special-conda-env-names"
582        );
583        assert_eq!(
584            PreviewFeature::RelocatableEnvsDefault.as_str(),
585            "relocatable-envs-default"
586        );
587        assert_eq!(
588            PreviewFeature::PublishRequireNormalized.as_str(),
589            "publish-require-normalized"
590        );
591        assert_eq!(
592            PreviewFeature::ProjectDirectoryMustExist.as_str(),
593            "project-directory-must-exist"
594        );
595        assert_eq!(
596            PreviewFeature::IndexExcludeNewer.as_str(),
597            "index-exclude-newer"
598        );
599        assert_eq!(PreviewFeature::AzureEndpoint.as_str(), "azure-endpoint");
600        assert_eq!(
601            PreviewFeature::TomlBackwardsCompatibility.as_str(),
602            "toml-backwards-compatibility"
603        );
604        assert_eq!(PreviewFeature::MalwareCheck.as_str(), "malware-check");
605    }
606
607    #[test]
608    fn test_global_preview() {
609        {
610            let _guard =
611                test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
612            assert!(!is_enabled(PreviewFeature::InitProjectFlag));
613            assert!(is_enabled(PreviewFeature::Pylock));
614            assert!(is_enabled(PreviewFeature::WorkspaceMetadata));
615            assert!(!is_enabled(PreviewFeature::AuthHelper));
616        }
617        {
618            let _guard =
619                test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
620            assert!(is_enabled(PreviewFeature::InitProjectFlag));
621            assert!(!is_enabled(PreviewFeature::Pylock));
622            assert!(!is_enabled(PreviewFeature::WorkspaceMetadata));
623            assert!(is_enabled(PreviewFeature::AuthHelper));
624        }
625    }
626
627    #[test]
628    #[should_panic(
629        expected = "Additional calls to `uv_preview::test::with_features` are not allowed while holding a `FeaturesGuard`"
630    )]
631    fn test_global_preview_panic_nested() {
632        let _guard =
633            test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
634        let _guard2 =
635            test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
636    }
637
638    #[test]
639    #[should_panic(expected = "uv_preview::test::with_features")]
640    fn test_global_preview_panic_uninitialized() {
641        let _preview = get();
642    }
643}