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