1use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6use std::env;
7use std::fmt;
8use std::str::FromStr;
9use std::sync::{OnceLock, RwLock};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum Feature {
16 #[serde(alias = "sign_separate")]
19 SignSeparate,
20
21 #[serde(alias = "renames_r4_r6")]
23 RenamesR4R6,
24
25 #[serde(alias = "comp_1")]
27 Comp1,
28
29 #[serde(alias = "comp_2")]
31 Comp2,
32
33 #[serde(alias = "audit_system")]
36 AuditSystem,
37
38 #[serde(alias = "sox_compliance")]
40 SoxCompliance,
41
42 #[serde(alias = "hipaa_compliance")]
44 HipaaCompliance,
45
46 #[serde(alias = "gdpr_compliance")]
48 GdprCompliance,
49
50 #[serde(alias = "pci_dss_compliance")]
52 PciDssCompliance,
53
54 #[serde(alias = "security_monitoring")]
56 SecurityMonitoring,
57
58 #[serde(alias = "advanced_optimization")]
61 AdvancedOptimization,
62
63 #[serde(alias = "lru_cache")]
65 LruCache,
66
67 #[serde(alias = "parallel_decode")]
69 ParallelDecode,
70
71 #[serde(alias = "zero_copy")]
73 ZeroCopy,
74
75 #[serde(alias = "verbose_logging")]
78 VerboseLogging,
79
80 #[serde(alias = "diagnostic_output")]
82 DiagnosticOutput,
83
84 #[serde(alias = "profiling")]
86 Profiling,
87
88 #[serde(alias = "memory_tracking")]
90 MemoryTracking,
91
92 #[serde(alias = "mutation_testing")]
95 MutationTesting,
96
97 #[serde(alias = "fuzzing_integration")]
99 FuzzingIntegration,
100
101 #[serde(alias = "coverage_instrumentation")]
103 CoverageInstrumentation,
104
105 #[serde(alias = "property_based_testing")]
107 PropertyBasedTesting,
108}
109
110impl Feature {
111 #[inline]
113 #[must_use]
114 pub const fn category(self) -> FeatureCategory {
115 match self {
116 Feature::SignSeparate | Feature::RenamesR4R6 | Feature::Comp1 | Feature::Comp2 => {
117 FeatureCategory::Experimental
118 }
119 Feature::AuditSystem
120 | Feature::SoxCompliance
121 | Feature::HipaaCompliance
122 | Feature::GdprCompliance
123 | Feature::PciDssCompliance
124 | Feature::SecurityMonitoring => FeatureCategory::Enterprise,
125 Feature::AdvancedOptimization
126 | Feature::LruCache
127 | Feature::ParallelDecode
128 | Feature::ZeroCopy => FeatureCategory::Performance,
129 Feature::VerboseLogging
130 | Feature::DiagnosticOutput
131 | Feature::Profiling
132 | Feature::MemoryTracking => FeatureCategory::Debug,
133 Feature::MutationTesting
134 | Feature::FuzzingIntegration
135 | Feature::CoverageInstrumentation
136 | Feature::PropertyBasedTesting => FeatureCategory::Testing,
137 }
138 }
139
140 #[inline]
142 #[must_use]
143 pub const fn default_enabled(self) -> bool {
144 match self {
145 Feature::SignSeparate | Feature::Comp1 | Feature::Comp2 | Feature::LruCache => true,
146 Feature::RenamesR4R6
147 | Feature::AuditSystem
148 | Feature::SoxCompliance
149 | Feature::HipaaCompliance
150 | Feature::GdprCompliance
151 | Feature::PciDssCompliance
152 | Feature::SecurityMonitoring
153 | Feature::AdvancedOptimization
154 | Feature::ParallelDecode
155 | Feature::ZeroCopy
156 | Feature::VerboseLogging
157 | Feature::DiagnosticOutput
158 | Feature::Profiling
159 | Feature::MemoryTracking
160 | Feature::MutationTesting
161 | Feature::FuzzingIntegration
162 | Feature::CoverageInstrumentation
163 | Feature::PropertyBasedTesting => false,
164 }
165 }
166
167 #[inline]
169 #[must_use]
170 pub fn env_var_name(self) -> String {
171 format!("COPYBOOK_FF_{}", self.to_string().to_uppercase())
172 }
173
174 #[inline]
176 #[must_use]
177 pub const fn description(self) -> &'static str {
178 match self {
179 Feature::SignSeparate => "Enable SIGN SEPARATE clause support",
180 Feature::RenamesR4R6 => "Enable RENAMES R4-R6 advanced scenarios",
181 Feature::Comp1 => "Enable COMP-1 (single precision floating point) support",
182 Feature::Comp2 => "Enable COMP-2 (double precision floating point) support",
183 Feature::AuditSystem => "Enable audit system for compliance tracking",
184 Feature::SoxCompliance => "Enable SOX compliance validation",
185 Feature::HipaaCompliance => "Enable HIPAA compliance validation",
186 Feature::GdprCompliance => "Enable GDPR compliance validation",
187 Feature::PciDssCompliance => "Enable PCI DSS compliance validation",
188 Feature::SecurityMonitoring => "Enable security monitoring integration",
189 Feature::AdvancedOptimization => {
190 "Enable advanced optimization mode (SIMD, vectorization)"
191 }
192 Feature::LruCache => "Enable LRU cache for parsed copybooks",
193 Feature::ParallelDecode => "Enable parallel decoding for large files",
194 Feature::ZeroCopy => "Enable zero-copy parsing where possible",
195 Feature::VerboseLogging => "Enable verbose logging with detailed diagnostics",
196 Feature::DiagnosticOutput => "Enable diagnostic output for troubleshooting",
197 Feature::Profiling => "Enable CPU profiling hooks",
198 Feature::MemoryTracking => "Enable memory usage tracking",
199 Feature::MutationTesting => "Enable mutation testing hooks",
200 Feature::FuzzingIntegration => "Enable fuzzing integration points",
201 Feature::CoverageInstrumentation => "Enable test coverage instrumentation",
202 Feature::PropertyBasedTesting => "Enable property-based testing integration",
203 }
204 }
205}
206
207impl fmt::Display for Feature {
208 #[inline]
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 let s = match self {
211 Feature::SignSeparate => "sign_separate",
212 Feature::RenamesR4R6 => "renames_r4_r6",
213 Feature::Comp1 => "comp_1",
214 Feature::Comp2 => "comp_2",
215 Feature::AuditSystem => "audit_system",
216 Feature::SoxCompliance => "sox_compliance",
217 Feature::HipaaCompliance => "hipaa_compliance",
218 Feature::GdprCompliance => "gdpr_compliance",
219 Feature::PciDssCompliance => "pci_dss_compliance",
220 Feature::SecurityMonitoring => "security_monitoring",
221 Feature::AdvancedOptimization => "advanced_optimization",
222 Feature::LruCache => "lru_cache",
223 Feature::ParallelDecode => "parallel_decode",
224 Feature::ZeroCopy => "zero_copy",
225 Feature::VerboseLogging => "verbose_logging",
226 Feature::DiagnosticOutput => "diagnostic_output",
227 Feature::Profiling => "profiling",
228 Feature::MemoryTracking => "memory_tracking",
229 Feature::MutationTesting => "mutation_testing",
230 Feature::FuzzingIntegration => "fuzzing_integration",
231 Feature::CoverageInstrumentation => "coverage_instrumentation",
232 Feature::PropertyBasedTesting => "property_based_testing",
233 };
234 write!(f, "{s}")
235 }
236}
237
238impl FromStr for Feature {
239 type Err = String;
240
241 #[inline]
242 fn from_str(s: &str) -> Result<Self, Self::Err> {
243 match s.to_lowercase().as_str() {
244 "sign_separate" => Ok(Self::SignSeparate),
245 "renames_r4_r6" => Ok(Self::RenamesR4R6),
246 "comp_1" => Ok(Self::Comp1),
247 "comp_2" => Ok(Self::Comp2),
248 "audit_system" => Ok(Self::AuditSystem),
249 "sox_compliance" => Ok(Self::SoxCompliance),
250 "hipaa_compliance" => Ok(Self::HipaaCompliance),
251 "gdpr_compliance" => Ok(Self::GdprCompliance),
252 "pci_dss_compliance" => Ok(Self::PciDssCompliance),
253 "security_monitoring" => Ok(Self::SecurityMonitoring),
254 "advanced_optimization" => Ok(Self::AdvancedOptimization),
255 "lru_cache" => Ok(Self::LruCache),
256 "parallel_decode" => Ok(Self::ParallelDecode),
257 "zero_copy" => Ok(Self::ZeroCopy),
258 "verbose_logging" => Ok(Self::VerboseLogging),
259 "diagnostic_output" => Ok(Self::DiagnosticOutput),
260 "profiling" => Ok(Self::Profiling),
261 "memory_tracking" => Ok(Self::MemoryTracking),
262 "mutation_testing" => Ok(Self::MutationTesting),
263 "fuzzing_integration" => Ok(Self::FuzzingIntegration),
264 "coverage_instrumentation" => Ok(Self::CoverageInstrumentation),
265 "property_based_testing" => Ok(Self::PropertyBasedTesting),
266 _ => Err(format!("Unknown feature flag: '{s}'")),
267 }
268 }
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case")]
274#[non_exhaustive]
275pub enum FeatureCategory {
276 Experimental,
278 Enterprise,
280 Performance,
282 Debug,
284 Testing,
286}
287
288impl fmt::Display for FeatureCategory {
289 #[inline]
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 FeatureCategory::Experimental => write!(f, "experimental"),
293 FeatureCategory::Enterprise => write!(f, "enterprise"),
294 FeatureCategory::Performance => write!(f, "performance"),
295 FeatureCategory::Debug => write!(f, "debug"),
296 FeatureCategory::Testing => write!(f, "testing"),
297 }
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
303#[serde(rename_all = "snake_case")]
304#[non_exhaustive]
305pub enum FeatureLifecycle {
306 Experimental,
308 Stable,
310 Deprecated,
312}
313
314impl fmt::Display for FeatureLifecycle {
315 #[inline]
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 match self {
318 FeatureLifecycle::Experimental => write!(f, "experimental"),
319 FeatureLifecycle::Stable => write!(f, "stable"),
320 FeatureLifecycle::Deprecated => write!(f, "deprecated"),
321 }
322 }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct FeatureFlags {
328 enabled: HashSet<Feature>,
329}
330
331impl Default for FeatureFlags {
332 #[inline]
333 fn default() -> Self {
334 let mut flags = Self {
335 enabled: HashSet::new(),
336 };
337
338 for feature in all_features() {
339 if feature.default_enabled() {
340 flags.enabled.insert(feature);
341 }
342 }
343
344 flags
345 }
346}
347
348impl FeatureFlags {
349 #[inline]
351 #[must_use]
352 pub fn global() -> &'static Self {
353 GLOBAL_FLAGS.get_or_init(Self::from_env)
354 }
355
356 #[inline]
358 pub fn set_global(flags: Self) {
359 let _ = GLOBAL_FLAGS.set(flags);
360 }
361
362 #[inline]
364 #[must_use]
365 pub fn from_env() -> Self {
366 let mut flags = Self::default();
367
368 for (key, value) in env::vars() {
369 if let Some(feature_name) = key.strip_prefix("COPYBOOK_FF_")
370 && let Ok(feature) = Feature::from_str(feature_name)
371 {
372 let enabled = matches!(
373 value.to_lowercase().as_str(),
374 "1" | "true" | "yes" | "on" | "enabled"
375 );
376 if enabled {
377 flags.enabled.insert(feature);
378 } else {
379 flags.enabled.remove(&feature);
380 }
381 }
382 }
383
384 flags
385 }
386
387 #[inline]
389 #[must_use]
390 pub fn is_enabled(&self, feature: Feature) -> bool {
391 self.enabled.contains(&feature)
392 }
393
394 #[inline]
396 pub fn enable(&mut self, feature: Feature) {
397 self.enabled.insert(feature);
398 }
399
400 #[inline]
402 pub fn disable(&mut self, feature: Feature) {
403 self.enabled.remove(&feature);
404 }
405
406 #[inline]
408 pub fn toggle(&mut self, feature: Feature) {
409 if self.enabled.contains(&feature) {
410 self.enabled.remove(&feature);
411 } else {
412 self.enabled.insert(feature);
413 }
414 }
415
416 #[inline]
418 pub fn enabled_features(&self) -> impl Iterator<Item = &Feature> {
419 self.enabled.iter()
420 }
421
422 #[inline]
424 #[must_use]
425 pub fn enabled_in_category(&self, category: FeatureCategory) -> Vec<Feature> {
426 self.enabled
427 .iter()
428 .filter(|f| f.category() == category)
429 .copied()
430 .collect()
431 }
432
433 #[inline]
435 #[must_use]
436 pub fn features_in_category(category: FeatureCategory) -> Vec<Feature> {
437 all_features()
438 .into_iter()
439 .filter(|f| f.category() == category)
440 .collect()
441 }
442
443 #[inline]
445 #[must_use]
446 pub fn builder() -> FeatureFlagsBuilder {
447 FeatureFlagsBuilder::default()
448 }
449}
450
451#[derive(Debug, Clone, Default)]
453pub struct FeatureFlagsBuilder {
454 flags: FeatureFlags,
455}
456
457impl FeatureFlagsBuilder {
458 #[inline]
460 #[must_use]
461 pub fn enable(mut self, feature: Feature) -> Self {
462 self.flags.enable(feature);
463 self
464 }
465
466 #[inline]
468 #[must_use]
469 pub fn disable(mut self, feature: Feature) -> Self {
470 self.flags.disable(feature);
471 self
472 }
473
474 #[inline]
476 #[must_use]
477 pub fn enable_category(mut self, category: FeatureCategory) -> Self {
478 for feature in FeatureFlags::features_in_category(category) {
479 self.flags.enable(feature);
480 }
481 self
482 }
483
484 #[inline]
486 #[must_use]
487 pub fn disable_category(mut self, category: FeatureCategory) -> Self {
488 for feature in FeatureFlags::features_in_category(category) {
489 self.flags.disable(feature);
490 }
491 self
492 }
493
494 #[inline]
496 #[must_use]
497 pub fn build(self) -> FeatureFlags {
498 self.flags
499 }
500}
501
502static GLOBAL_FLAGS: OnceLock<FeatureFlags> = OnceLock::new();
503
504#[derive(Debug)]
506pub struct FeatureFlagsHandle {
507 flags: RwLock<FeatureFlags>,
508}
509
510impl Default for FeatureFlagsHandle {
511 #[inline]
512 fn default() -> Self {
513 Self {
514 flags: RwLock::new(FeatureFlags::from_env()),
515 }
516 }
517}
518
519impl FeatureFlagsHandle {
520 #[inline]
522 #[must_use]
523 pub fn new() -> Self {
524 Self::default()
525 }
526
527 #[inline]
529 #[must_use]
530 pub fn is_enabled(&self, feature: Feature) -> bool {
531 self.flags
532 .read()
533 .map(|flags| flags.is_enabled(feature))
534 .unwrap_or(false)
535 }
536
537 #[inline]
539 pub fn enable(&self, feature: Feature) {
540 if let Ok(mut flags) = self.flags.write() {
541 flags.enable(feature);
542 }
543 }
544
545 #[inline]
547 pub fn disable(&self, feature: Feature) {
548 if let Ok(mut flags) = self.flags.write() {
549 flags.disable(feature);
550 }
551 }
552
553 #[inline]
555 pub fn toggle(&self, feature: Feature) {
556 if let Ok(mut flags) = self.flags.write() {
557 flags.toggle(feature);
558 }
559 }
560
561 #[inline]
563 #[must_use]
564 pub fn snapshot(&self) -> FeatureFlags {
565 self.flags
566 .read()
567 .map(|flags| flags.clone())
568 .unwrap_or_default()
569 }
570}
571
572impl Clone for FeatureFlagsHandle {
573 #[inline]
574 fn clone(&self) -> Self {
575 Self {
576 flags: RwLock::new(self.snapshot()),
577 }
578 }
579}
580
581#[inline]
583#[must_use]
584pub fn all_features() -> Vec<Feature> {
585 vec![
586 Feature::SignSeparate,
587 Feature::RenamesR4R6,
588 Feature::Comp1,
589 Feature::Comp2,
590 Feature::AuditSystem,
591 Feature::SoxCompliance,
592 Feature::HipaaCompliance,
593 Feature::GdprCompliance,
594 Feature::PciDssCompliance,
595 Feature::SecurityMonitoring,
596 Feature::AdvancedOptimization,
597 Feature::LruCache,
598 Feature::ParallelDecode,
599 Feature::ZeroCopy,
600 Feature::VerboseLogging,
601 Feature::DiagnosticOutput,
602 Feature::Profiling,
603 Feature::MemoryTracking,
604 Feature::MutationTesting,
605 Feature::FuzzingIntegration,
606 Feature::CoverageInstrumentation,
607 Feature::PropertyBasedTesting,
608 ]
609}
610
611#[cfg(test)]
612#[allow(clippy::expect_used)]
613#[allow(clippy::unwrap_used)]
614mod tests {
615 use super::*;
616
617 #[test]
618 fn test_feature_display() {
619 assert_eq!(Feature::SignSeparate.to_string(), "sign_separate");
620 assert_eq!(Feature::LruCache.to_string(), "lru_cache");
621 }
622
623 #[test]
624 fn test_feature_from_str() {
625 assert_eq!(
626 Feature::from_str("sign_separate").unwrap(),
627 Feature::SignSeparate
628 );
629 assert_eq!(Feature::from_str("LRU_CACHE").unwrap(), Feature::LruCache);
630 assert!(Feature::from_str("unknown_feature").is_err());
631 }
632
633 #[test]
634 fn test_feature_category() {
635 assert_eq!(
636 Feature::SignSeparate.category(),
637 FeatureCategory::Experimental
638 );
639 assert_eq!(Feature::AuditSystem.category(), FeatureCategory::Enterprise);
640 assert_eq!(Feature::LruCache.category(), FeatureCategory::Performance);
641 assert_eq!(Feature::VerboseLogging.category(), FeatureCategory::Debug);
642 assert_eq!(
643 Feature::MutationTesting.category(),
644 FeatureCategory::Testing
645 );
646 }
647
648 #[test]
649 fn test_default_enabled() {
650 assert!(Feature::SignSeparate.default_enabled());
651 assert!(Feature::Comp1.default_enabled());
652 assert!(Feature::Comp2.default_enabled());
653 assert!(Feature::LruCache.default_enabled());
654 assert!(!Feature::VerboseLogging.default_enabled());
655 }
656
657 #[test]
658 fn test_feature_flags_default() {
659 let flags = FeatureFlags::default();
660 assert!(flags.is_enabled(Feature::LruCache));
661 assert!(flags.is_enabled(Feature::SignSeparate));
662 assert!(flags.is_enabled(Feature::Comp1));
663 assert!(flags.is_enabled(Feature::Comp2));
664 }
665
666 #[test]
667 fn test_feature_flags_enable_disable() {
668 let mut flags = FeatureFlags::default();
669 flags.enable(Feature::SignSeparate);
670 assert!(flags.is_enabled(Feature::SignSeparate));
671 flags.disable(Feature::SignSeparate);
672 assert!(!flags.is_enabled(Feature::SignSeparate));
673 }
674
675 #[test]
676 fn test_feature_flags_toggle() {
677 let mut flags = FeatureFlags::default();
678 flags.toggle(Feature::LruCache);
679 assert!(!flags.is_enabled(Feature::LruCache));
680 flags.toggle(Feature::LruCache);
681 assert!(flags.is_enabled(Feature::LruCache));
682 }
683
684 #[test]
685 fn test_feature_flags_builder() {
686 let flags = FeatureFlags::builder()
687 .enable(Feature::SignSeparate)
688 .disable(Feature::LruCache)
689 .build();
690 assert!(flags.is_enabled(Feature::SignSeparate));
691 assert!(!flags.is_enabled(Feature::LruCache));
692 }
693
694 #[test]
695 fn test_feature_flags_enable_category() {
696 let flags = FeatureFlags::builder()
697 .enable_category(FeatureCategory::Experimental)
698 .build();
699 assert!(flags.is_enabled(Feature::SignSeparate));
700 assert!(flags.is_enabled(Feature::RenamesR4R6));
701 assert!(flags.is_enabled(Feature::Comp1));
702 assert!(flags.is_enabled(Feature::Comp2));
703 }
704
705 #[test]
706 fn test_feature_flags_handle() {
707 let handle = FeatureFlagsHandle::new();
708 handle.enable(Feature::SignSeparate);
709 assert!(handle.is_enabled(Feature::SignSeparate));
710 handle.disable(Feature::SignSeparate);
711 assert!(!handle.is_enabled(Feature::SignSeparate));
712 handle.enable(Feature::SignSeparate);
713 assert!(handle.is_enabled(Feature::SignSeparate));
714 }
715
716 #[test]
717 fn test_all_features() {
718 let features = all_features();
719 assert!(features.contains(&Feature::SignSeparate));
720 assert!(features.contains(&Feature::LruCache));
721 assert!(features.contains(&Feature::VerboseLogging));
722 }
723
724 #[test]
725 fn test_enabled_in_category() {
726 let mut flags = FeatureFlags::default();
727 flags.enable(Feature::RenamesR4R6);
728 let experimental = flags.enabled_in_category(FeatureCategory::Experimental);
729 assert_eq!(experimental.len(), 4);
730 assert!(experimental.contains(&Feature::SignSeparate));
731 assert!(experimental.contains(&Feature::Comp1));
732 assert!(experimental.contains(&Feature::Comp2));
733 assert!(experimental.contains(&Feature::RenamesR4R6));
734 }
735
736 #[test]
737 fn test_env_var_name() {
738 assert_eq!(
739 Feature::SignSeparate.env_var_name(),
740 "COPYBOOK_FF_SIGN_SEPARATE"
741 );
742 assert_eq!(Feature::LruCache.env_var_name(), "COPYBOOK_FF_LRU_CACHE");
743 }
744
745 #[test]
746 fn test_feature_serde_json_roundtrip() {
747 let feature = Feature::ParallelDecode;
748 let json = serde_json::to_string(&feature).unwrap();
749 let back: Feature = serde_json::from_str(&json).unwrap();
750 assert_eq!(back, feature);
751 }
752
753 #[test]
754 fn test_feature_category_serde_roundtrip() {
755 let cat = FeatureCategory::Enterprise;
756 let json = serde_json::to_string(&cat).unwrap();
757 let back: FeatureCategory = serde_json::from_str(&json).unwrap();
758 assert_eq!(back, cat);
759 }
760
761 #[test]
762 fn test_feature_lifecycle_display() {
763 assert_eq!(FeatureLifecycle::Experimental.to_string(), "experimental");
764 assert_eq!(FeatureLifecycle::Stable.to_string(), "stable");
765 assert_eq!(FeatureLifecycle::Deprecated.to_string(), "deprecated");
766 }
767
768 #[test]
769 fn test_feature_category_display() {
770 assert_eq!(FeatureCategory::Experimental.to_string(), "experimental");
771 assert_eq!(FeatureCategory::Enterprise.to_string(), "enterprise");
772 assert_eq!(FeatureCategory::Performance.to_string(), "performance");
773 assert_eq!(FeatureCategory::Debug.to_string(), "debug");
774 assert_eq!(FeatureCategory::Testing.to_string(), "testing");
775 }
776
777 #[test]
778 fn test_feature_flags_all_disabled() {
779 let mut flags = FeatureFlags::default();
780 for feature in all_features() {
781 flags.disable(feature);
782 }
783 for feature in all_features() {
784 assert!(!flags.is_enabled(feature), "{feature} should be disabled");
785 }
786 }
787
788 #[test]
789 fn test_feature_flags_all_enabled() {
790 let mut flags = FeatureFlags::default();
791 for feature in all_features() {
792 flags.enable(feature);
793 }
794 for feature in all_features() {
795 assert!(flags.is_enabled(feature), "{feature} should be enabled");
796 }
797 }
798
799 #[test]
800 fn test_builder_disable_category() {
801 let flags = FeatureFlags::builder()
802 .enable_category(FeatureCategory::Debug)
803 .disable_category(FeatureCategory::Debug)
804 .build();
805 let debug_features = flags.enabled_in_category(FeatureCategory::Debug);
806 assert!(debug_features.is_empty());
807 }
808
809 #[test]
810 fn test_handle_toggle_and_snapshot() {
811 let handle = FeatureFlagsHandle::new();
812 let initially_enabled = handle.is_enabled(Feature::LruCache);
813 handle.toggle(Feature::LruCache);
814 assert_ne!(handle.is_enabled(Feature::LruCache), initially_enabled);
815 let snap = handle.snapshot();
816 assert_ne!(snap.is_enabled(Feature::LruCache), initially_enabled);
817 }
818
819 #[test]
820 fn test_handle_clone_is_independent() {
821 let handle = FeatureFlagsHandle::new();
822 handle.enable(Feature::Profiling);
823 let cloned = handle.clone();
824 handle.disable(Feature::Profiling);
825 assert!(cloned.is_enabled(Feature::Profiling));
826 assert!(!handle.is_enabled(Feature::Profiling));
827 }
828
829 #[test]
830 fn test_features_in_category_counts() {
831 let experimental = FeatureFlags::features_in_category(FeatureCategory::Experimental);
832 assert_eq!(experimental.len(), 4);
833 let enterprise = FeatureFlags::features_in_category(FeatureCategory::Enterprise);
834 assert_eq!(enterprise.len(), 6);
835 let performance = FeatureFlags::features_in_category(FeatureCategory::Performance);
836 assert_eq!(performance.len(), 4);
837 let debug = FeatureFlags::features_in_category(FeatureCategory::Debug);
838 assert_eq!(debug.len(), 4);
839 let testing = FeatureFlags::features_in_category(FeatureCategory::Testing);
840 assert_eq!(testing.len(), 4);
841 }
842
843 #[test]
844 fn test_enabled_features_iterator_count() {
845 let flags = FeatureFlags::default();
846 let count = flags.enabled_features().count();
847 assert_eq!(count, 4);
849 }
850
851 #[test]
852 fn test_description_nonempty_for_all_features() {
853 for feature in all_features() {
854 assert!(
855 !feature.description().is_empty(),
856 "{feature} has empty description"
857 );
858 }
859 }
860
861 #[test]
862 fn test_from_str_unknown_returns_err() {
863 assert!(Feature::from_str("does_not_exist").is_err());
864 assert!(Feature::from_str("").is_err());
865 }
866
867 #[test]
868 fn test_feature_flags_serde_json_roundtrip() {
869 let flags = FeatureFlags::builder()
870 .enable(Feature::Profiling)
871 .disable(Feature::LruCache)
872 .build();
873 let json = serde_json::to_string(&flags).unwrap();
874 let back: FeatureFlags = serde_json::from_str(&json).unwrap();
875 assert!(back.is_enabled(Feature::Profiling));
876 assert!(!back.is_enabled(Feature::LruCache));
877 }
878}