Skip to main content

copybook_contracts/
feature_flags.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Feature flag system contract for copybook-rs.
3
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6use std::env;
7use std::fmt;
8use std::str::FromStr;
9use std::sync::{OnceLock, RwLock};
10
11/// All available feature flags.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum Feature {
16    // ========== Experimental Features ==========
17    /// Enable SIGN SEPARATE clause support (experimental)
18    #[serde(alias = "sign_separate")]
19    SignSeparate,
20
21    /// Enable RENAMES R4-R6 advanced scenarios
22    #[serde(alias = "renames_r4_r6")]
23    RenamesR4R6,
24
25    /// Enable COMP-1 (single precision floating point) support
26    #[serde(alias = "comp_1")]
27    Comp1,
28
29    /// Enable COMP-2 (double precision floating point) support
30    #[serde(alias = "comp_2")]
31    Comp2,
32
33    // ========== Enterprise Features ==========
34    /// Enable audit system for compliance tracking
35    #[serde(alias = "audit_system")]
36    AuditSystem,
37
38    /// Enable SOX compliance validation
39    #[serde(alias = "sox_compliance")]
40    SoxCompliance,
41
42    /// Enable HIPAA compliance validation
43    #[serde(alias = "hipaa_compliance")]
44    HipaaCompliance,
45
46    /// Enable GDPR compliance validation
47    #[serde(alias = "gdpr_compliance")]
48    GdprCompliance,
49
50    /// Enable PCI DSS compliance validation
51    #[serde(alias = "pci_dss_compliance")]
52    PciDssCompliance,
53
54    /// Enable security monitoring integration
55    #[serde(alias = "security_monitoring")]
56    SecurityMonitoring,
57
58    // ========== Performance Features ==========
59    /// Enable advanced optimization mode (SIMD, vectorization)
60    #[serde(alias = "advanced_optimization")]
61    AdvancedOptimization,
62
63    /// Enable LRU cache for parsed copybooks
64    #[serde(alias = "lru_cache")]
65    LruCache,
66
67    /// Enable parallel decoding for large files
68    #[serde(alias = "parallel_decode")]
69    ParallelDecode,
70
71    /// Enable zero-copy parsing where possible
72    #[serde(alias = "zero_copy")]
73    ZeroCopy,
74
75    // ========== Debug Features ==========
76    /// Enable verbose logging with detailed diagnostics
77    #[serde(alias = "verbose_logging")]
78    VerboseLogging,
79
80    /// Enable diagnostic output for troubleshooting
81    #[serde(alias = "diagnostic_output")]
82    DiagnosticOutput,
83
84    /// Enable CPU profiling hooks
85    #[serde(alias = "profiling")]
86    Profiling,
87
88    /// Enable memory usage tracking
89    #[serde(alias = "memory_tracking")]
90    MemoryTracking,
91
92    // ========== Testing Features ==========
93    /// Enable mutation testing hooks
94    #[serde(alias = "mutation_testing")]
95    MutationTesting,
96
97    /// Enable fuzzing integration points
98    #[serde(alias = "fuzzing_integration")]
99    FuzzingIntegration,
100
101    /// Enable test coverage instrumentation
102    #[serde(alias = "coverage_instrumentation")]
103    CoverageInstrumentation,
104
105    /// Enable property-based testing integration
106    #[serde(alias = "property_based_testing")]
107    PropertyBasedTesting,
108}
109
110impl Feature {
111    /// Get the toggle-group category this feature belongs to.
112    ///
113    /// `category()` is a **grouping mechanism** used by `enable_category` /
114    /// `enabled_in_category` to toggle related flags together; it is **not** a
115    /// stability class. In particular, `FeatureCategory::Experimental` names a
116    /// toggle group, not a claim that every member is unstable — the stability
117    /// class of a feature is reported by [`Feature::lifecycle`]. For example,
118    /// `Comp1`/`Comp2` are in the `Experimental` toggle group but have
119    /// `lifecycle() == FeatureLifecycle::Stable` (they are fully supported and
120    /// enabled by default; see `docs/reference/COBOL_SUPPORT_MATRIX.md`).
121    #[inline]
122    #[must_use]
123    pub const fn category(self) -> FeatureCategory {
124        match self {
125            Feature::SignSeparate | Feature::RenamesR4R6 | Feature::Comp1 | Feature::Comp2 => {
126                FeatureCategory::Experimental
127            }
128            Feature::AuditSystem
129            | Feature::SoxCompliance
130            | Feature::HipaaCompliance
131            | Feature::GdprCompliance
132            | Feature::PciDssCompliance
133            | Feature::SecurityMonitoring => FeatureCategory::Enterprise,
134            Feature::AdvancedOptimization
135            | Feature::LruCache
136            | Feature::ParallelDecode
137            | Feature::ZeroCopy => FeatureCategory::Performance,
138            Feature::VerboseLogging
139            | Feature::DiagnosticOutput
140            | Feature::Profiling
141            | Feature::MemoryTracking => FeatureCategory::Debug,
142            Feature::MutationTesting
143            | Feature::FuzzingIntegration
144            | Feature::CoverageInstrumentation
145            | Feature::PropertyBasedTesting => FeatureCategory::Testing,
146        }
147    }
148
149    /// Get the stability lifecycle class of this feature.
150    ///
151    /// This is the authoritative stability signal (distinct from the
152    /// toggle-group [`Feature::category`]). `SignSeparate`, `Comp1`, and
153    /// `Comp2` are promoted COBOL-language features that are fully supported
154    /// and enabled by default, so they report [`FeatureLifecycle::Stable`];
155    /// every other flag (advanced RENAMES R4-R6, enterprise/compliance,
156    /// performance, debug, and testing hooks) is opt-in and reports
157    /// [`FeatureLifecycle::Experimental`].
158    #[inline]
159    #[must_use]
160    pub const fn lifecycle(self) -> FeatureLifecycle {
161        match self {
162            Feature::SignSeparate | Feature::Comp1 | Feature::Comp2 => FeatureLifecycle::Stable,
163            Feature::RenamesR4R6
164            | Feature::AuditSystem
165            | Feature::SoxCompliance
166            | Feature::HipaaCompliance
167            | Feature::GdprCompliance
168            | Feature::PciDssCompliance
169            | Feature::SecurityMonitoring
170            | Feature::AdvancedOptimization
171            | Feature::LruCache
172            | Feature::ParallelDecode
173            | Feature::ZeroCopy
174            | Feature::VerboseLogging
175            | Feature::DiagnosticOutput
176            | Feature::Profiling
177            | Feature::MemoryTracking
178            | Feature::MutationTesting
179            | Feature::FuzzingIntegration
180            | Feature::CoverageInstrumentation
181            | Feature::PropertyBasedTesting => FeatureLifecycle::Experimental,
182        }
183    }
184
185    /// Get the default enabled state for this feature.
186    #[inline]
187    #[must_use]
188    pub const fn default_enabled(self) -> bool {
189        match self {
190            Feature::SignSeparate | Feature::Comp1 | Feature::Comp2 | Feature::LruCache => true,
191            Feature::RenamesR4R6
192            | Feature::AuditSystem
193            | Feature::SoxCompliance
194            | Feature::HipaaCompliance
195            | Feature::GdprCompliance
196            | Feature::PciDssCompliance
197            | Feature::SecurityMonitoring
198            | Feature::AdvancedOptimization
199            | Feature::ParallelDecode
200            | Feature::ZeroCopy
201            | Feature::VerboseLogging
202            | Feature::DiagnosticOutput
203            | Feature::Profiling
204            | Feature::MemoryTracking
205            | Feature::MutationTesting
206            | Feature::FuzzingIntegration
207            | Feature::CoverageInstrumentation
208            | Feature::PropertyBasedTesting => false,
209        }
210    }
211
212    /// Get the environment variable name for this feature.
213    #[inline]
214    #[must_use]
215    pub fn env_var_name(self) -> String {
216        format!("COPYBOOK_FF_{}", self.to_string().to_uppercase())
217    }
218
219    /// Get a human-readable description of this feature.
220    #[inline]
221    #[must_use]
222    pub const fn description(self) -> &'static str {
223        match self {
224            Feature::SignSeparate => "Enable SIGN SEPARATE clause support",
225            Feature::RenamesR4R6 => "Enable RENAMES R4-R6 advanced scenarios",
226            Feature::Comp1 => "Enable COMP-1 (single precision floating point) support",
227            Feature::Comp2 => "Enable COMP-2 (double precision floating point) support",
228            Feature::AuditSystem => "Enable audit system for compliance tracking",
229            Feature::SoxCompliance => "Enable SOX compliance validation",
230            Feature::HipaaCompliance => "Enable HIPAA compliance validation",
231            Feature::GdprCompliance => "Enable GDPR compliance validation",
232            Feature::PciDssCompliance => "Enable PCI DSS compliance validation",
233            Feature::SecurityMonitoring => "Enable security monitoring integration",
234            Feature::AdvancedOptimization => {
235                "Enable advanced optimization mode (SIMD, vectorization)"
236            }
237            Feature::LruCache => "Enable LRU cache for parsed copybooks",
238            Feature::ParallelDecode => "Enable parallel decoding for large files",
239            Feature::ZeroCopy => "Enable zero-copy parsing where possible",
240            Feature::VerboseLogging => "Enable verbose logging with detailed diagnostics",
241            Feature::DiagnosticOutput => "Enable diagnostic output for troubleshooting",
242            Feature::Profiling => "Enable CPU profiling hooks",
243            Feature::MemoryTracking => "Enable memory usage tracking",
244            Feature::MutationTesting => "Enable mutation testing hooks",
245            Feature::FuzzingIntegration => "Enable fuzzing integration points",
246            Feature::CoverageInstrumentation => "Enable test coverage instrumentation",
247            Feature::PropertyBasedTesting => "Enable property-based testing integration",
248        }
249    }
250}
251
252impl fmt::Display for Feature {
253    #[inline]
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        let s = match self {
256            Feature::SignSeparate => "sign_separate",
257            Feature::RenamesR4R6 => "renames_r4_r6",
258            Feature::Comp1 => "comp_1",
259            Feature::Comp2 => "comp_2",
260            Feature::AuditSystem => "audit_system",
261            Feature::SoxCompliance => "sox_compliance",
262            Feature::HipaaCompliance => "hipaa_compliance",
263            Feature::GdprCompliance => "gdpr_compliance",
264            Feature::PciDssCompliance => "pci_dss_compliance",
265            Feature::SecurityMonitoring => "security_monitoring",
266            Feature::AdvancedOptimization => "advanced_optimization",
267            Feature::LruCache => "lru_cache",
268            Feature::ParallelDecode => "parallel_decode",
269            Feature::ZeroCopy => "zero_copy",
270            Feature::VerboseLogging => "verbose_logging",
271            Feature::DiagnosticOutput => "diagnostic_output",
272            Feature::Profiling => "profiling",
273            Feature::MemoryTracking => "memory_tracking",
274            Feature::MutationTesting => "mutation_testing",
275            Feature::FuzzingIntegration => "fuzzing_integration",
276            Feature::CoverageInstrumentation => "coverage_instrumentation",
277            Feature::PropertyBasedTesting => "property_based_testing",
278        };
279        write!(f, "{s}")
280    }
281}
282
283impl FromStr for Feature {
284    type Err = String;
285
286    #[inline]
287    fn from_str(s: &str) -> Result<Self, Self::Err> {
288        match s.to_lowercase().as_str() {
289            "sign_separate" => Ok(Self::SignSeparate),
290            "renames_r4_r6" => Ok(Self::RenamesR4R6),
291            "comp_1" => Ok(Self::Comp1),
292            "comp_2" => Ok(Self::Comp2),
293            "audit_system" => Ok(Self::AuditSystem),
294            "sox_compliance" => Ok(Self::SoxCompliance),
295            "hipaa_compliance" => Ok(Self::HipaaCompliance),
296            "gdpr_compliance" => Ok(Self::GdprCompliance),
297            "pci_dss_compliance" => Ok(Self::PciDssCompliance),
298            "security_monitoring" => Ok(Self::SecurityMonitoring),
299            "advanced_optimization" => Ok(Self::AdvancedOptimization),
300            "lru_cache" => Ok(Self::LruCache),
301            "parallel_decode" => Ok(Self::ParallelDecode),
302            "zero_copy" => Ok(Self::ZeroCopy),
303            "verbose_logging" => Ok(Self::VerboseLogging),
304            "diagnostic_output" => Ok(Self::DiagnosticOutput),
305            "profiling" => Ok(Self::Profiling),
306            "memory_tracking" => Ok(Self::MemoryTracking),
307            "mutation_testing" => Ok(Self::MutationTesting),
308            "fuzzing_integration" => Ok(Self::FuzzingIntegration),
309            "coverage_instrumentation" => Ok(Self::CoverageInstrumentation),
310            "property_based_testing" => Ok(Self::PropertyBasedTesting),
311            _ => Err(format!("Unknown feature flag: '{s}'")),
312        }
313    }
314}
315
316/// Feature category for grouping related features.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
318#[serde(rename_all = "snake_case")]
319#[non_exhaustive]
320pub enum FeatureCategory {
321    /// Pre-release features under active development.
322    Experimental,
323    /// Compliance and audit-related features for regulated environments.
324    Enterprise,
325    /// Optimization features for throughput and memory.
326    Performance,
327    /// Diagnostic and profiling features for development.
328    Debug,
329    /// Testing infrastructure hooks (mutation, fuzzing, coverage).
330    Testing,
331}
332
333impl fmt::Display for FeatureCategory {
334    #[inline]
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        match self {
337            FeatureCategory::Experimental => write!(f, "experimental"),
338            FeatureCategory::Enterprise => write!(f, "enterprise"),
339            FeatureCategory::Performance => write!(f, "performance"),
340            FeatureCategory::Debug => write!(f, "debug"),
341            FeatureCategory::Testing => write!(f, "testing"),
342        }
343    }
344}
345
346/// Feature lifecycle stage.
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
348#[serde(rename_all = "snake_case")]
349#[non_exhaustive]
350pub enum FeatureLifecycle {
351    /// Feature is under active development and may change.
352    Experimental,
353    /// Feature is production-ready with stable API.
354    Stable,
355    /// Feature is scheduled for removal in a future release.
356    Deprecated,
357}
358
359impl fmt::Display for FeatureLifecycle {
360    #[inline]
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        match self {
363            FeatureLifecycle::Experimental => write!(f, "experimental"),
364            FeatureLifecycle::Stable => write!(f, "stable"),
365            FeatureLifecycle::Deprecated => write!(f, "deprecated"),
366        }
367    }
368}
369
370/// Feature flag configuration.
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct FeatureFlags {
373    enabled: HashSet<Feature>,
374}
375
376impl Default for FeatureFlags {
377    #[inline]
378    fn default() -> Self {
379        let mut flags = Self {
380            enabled: HashSet::new(),
381        };
382
383        for feature in all_features() {
384            if feature.default_enabled() {
385                flags.enabled.insert(feature);
386            }
387        }
388
389        flags
390    }
391}
392
393impl FeatureFlags {
394    /// Get the global feature flags instance.
395    #[inline]
396    #[must_use]
397    pub fn global() -> &'static Self {
398        GLOBAL_FLAGS.get_or_init(Self::from_env)
399    }
400
401    /// Set the global feature flags.
402    #[inline]
403    pub fn set_global(flags: Self) {
404        let _ = GLOBAL_FLAGS.set(flags);
405    }
406
407    /// Create feature flags from environment variables.
408    #[inline]
409    #[must_use]
410    pub fn from_env() -> Self {
411        let mut flags = Self::default();
412
413        for (key, value) in env::vars() {
414            if let Some(feature_name) = key.strip_prefix("COPYBOOK_FF_")
415                && let Ok(feature) = Feature::from_str(feature_name)
416            {
417                let enabled = matches!(
418                    value.to_lowercase().as_str(),
419                    "1" | "true" | "yes" | "on" | "enabled"
420                );
421                if enabled {
422                    flags.enabled.insert(feature);
423                } else {
424                    flags.enabled.remove(&feature);
425                }
426            }
427        }
428
429        flags
430    }
431
432    /// Check whether a specific feature is currently enabled.
433    #[inline]
434    #[must_use]
435    pub fn is_enabled(&self, feature: Feature) -> bool {
436        self.enabled.contains(&feature)
437    }
438
439    /// Enable a feature flag.
440    #[inline]
441    pub fn enable(&mut self, feature: Feature) {
442        self.enabled.insert(feature);
443    }
444
445    /// Disable a feature flag.
446    #[inline]
447    pub fn disable(&mut self, feature: Feature) {
448        self.enabled.remove(&feature);
449    }
450
451    /// Toggle a feature flag between enabled and disabled.
452    #[inline]
453    pub fn toggle(&mut self, feature: Feature) {
454        if self.enabled.contains(&feature) {
455            self.enabled.remove(&feature);
456        } else {
457            self.enabled.insert(feature);
458        }
459    }
460
461    /// Iterate over all currently enabled features.
462    #[inline]
463    pub fn enabled_features(&self) -> impl Iterator<Item = &Feature> {
464        self.enabled.iter()
465    }
466
467    /// Return all enabled features belonging to a specific category.
468    #[inline]
469    #[must_use]
470    pub fn enabled_in_category(&self, category: FeatureCategory) -> Vec<Feature> {
471        self.enabled
472            .iter()
473            .filter(|f| f.category() == category)
474            .copied()
475            .collect()
476    }
477
478    /// Return all features (enabled or not) that belong to a category.
479    #[inline]
480    #[must_use]
481    pub fn features_in_category(category: FeatureCategory) -> Vec<Feature> {
482        all_features()
483            .into_iter()
484            .filter(|f| f.category() == category)
485            .collect()
486    }
487
488    /// Create a new [`FeatureFlagsBuilder`] starting from defaults.
489    #[inline]
490    #[must_use]
491    pub fn builder() -> FeatureFlagsBuilder {
492        FeatureFlagsBuilder::default()
493    }
494}
495
496/// Builder for constructing [`FeatureFlags`] with a fluent API.
497#[derive(Debug, Clone, Default)]
498pub struct FeatureFlagsBuilder {
499    flags: FeatureFlags,
500}
501
502impl FeatureFlagsBuilder {
503    /// Enable a single feature flag.
504    #[inline]
505    #[must_use]
506    pub fn enable(mut self, feature: Feature) -> Self {
507        self.flags.enable(feature);
508        self
509    }
510
511    /// Disable a single feature flag.
512    #[inline]
513    #[must_use]
514    pub fn disable(mut self, feature: Feature) -> Self {
515        self.flags.disable(feature);
516        self
517    }
518
519    /// Enable all features in a category.
520    #[inline]
521    #[must_use]
522    pub fn enable_category(mut self, category: FeatureCategory) -> Self {
523        for feature in FeatureFlags::features_in_category(category) {
524            self.flags.enable(feature);
525        }
526        self
527    }
528
529    /// Disable all features in a category.
530    #[inline]
531    #[must_use]
532    pub fn disable_category(mut self, category: FeatureCategory) -> Self {
533        for feature in FeatureFlags::features_in_category(category) {
534            self.flags.disable(feature);
535        }
536        self
537    }
538
539    /// Consume the builder and return the configured [`FeatureFlags`].
540    #[inline]
541    #[must_use]
542    pub fn build(self) -> FeatureFlags {
543        self.flags
544    }
545}
546
547static GLOBAL_FLAGS: OnceLock<FeatureFlags> = OnceLock::new();
548
549/// Thread-safe, mutable handle to [`FeatureFlags`] for runtime toggling.
550#[derive(Debug)]
551pub struct FeatureFlagsHandle {
552    flags: RwLock<FeatureFlags>,
553}
554
555impl Default for FeatureFlagsHandle {
556    #[inline]
557    fn default() -> Self {
558        Self {
559            flags: RwLock::new(FeatureFlags::from_env()),
560        }
561    }
562}
563
564impl FeatureFlagsHandle {
565    /// Create a new handle initialized from environment variables.
566    #[inline]
567    #[must_use]
568    pub fn new() -> Self {
569        Self::default()
570    }
571
572    /// Check whether a feature is enabled in this handle.
573    #[inline]
574    #[must_use]
575    pub fn is_enabled(&self, feature: Feature) -> bool {
576        self.flags
577            .read()
578            .is_ok_and(|flags| flags.is_enabled(feature))
579    }
580
581    /// Enable a feature flag through the handle.
582    #[inline]
583    pub fn enable(&self, feature: Feature) {
584        if let Ok(mut flags) = self.flags.write() {
585            flags.enable(feature);
586        }
587    }
588
589    /// Disable a feature flag through the handle.
590    #[inline]
591    pub fn disable(&self, feature: Feature) {
592        if let Ok(mut flags) = self.flags.write() {
593            flags.disable(feature);
594        }
595    }
596
597    /// Toggle a feature flag through the handle.
598    #[inline]
599    pub fn toggle(&self, feature: Feature) {
600        if let Ok(mut flags) = self.flags.write() {
601            flags.toggle(feature);
602        }
603    }
604
605    /// Take an immutable snapshot of the current feature flags.
606    #[inline]
607    #[must_use]
608    pub fn snapshot(&self) -> FeatureFlags {
609        self.flags
610            .read()
611            .map(|flags| flags.clone())
612            .unwrap_or_default()
613    }
614}
615
616impl Clone for FeatureFlagsHandle {
617    #[inline]
618    fn clone(&self) -> Self {
619        Self {
620            flags: RwLock::new(self.snapshot()),
621        }
622    }
623}
624
625/// Return a list of every defined [`Feature`] variant.
626#[inline]
627#[must_use]
628pub fn all_features() -> Vec<Feature> {
629    vec![
630        Feature::SignSeparate,
631        Feature::RenamesR4R6,
632        Feature::Comp1,
633        Feature::Comp2,
634        Feature::AuditSystem,
635        Feature::SoxCompliance,
636        Feature::HipaaCompliance,
637        Feature::GdprCompliance,
638        Feature::PciDssCompliance,
639        Feature::SecurityMonitoring,
640        Feature::AdvancedOptimization,
641        Feature::LruCache,
642        Feature::ParallelDecode,
643        Feature::ZeroCopy,
644        Feature::VerboseLogging,
645        Feature::DiagnosticOutput,
646        Feature::Profiling,
647        Feature::MemoryTracking,
648        Feature::MutationTesting,
649        Feature::FuzzingIntegration,
650        Feature::CoverageInstrumentation,
651        Feature::PropertyBasedTesting,
652    ]
653}
654
655#[cfg(test)]
656#[allow(clippy::expect_used)]
657#[allow(clippy::unwrap_used)]
658mod tests {
659    use super::*;
660
661    #[test]
662    fn test_feature_display() {
663        assert_eq!(Feature::SignSeparate.to_string(), "sign_separate");
664        assert_eq!(Feature::LruCache.to_string(), "lru_cache");
665    }
666
667    #[test]
668    fn test_feature_from_str() {
669        assert_eq!(
670            Feature::from_str("sign_separate").unwrap(),
671            Feature::SignSeparate
672        );
673        assert_eq!(Feature::from_str("LRU_CACHE").unwrap(), Feature::LruCache);
674        assert!(Feature::from_str("unknown_feature").is_err());
675    }
676
677    #[test]
678    fn test_feature_category() {
679        assert_eq!(
680            Feature::SignSeparate.category(),
681            FeatureCategory::Experimental
682        );
683        assert_eq!(Feature::AuditSystem.category(), FeatureCategory::Enterprise);
684        assert_eq!(Feature::LruCache.category(), FeatureCategory::Performance);
685        assert_eq!(Feature::VerboseLogging.category(), FeatureCategory::Debug);
686        assert_eq!(
687            Feature::MutationTesting.category(),
688            FeatureCategory::Testing
689        );
690    }
691
692    #[test]
693    fn test_default_enabled() {
694        assert!(Feature::SignSeparate.default_enabled());
695        assert!(Feature::Comp1.default_enabled());
696        assert!(Feature::Comp2.default_enabled());
697        assert!(Feature::LruCache.default_enabled());
698        assert!(!Feature::VerboseLogging.default_enabled());
699    }
700
701    #[test]
702    fn lifecycle_reconciles_comp1_comp2_with_support_matrix() {
703        // Promoted COBOL-language features report a Stable lifecycle and are
704        // enabled by default, matching docs/reference/COBOL_SUPPORT_MATRIX.md
705        // ("Fully Supported"), even though category() groups them under the
706        // Experimental toggle group. lifecycle() is the authoritative class.
707        for feat in [Feature::Comp1, Feature::Comp2, Feature::SignSeparate] {
708            assert_eq!(
709                feat.lifecycle(),
710                FeatureLifecycle::Stable,
711                "{feat} should report a Stable lifecycle"
712            );
713            assert!(feat.default_enabled(), "{feat} should be default-enabled");
714        }
715
716        // category() stays a toggle group, orthogonal to the stability class.
717        assert_eq!(Feature::Comp1.category(), FeatureCategory::Experimental);
718        assert_eq!(Feature::Comp2.category(), FeatureCategory::Experimental);
719
720        // Opt-in / advanced flags remain Experimental in lifecycle.
721        assert_eq!(
722            Feature::RenamesR4R6.lifecycle(),
723            FeatureLifecycle::Experimental
724        );
725        assert_eq!(
726            Feature::AuditSystem.lifecycle(),
727            FeatureLifecycle::Experimental
728        );
729    }
730
731    #[test]
732    fn test_feature_flags_default() {
733        let flags = FeatureFlags::default();
734        assert!(flags.is_enabled(Feature::LruCache));
735        assert!(flags.is_enabled(Feature::SignSeparate));
736        assert!(flags.is_enabled(Feature::Comp1));
737        assert!(flags.is_enabled(Feature::Comp2));
738    }
739
740    #[test]
741    fn test_feature_flags_enable_disable() {
742        let mut flags = FeatureFlags::default();
743        flags.enable(Feature::SignSeparate);
744        assert!(flags.is_enabled(Feature::SignSeparate));
745        flags.disable(Feature::SignSeparate);
746        assert!(!flags.is_enabled(Feature::SignSeparate));
747    }
748
749    #[test]
750    fn test_feature_flags_toggle() {
751        let mut flags = FeatureFlags::default();
752        flags.toggle(Feature::LruCache);
753        assert!(!flags.is_enabled(Feature::LruCache));
754        flags.toggle(Feature::LruCache);
755        assert!(flags.is_enabled(Feature::LruCache));
756    }
757
758    #[test]
759    fn test_feature_flags_builder() {
760        let flags = FeatureFlags::builder()
761            .enable(Feature::SignSeparate)
762            .disable(Feature::LruCache)
763            .build();
764        assert!(flags.is_enabled(Feature::SignSeparate));
765        assert!(!flags.is_enabled(Feature::LruCache));
766    }
767
768    #[test]
769    fn test_feature_flags_enable_category() {
770        let flags = FeatureFlags::builder()
771            .enable_category(FeatureCategory::Experimental)
772            .build();
773        assert!(flags.is_enabled(Feature::SignSeparate));
774        assert!(flags.is_enabled(Feature::RenamesR4R6));
775        assert!(flags.is_enabled(Feature::Comp1));
776        assert!(flags.is_enabled(Feature::Comp2));
777    }
778
779    #[test]
780    fn test_feature_flags_handle() {
781        let handle = FeatureFlagsHandle::new();
782        handle.enable(Feature::SignSeparate);
783        assert!(handle.is_enabled(Feature::SignSeparate));
784        handle.disable(Feature::SignSeparate);
785        assert!(!handle.is_enabled(Feature::SignSeparate));
786        handle.enable(Feature::SignSeparate);
787        assert!(handle.is_enabled(Feature::SignSeparate));
788    }
789
790    #[test]
791    fn test_all_features() {
792        let features = all_features();
793        assert!(features.contains(&Feature::SignSeparate));
794        assert!(features.contains(&Feature::LruCache));
795        assert!(features.contains(&Feature::VerboseLogging));
796    }
797
798    #[test]
799    fn test_enabled_in_category() {
800        let mut flags = FeatureFlags::default();
801        flags.enable(Feature::RenamesR4R6);
802        let experimental = flags.enabled_in_category(FeatureCategory::Experimental);
803        assert_eq!(experimental.len(), 4);
804        assert!(experimental.contains(&Feature::SignSeparate));
805        assert!(experimental.contains(&Feature::Comp1));
806        assert!(experimental.contains(&Feature::Comp2));
807        assert!(experimental.contains(&Feature::RenamesR4R6));
808    }
809
810    #[test]
811    fn test_env_var_name() {
812        assert_eq!(
813            Feature::SignSeparate.env_var_name(),
814            "COPYBOOK_FF_SIGN_SEPARATE"
815        );
816        assert_eq!(Feature::LruCache.env_var_name(), "COPYBOOK_FF_LRU_CACHE");
817    }
818
819    #[test]
820    fn test_feature_serde_json_roundtrip() {
821        let feature = Feature::ParallelDecode;
822        let json = serde_json::to_string(&feature).unwrap();
823        let back: Feature = serde_json::from_str(&json).unwrap();
824        assert_eq!(back, feature);
825    }
826
827    #[test]
828    fn test_feature_category_serde_roundtrip() {
829        let cat = FeatureCategory::Enterprise;
830        let json = serde_json::to_string(&cat).unwrap();
831        let back: FeatureCategory = serde_json::from_str(&json).unwrap();
832        assert_eq!(back, cat);
833    }
834
835    #[test]
836    fn test_feature_lifecycle_display() {
837        assert_eq!(FeatureLifecycle::Experimental.to_string(), "experimental");
838        assert_eq!(FeatureLifecycle::Stable.to_string(), "stable");
839        assert_eq!(FeatureLifecycle::Deprecated.to_string(), "deprecated");
840    }
841
842    #[test]
843    fn test_feature_category_display() {
844        assert_eq!(FeatureCategory::Experimental.to_string(), "experimental");
845        assert_eq!(FeatureCategory::Enterprise.to_string(), "enterprise");
846        assert_eq!(FeatureCategory::Performance.to_string(), "performance");
847        assert_eq!(FeatureCategory::Debug.to_string(), "debug");
848        assert_eq!(FeatureCategory::Testing.to_string(), "testing");
849    }
850
851    #[test]
852    fn test_feature_flags_all_disabled() {
853        let mut flags = FeatureFlags::default();
854        for feature in all_features() {
855            flags.disable(feature);
856        }
857        for feature in all_features() {
858            assert!(!flags.is_enabled(feature), "{feature} should be disabled");
859        }
860    }
861
862    #[test]
863    fn test_feature_flags_all_enabled() {
864        let mut flags = FeatureFlags::default();
865        for feature in all_features() {
866            flags.enable(feature);
867        }
868        for feature in all_features() {
869            assert!(flags.is_enabled(feature), "{feature} should be enabled");
870        }
871    }
872
873    #[test]
874    fn test_builder_disable_category() {
875        let flags = FeatureFlags::builder()
876            .enable_category(FeatureCategory::Debug)
877            .disable_category(FeatureCategory::Debug)
878            .build();
879        let debug_features = flags.enabled_in_category(FeatureCategory::Debug);
880        assert!(debug_features.is_empty());
881    }
882
883    #[test]
884    fn test_handle_toggle_and_snapshot() {
885        let handle = FeatureFlagsHandle::new();
886        let initially_enabled = handle.is_enabled(Feature::LruCache);
887        handle.toggle(Feature::LruCache);
888        assert_ne!(handle.is_enabled(Feature::LruCache), initially_enabled);
889        let snap = handle.snapshot();
890        assert_ne!(snap.is_enabled(Feature::LruCache), initially_enabled);
891    }
892
893    #[test]
894    fn test_handle_clone_is_independent() {
895        let handle = FeatureFlagsHandle::new();
896        handle.enable(Feature::Profiling);
897        let cloned = handle.clone();
898        handle.disable(Feature::Profiling);
899        assert!(cloned.is_enabled(Feature::Profiling));
900        assert!(!handle.is_enabled(Feature::Profiling));
901    }
902
903    #[test]
904    fn test_features_in_category_counts() {
905        let experimental = FeatureFlags::features_in_category(FeatureCategory::Experimental);
906        assert_eq!(experimental.len(), 4);
907        let enterprise = FeatureFlags::features_in_category(FeatureCategory::Enterprise);
908        assert_eq!(enterprise.len(), 6);
909        let performance = FeatureFlags::features_in_category(FeatureCategory::Performance);
910        assert_eq!(performance.len(), 4);
911        let debug = FeatureFlags::features_in_category(FeatureCategory::Debug);
912        assert_eq!(debug.len(), 4);
913        let testing = FeatureFlags::features_in_category(FeatureCategory::Testing);
914        assert_eq!(testing.len(), 4);
915    }
916
917    #[test]
918    fn test_enabled_features_iterator_count() {
919        let flags = FeatureFlags::default();
920        let count = flags.enabled_features().count();
921        // Default-enabled: SignSeparate, Comp1, Comp2, LruCache
922        assert_eq!(count, 4);
923    }
924
925    #[test]
926    fn test_description_nonempty_for_all_features() {
927        for feature in all_features() {
928            assert!(
929                !feature.description().is_empty(),
930                "{feature} has empty description"
931            );
932        }
933    }
934
935    #[test]
936    fn test_from_str_unknown_returns_err() {
937        assert!(Feature::from_str("does_not_exist").is_err());
938        assert!(Feature::from_str("").is_err());
939    }
940
941    #[test]
942    fn test_feature_flags_serde_json_roundtrip() {
943        let flags = FeatureFlags::builder()
944            .enable(Feature::Profiling)
945            .disable(Feature::LruCache)
946            .build();
947        let json = serde_json::to_string(&flags).unwrap();
948        let back: FeatureFlags = serde_json::from_str(&json).unwrap();
949        assert!(back.is_enabled(Feature::Profiling));
950        assert!(!back.is_enabled(Feature::LruCache));
951    }
952}