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
14enum PreviewState {
16 Provisional(Preview),
17 Final(Preview),
18}
19
20enum PreviewMode {
23 Normal(Mutex<PreviewState>),
25 #[cfg(feature = "testing")]
27 Test(std::sync::RwLock<Option<Preview>>),
28}
29
30static PREVIEW: OnceLock<PreviewMode> = OnceLock::new();
31
32#[derive(Debug, Error)]
34pub enum PreviewError {
35 #[error("The preview configuration has already been finalized")]
37 AlreadyFinalized,
38
39 #[error("The preview configuration has not been initialized yet")]
41 NotInitialized,
42
43 #[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
49pub 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 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 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
93fn 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 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
131pub fn is_enabled(flag: PreviewFeature) -> bool {
133 get().is_enabled(flag)
134}
135
136#[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 static MUTEX: Mutex<()> = Mutex::new(());
147
148 thread_local! {
149 pub(crate) static HELD: Cell<bool> = const { Cell::new(false) };
155 }
156
157 #[derive(Debug)]
160 #[expect(unused)]
161 pub struct FeaturesGuard(MutexGuard<'static, ()>);
162
163 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 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 PythonUpgrade = 1 << 1,
229 JsonOutput = 1 << 2,
230 Pylock = 1 << 3,
231 AddBounds = 1 << 4,
232 PackageConflicts = 1 << 5,
233 ExtraBuildDependencies = 1 << 6,
234 DetectModuleConflicts = 1 << 7,
235 Format = 1 << 8,
236 NativeAuth = 1 << 9,
237 S3Endpoint = 1 << 10,
238 CacheSize = 1 << 11,
239 InitProjectFlag = 1 << 12,
240 WorkspaceMetadata = 1 << 13,
241 WorkspaceDir = 1 << 14,
242 WorkspaceList = 1 << 15,
243 SbomExport = 1 << 16,
244 AuthHelper = 1 << 17,
245 DirectPublish = 1 << 18,
246 TargetWorkspaceDiscovery = 1 << 19,
247 MetadataJson = 1 << 20,
248 GcsEndpoint = 1 << 21,
249 AdjustUlimit = 1 << 22,
250 SpecialCondaEnvNames = 1 << 23,
251 RelocatableEnvsDefault = 1 << 24,
252 PublishRequireNormalized = 1 << 25,
253 Audit = 1 << 26,
254 ProjectDirectoryMustExist = 1 << 27,
255 IndexExcludeNewer = 1 << 28,
256 AzureEndpoint = 1 << 29,
257 TomlBackwardsCompatibility = 1 << 30,
258 MalwareCheck = 1 << 31,
259 VenvSafeClear = 1 << 32,
260 Check = 1 << 33,
261 PackagedInit = 1 << 34,
262 CentralizedProjectEnvs = 1 << 35,
263 ToolInstallLocks = 1 << 36,
264 WorkspaceListScripts = 1 << 37,
265 NoDistutilsPatch = 1 << 38,
266 IndexHashAlgorithm = 1 << 39,
267}
268
269impl PreviewFeature {
270 fn as_str(self) -> &'static str {
272 match self {
273 Self::PythonInstallDefault => "python-install-default",
274 Self::PythonUpgrade => "python-upgrade",
275 Self::JsonOutput => "json-output",
276 Self::Pylock => "pylock",
277 Self::AddBounds => "add-bounds",
278 Self::PackageConflicts => "package-conflicts",
279 Self::ExtraBuildDependencies => "extra-build-dependencies",
280 Self::DetectModuleConflicts => "detect-module-conflicts",
281 Self::Format => "format-command",
282 Self::NativeAuth => "native-auth",
283 Self::S3Endpoint => "s3-endpoint",
284 Self::CacheSize => "cache-size",
285 Self::InitProjectFlag => "init-project-flag",
286 Self::WorkspaceMetadata => "workspace-metadata",
287 Self::WorkspaceDir => "workspace-dir",
288 Self::WorkspaceList => "workspace-list",
289 Self::SbomExport => "sbom-export",
290 Self::AuthHelper => "auth-helper",
291 Self::DirectPublish => "direct-publish",
292 Self::TargetWorkspaceDiscovery => "target-workspace-discovery",
293 Self::MetadataJson => "metadata-json",
294 Self::GcsEndpoint => "gcs-endpoint",
295 Self::AdjustUlimit => "adjust-ulimit",
296 Self::SpecialCondaEnvNames => "special-conda-env-names",
297 Self::RelocatableEnvsDefault => "relocatable-envs-default",
298 Self::PublishRequireNormalized => "publish-require-normalized",
299 Self::Audit => "audit-command",
300 Self::ProjectDirectoryMustExist => "project-directory-must-exist",
301 Self::IndexExcludeNewer => "index-exclude-newer",
302 Self::AzureEndpoint => "azure-endpoint",
303 Self::TomlBackwardsCompatibility => "toml-backwards-compatibility",
304 Self::MalwareCheck => "malware-check",
305 Self::VenvSafeClear => "venv-safe-clear",
306 Self::Check => "check-command",
307 Self::PackagedInit => "packaged-init",
308 Self::CentralizedProjectEnvs => "centralized-project-envs",
309 Self::ToolInstallLocks => "tool-install-locks",
310 Self::WorkspaceListScripts => "workspace-list-scripts",
311 Self::NoDistutilsPatch => "no-distutils-patch",
312 Self::IndexHashAlgorithm => "index-hash-algorithm",
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 "python-upgrade" => Self::PythonUpgrade,
334 "json-output" => Self::JsonOutput,
335 "pylock" => Self::Pylock,
336 "add-bounds" => Self::AddBounds,
337 "package-conflicts" => Self::PackageConflicts,
338 "extra-build-dependencies" => Self::ExtraBuildDependencies,
339 "detect-module-conflicts" => Self::DetectModuleConflicts,
340 "format" | "format-command" => Self::Format,
341 "native-auth" => Self::NativeAuth,
342 "s3-endpoint" => Self::S3Endpoint,
343 "gcs-endpoint" => Self::GcsEndpoint,
344 "cache-size" => Self::CacheSize,
345 "init-project-flag" => Self::InitProjectFlag,
346 "workspace-metadata" => Self::WorkspaceMetadata,
347 "workspace-dir" => Self::WorkspaceDir,
348 "workspace-list" => Self::WorkspaceList,
349 "sbom-export" => Self::SbomExport,
350 "auth-helper" => Self::AuthHelper,
351 "direct-publish" => Self::DirectPublish,
352 "target-workspace-discovery" => Self::TargetWorkspaceDiscovery,
353 "metadata-json" => Self::MetadataJson,
354 "adjust-ulimit" => Self::AdjustUlimit,
355 "special-conda-env-names" => Self::SpecialCondaEnvNames,
356 "relocatable-envs-default" => Self::RelocatableEnvsDefault,
357 "publish-require-normalized" => Self::PublishRequireNormalized,
358 "audit" | "audit-command" => Self::Audit,
359 "project-directory-must-exist" => Self::ProjectDirectoryMustExist,
360 "index-exclude-newer" => Self::IndexExcludeNewer,
361 "azure-endpoint" => Self::AzureEndpoint,
362 "toml-backwards-compatibility" => Self::TomlBackwardsCompatibility,
363 "malware-check" => Self::MalwareCheck,
364 "venv-safe-clear" => Self::VenvSafeClear,
365 "check" | "check-command" => Self::Check,
366 "packaged-init" => Self::PackagedInit,
367 "centralized-project-envs" => Self::CentralizedProjectEnvs,
368 "tool-install-locks" => Self::ToolInstallLocks,
369 "workspace-list-scripts" => Self::WorkspaceListScripts,
370 "no-distutils-patch" => Self::NoDistutilsPatch,
371 "index-hash-algorithm" => Self::IndexHashAlgorithm,
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#[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 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 pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
469 self.flags.contains(flag)
470 }
471
472 pub fn all_enabled(&self) -> bool {
474 self.flags.is_all()
475 }
476
477 pub fn any_enabled(&self) -> bool {
479 !self.flags.is_empty()
480 }
481
482 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 let preview = Preview::from_str("python-install-default").unwrap();
544 assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault);
545
546 let preview = Preview::from_str("python-upgrade,json-output").unwrap();
548 assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
549 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
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 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 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 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 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 let preview = Preview::all();
581 assert_eq!(preview.to_string(), "enabled");
582
583 let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
585 assert_eq!(preview.to_string(), "python-install-default");
586
587 let preview = Preview::new(&[PreviewFeature::PythonUpgrade, PreviewFeature::Pylock]);
589 assert_eq!(preview.to_string(), "python-upgrade,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::PythonUpgrade.as_str(), "python-upgrade");
599 assert_eq!(PreviewFeature::JsonOutput.as_str(), "json-output");
600 assert_eq!(PreviewFeature::Pylock.as_str(), "pylock");
601 assert_eq!(
602 PreviewFeature::ToolInstallLocks.as_str(),
603 "tool-install-locks"
604 );
605 assert_eq!(PreviewFeature::AddBounds.as_str(), "add-bounds");
606 assert_eq!(
607 PreviewFeature::PackageConflicts.as_str(),
608 "package-conflicts"
609 );
610 assert_eq!(
611 PreviewFeature::ExtraBuildDependencies.as_str(),
612 "extra-build-dependencies"
613 );
614 assert_eq!(
615 PreviewFeature::DetectModuleConflicts.as_str(),
616 "detect-module-conflicts"
617 );
618 assert_eq!(PreviewFeature::Format.as_str(), "format-command");
619 assert_eq!(PreviewFeature::NativeAuth.as_str(), "native-auth");
620 assert_eq!(PreviewFeature::S3Endpoint.as_str(), "s3-endpoint");
621 assert_eq!(PreviewFeature::CacheSize.as_str(), "cache-size");
622 assert_eq!(
623 PreviewFeature::InitProjectFlag.as_str(),
624 "init-project-flag"
625 );
626 assert_eq!(
627 PreviewFeature::WorkspaceMetadata.as_str(),
628 "workspace-metadata"
629 );
630 assert_eq!(PreviewFeature::WorkspaceDir.as_str(), "workspace-dir");
631 assert_eq!(PreviewFeature::WorkspaceList.as_str(), "workspace-list");
632 assert_eq!(PreviewFeature::SbomExport.as_str(), "sbom-export");
633 assert_eq!(PreviewFeature::AuthHelper.as_str(), "auth-helper");
634 assert_eq!(PreviewFeature::DirectPublish.as_str(), "direct-publish");
635 assert_eq!(
636 PreviewFeature::TargetWorkspaceDiscovery.as_str(),
637 "target-workspace-discovery"
638 );
639 assert_eq!(PreviewFeature::MetadataJson.as_str(), "metadata-json");
640 assert_eq!(PreviewFeature::GcsEndpoint.as_str(), "gcs-endpoint");
641 assert_eq!(PreviewFeature::AdjustUlimit.as_str(), "adjust-ulimit");
642 assert_eq!(
643 PreviewFeature::SpecialCondaEnvNames.as_str(),
644 "special-conda-env-names"
645 );
646 assert_eq!(
647 PreviewFeature::RelocatableEnvsDefault.as_str(),
648 "relocatable-envs-default"
649 );
650 assert_eq!(
651 PreviewFeature::PublishRequireNormalized.as_str(),
652 "publish-require-normalized"
653 );
654 assert_eq!(
655 PreviewFeature::ProjectDirectoryMustExist.as_str(),
656 "project-directory-must-exist"
657 );
658 assert_eq!(
659 PreviewFeature::IndexExcludeNewer.as_str(),
660 "index-exclude-newer"
661 );
662 assert_eq!(PreviewFeature::AzureEndpoint.as_str(), "azure-endpoint");
663 assert_eq!(
664 PreviewFeature::TomlBackwardsCompatibility.as_str(),
665 "toml-backwards-compatibility"
666 );
667 assert_eq!(PreviewFeature::MalwareCheck.as_str(), "malware-check");
668 assert_eq!(PreviewFeature::VenvSafeClear.as_str(), "venv-safe-clear");
669 assert_eq!(PreviewFeature::Audit.as_str(), "audit-command");
670 assert_eq!(PreviewFeature::Check.as_str(), "check-command");
671 assert_eq!(
672 PreviewFeature::CentralizedProjectEnvs.as_str(),
673 "centralized-project-envs"
674 );
675 assert_eq!(
676 PreviewFeature::WorkspaceListScripts.as_str(),
677 "workspace-list-scripts"
678 );
679 assert_eq!(
680 PreviewFeature::NoDistutilsPatch.as_str(),
681 "no-distutils-patch"
682 );
683 assert_eq!(
684 PreviewFeature::IndexHashAlgorithm.as_str(),
685 "index-hash-algorithm"
686 );
687 }
688
689 #[test]
690 fn test_global_preview() {
691 {
692 let _guard =
693 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
694 assert!(!is_enabled(PreviewFeature::InitProjectFlag));
695 assert!(is_enabled(PreviewFeature::Pylock));
696 assert!(is_enabled(PreviewFeature::WorkspaceMetadata));
697 assert!(!is_enabled(PreviewFeature::AuthHelper));
698 }
699 {
700 let _guard =
701 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
702 assert!(is_enabled(PreviewFeature::InitProjectFlag));
703 assert!(!is_enabled(PreviewFeature::Pylock));
704 assert!(!is_enabled(PreviewFeature::WorkspaceMetadata));
705 assert!(is_enabled(PreviewFeature::AuthHelper));
706 }
707 }
708
709 #[test]
710 #[should_panic(
711 expected = "Additional calls to `uv_preview::test::with_features` are not allowed while holding a `FeaturesGuard`"
712 )]
713 fn test_global_preview_panic_nested() {
714 let _guard =
715 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
716 let _guard2 =
717 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
718 }
719
720 #[test]
721 #[should_panic(expected = "uv_preview::test::with_features")]
722 fn test_global_preview_panic_uninitialized() {
723 let _preview = get();
724 }
725}