Skip to main content

uv_preview/
lib.rs

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