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}
267
268impl PreviewFeature {
269 fn as_str(self) -> &'static str {
271 match self {
272 Self::PythonInstallDefault => "python-install-default",
273 Self::PythonUpgrade => "python-upgrade",
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 }
312 }
313}
314
315impl Display for PreviewFeature {
316 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
317 write!(f, "{}", self.as_str())
318 }
319}
320
321#[derive(Debug, Error, Clone)]
322#[error("Unknown feature flag")]
323pub struct PreviewFeatureParseError;
324
325impl FromStr for PreviewFeature {
326 type Err = PreviewFeatureParseError;
327
328 fn from_str(s: &str) -> Result<Self, Self::Err> {
329 Ok(match s {
330 "python-install-default" => Self::PythonInstallDefault,
331 "python-upgrade" => Self::PythonUpgrade,
332 "json-output" => Self::JsonOutput,
333 "pylock" => Self::Pylock,
334 "add-bounds" => Self::AddBounds,
335 "package-conflicts" => Self::PackageConflicts,
336 "extra-build-dependencies" => Self::ExtraBuildDependencies,
337 "detect-module-conflicts" => Self::DetectModuleConflicts,
338 "format" | "format-command" => Self::Format,
339 "native-auth" => Self::NativeAuth,
340 "s3-endpoint" => Self::S3Endpoint,
341 "gcs-endpoint" => Self::GcsEndpoint,
342 "cache-size" => Self::CacheSize,
343 "init-project-flag" => Self::InitProjectFlag,
344 "workspace-metadata" => Self::WorkspaceMetadata,
345 "workspace-dir" => Self::WorkspaceDir,
346 "workspace-list" => Self::WorkspaceList,
347 "sbom-export" => Self::SbomExport,
348 "auth-helper" => Self::AuthHelper,
349 "direct-publish" => Self::DirectPublish,
350 "target-workspace-discovery" => Self::TargetWorkspaceDiscovery,
351 "metadata-json" => Self::MetadataJson,
352 "adjust-ulimit" => Self::AdjustUlimit,
353 "special-conda-env-names" => Self::SpecialCondaEnvNames,
354 "relocatable-envs-default" => Self::RelocatableEnvsDefault,
355 "publish-require-normalized" => Self::PublishRequireNormalized,
356 "audit" | "audit-command" => Self::Audit,
357 "project-directory-must-exist" => Self::ProjectDirectoryMustExist,
358 "index-exclude-newer" => Self::IndexExcludeNewer,
359 "azure-endpoint" => Self::AzureEndpoint,
360 "toml-backwards-compatibility" => Self::TomlBackwardsCompatibility,
361 "malware-check" => Self::MalwareCheck,
362 "venv-safe-clear" => Self::VenvSafeClear,
363 "check" | "check-command" => Self::Check,
364 "packaged-init" => Self::PackagedInit,
365 "centralized-project-envs" => Self::CentralizedProjectEnvs,
366 "tool-install-locks" => Self::ToolInstallLocks,
367 "workspace-list-scripts" => Self::WorkspaceListScripts,
368 "no-distutils-patch" => Self::NoDistutilsPatch,
369 _ => return Err(PreviewFeatureParseError),
370 })
371 }
372}
373
374#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
375#[error("preview feature name cannot be empty")]
376pub struct EmptyPreviewFeatureNameError;
377
378#[derive(Debug, Clone)]
380pub enum MaybePreviewFeature {
381 Known(PreviewFeature),
382 Unknown(String),
383}
384
385impl FromStr for MaybePreviewFeature {
386 type Err = EmptyPreviewFeatureNameError;
387
388 fn from_str(s: &str) -> Result<Self, Self::Err> {
389 let s = s.trim();
390 if s.is_empty() {
391 return Err(EmptyPreviewFeatureNameError);
392 }
393
394 Ok(match PreviewFeature::from_str(s) {
395 Ok(feature) => Self::Known(feature),
396 Err(_) => Self::Unknown(s.to_string()),
397 })
398 }
399}
400
401impl<'de> serde::Deserialize<'de> for MaybePreviewFeature {
402 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403 where
404 D: serde::Deserializer<'de>,
405 {
406 let name: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?;
407 Self::from_str(&name).map_err(serde::de::Error::custom)
408 }
409}
410
411#[cfg(feature = "schemars")]
412impl schemars::JsonSchema for MaybePreviewFeature {
413 fn schema_name() -> Cow<'static, str> {
414 Cow::Borrowed("PreviewFeature")
415 }
416
417 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
418 let choices: Vec<&str> = BitFlags::<PreviewFeature>::all()
421 .iter()
422 .map(PreviewFeature::as_str)
423 .collect();
424 schemars::json_schema!({
425 "type": "string",
426 "anyOf": [
427 {
428 "enum": choices,
429 },
430 {
431 "pattern": "\\S",
432 },
433 ],
434 })
435 }
436}
437
438#[derive(Clone, Copy, PartialEq, Eq, Default)]
439pub struct Preview {
440 flags: BitFlags<PreviewFeature>,
441}
442
443impl Debug for Preview {
444 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
445 let flags: Vec<_> = self.flags.iter().collect();
446 f.debug_struct("Preview").field("flags", &flags).finish()
447 }
448}
449
450impl Preview {
451 #[cfg(any(test, feature = "testing"))]
452 fn new(flags: &[PreviewFeature]) -> Self {
453 Self {
454 flags: flags.iter().copied().fold(BitFlags::empty(), BitOr::bitor),
455 }
456 }
457
458 pub fn all() -> Self {
459 Self {
460 flags: BitFlags::all(),
461 }
462 }
463
464 pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
466 self.flags.contains(flag)
467 }
468
469 pub fn all_enabled(&self) -> bool {
471 self.flags.is_all()
472 }
473
474 pub fn any_enabled(&self) -> bool {
476 !self.flags.is_empty()
477 }
478
479 pub fn from_feature_names<'a>(
481 feature_names: impl IntoIterator<Item = &'a MaybePreviewFeature>,
482 ) -> Self {
483 let mut flags = BitFlags::empty();
484
485 for feature_name in feature_names {
486 match feature_name {
487 MaybePreviewFeature::Known(feature) => flags |= *feature,
488 MaybePreviewFeature::Unknown(feature_name) => {
489 warn_user_once!("Unknown preview feature: `{feature_name}`");
490 }
491 }
492 }
493
494 Self { flags }
495 }
496}
497
498impl Display for Preview {
499 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
500 if self.flags.is_empty() {
501 write!(f, "disabled")
502 } else if self.flags.is_all() {
503 write!(f, "enabled")
504 } else {
505 write!(
506 f,
507 "{}",
508 itertools::join(self.flags.iter().map(PreviewFeature::as_str), ",")
509 )
510 }
511 }
512}
513
514impl FromStr for Preview {
515 type Err = EmptyPreviewFeatureNameError;
516
517 fn from_str(s: &str) -> Result<Self, Self::Err> {
518 let feature_names = s
519 .split(',')
520 .map(MaybePreviewFeature::from_str)
521 .collect::<Result<Vec<_>, _>>()?;
522
523 Ok(Self::from_feature_names(&feature_names))
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530
531 #[test]
532 fn test_preview_feature_from_str() {
533 let features = PreviewFeature::from_str("python-install-default").unwrap();
534 assert_eq!(features, PreviewFeature::PythonInstallDefault);
535 }
536
537 #[test]
538 fn test_preview_from_str() {
539 let preview = Preview::from_str("python-install-default").unwrap();
541 assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault);
542
543 let preview = Preview::from_str("python-upgrade,json-output").unwrap();
545 assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
546 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
547 assert_eq!(preview.flags.bits().count_ones(), 2);
548
549 let preview = Preview::from_str("tool-install-locks").unwrap();
550 assert!(preview.is_enabled(PreviewFeature::ToolInstallLocks));
551
552 let preview = Preview::from_str("pylock , add-bounds").unwrap();
554 assert!(preview.is_enabled(PreviewFeature::Pylock));
555 assert!(preview.is_enabled(PreviewFeature::AddBounds));
556
557 assert_eq!(Preview::from_str(""), Err(EmptyPreviewFeatureNameError));
559 assert!(Preview::from_str("pylock,").is_err());
560 assert!(Preview::from_str(",pylock").is_err());
561
562 let preview = Preview::from_str("unknown-feature,pylock").unwrap();
564 assert!(preview.is_enabled(PreviewFeature::Pylock));
565 assert_eq!(preview.flags.bits().count_ones(), 1);
566 }
567
568 #[test]
569 fn test_preview_display() {
570 let preview = Preview::default();
572 assert_eq!(preview.to_string(), "disabled");
573 let preview = Preview::new(&[]);
574 assert_eq!(preview.to_string(), "disabled");
575
576 let preview = Preview::all();
578 assert_eq!(preview.to_string(), "enabled");
579
580 let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
582 assert_eq!(preview.to_string(), "python-install-default");
583
584 let preview = Preview::new(&[PreviewFeature::PythonUpgrade, PreviewFeature::Pylock]);
586 assert_eq!(preview.to_string(), "python-upgrade,pylock");
587 }
588
589 #[test]
590 fn test_preview_feature_as_str() {
591 assert_eq!(
592 PreviewFeature::PythonInstallDefault.as_str(),
593 "python-install-default"
594 );
595 assert_eq!(PreviewFeature::PythonUpgrade.as_str(), "python-upgrade");
596 assert_eq!(PreviewFeature::JsonOutput.as_str(), "json-output");
597 assert_eq!(PreviewFeature::Pylock.as_str(), "pylock");
598 assert_eq!(
599 PreviewFeature::ToolInstallLocks.as_str(),
600 "tool-install-locks"
601 );
602 assert_eq!(PreviewFeature::AddBounds.as_str(), "add-bounds");
603 assert_eq!(
604 PreviewFeature::PackageConflicts.as_str(),
605 "package-conflicts"
606 );
607 assert_eq!(
608 PreviewFeature::ExtraBuildDependencies.as_str(),
609 "extra-build-dependencies"
610 );
611 assert_eq!(
612 PreviewFeature::DetectModuleConflicts.as_str(),
613 "detect-module-conflicts"
614 );
615 assert_eq!(PreviewFeature::Format.as_str(), "format-command");
616 assert_eq!(PreviewFeature::NativeAuth.as_str(), "native-auth");
617 assert_eq!(PreviewFeature::S3Endpoint.as_str(), "s3-endpoint");
618 assert_eq!(PreviewFeature::CacheSize.as_str(), "cache-size");
619 assert_eq!(
620 PreviewFeature::InitProjectFlag.as_str(),
621 "init-project-flag"
622 );
623 assert_eq!(
624 PreviewFeature::WorkspaceMetadata.as_str(),
625 "workspace-metadata"
626 );
627 assert_eq!(PreviewFeature::WorkspaceDir.as_str(), "workspace-dir");
628 assert_eq!(PreviewFeature::WorkspaceList.as_str(), "workspace-list");
629 assert_eq!(PreviewFeature::SbomExport.as_str(), "sbom-export");
630 assert_eq!(PreviewFeature::AuthHelper.as_str(), "auth-helper");
631 assert_eq!(PreviewFeature::DirectPublish.as_str(), "direct-publish");
632 assert_eq!(
633 PreviewFeature::TargetWorkspaceDiscovery.as_str(),
634 "target-workspace-discovery"
635 );
636 assert_eq!(PreviewFeature::MetadataJson.as_str(), "metadata-json");
637 assert_eq!(PreviewFeature::GcsEndpoint.as_str(), "gcs-endpoint");
638 assert_eq!(PreviewFeature::AdjustUlimit.as_str(), "adjust-ulimit");
639 assert_eq!(
640 PreviewFeature::SpecialCondaEnvNames.as_str(),
641 "special-conda-env-names"
642 );
643 assert_eq!(
644 PreviewFeature::RelocatableEnvsDefault.as_str(),
645 "relocatable-envs-default"
646 );
647 assert_eq!(
648 PreviewFeature::PublishRequireNormalized.as_str(),
649 "publish-require-normalized"
650 );
651 assert_eq!(
652 PreviewFeature::ProjectDirectoryMustExist.as_str(),
653 "project-directory-must-exist"
654 );
655 assert_eq!(
656 PreviewFeature::IndexExcludeNewer.as_str(),
657 "index-exclude-newer"
658 );
659 assert_eq!(PreviewFeature::AzureEndpoint.as_str(), "azure-endpoint");
660 assert_eq!(
661 PreviewFeature::TomlBackwardsCompatibility.as_str(),
662 "toml-backwards-compatibility"
663 );
664 assert_eq!(PreviewFeature::MalwareCheck.as_str(), "malware-check");
665 assert_eq!(PreviewFeature::VenvSafeClear.as_str(), "venv-safe-clear");
666 assert_eq!(PreviewFeature::Audit.as_str(), "audit-command");
667 assert_eq!(PreviewFeature::Check.as_str(), "check-command");
668 assert_eq!(
669 PreviewFeature::CentralizedProjectEnvs.as_str(),
670 "centralized-project-envs"
671 );
672 assert_eq!(
673 PreviewFeature::WorkspaceListScripts.as_str(),
674 "workspace-list-scripts"
675 );
676 assert_eq!(
677 PreviewFeature::NoDistutilsPatch.as_str(),
678 "no-distutils-patch"
679 );
680 }
681
682 #[test]
683 fn test_global_preview() {
684 {
685 let _guard =
686 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
687 assert!(!is_enabled(PreviewFeature::InitProjectFlag));
688 assert!(is_enabled(PreviewFeature::Pylock));
689 assert!(is_enabled(PreviewFeature::WorkspaceMetadata));
690 assert!(!is_enabled(PreviewFeature::AuthHelper));
691 }
692 {
693 let _guard =
694 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
695 assert!(is_enabled(PreviewFeature::InitProjectFlag));
696 assert!(!is_enabled(PreviewFeature::Pylock));
697 assert!(!is_enabled(PreviewFeature::WorkspaceMetadata));
698 assert!(is_enabled(PreviewFeature::AuthHelper));
699 }
700 }
701
702 #[test]
703 #[should_panic(
704 expected = "Additional calls to `uv_preview::test::with_features` are not allowed while holding a `FeaturesGuard`"
705 )]
706 fn test_global_preview_panic_nested() {
707 let _guard =
708 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
709 let _guard2 =
710 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
711 }
712
713 #[test]
714 #[should_panic(expected = "uv_preview::test::with_features")]
715 fn test_global_preview_panic_uninitialized() {
716 let _preview = get();
717 }
718}