Skip to main content

jugar_probar/coverage/
collector.rs

1//! Coverage Collector
2//!
3//! Per spec ยง9.1: Coverage Collection API
4//!
5//! Manages coverage collection sessions and test runs.
6
7use super::{BlockId, CoverageReport, CoverageViolation, JidokaAction, ThreadLocalCounters};
8
9/// Coverage granularity level
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum Granularity {
12    /// Function-level coverage (coarsest)
13    #[default]
14    Function,
15    /// Basic block coverage
16    BasicBlock,
17    /// Edge/branch coverage
18    Edge,
19    /// Path coverage (finest)
20    Path,
21}
22
23/// Coverage collection configuration
24#[derive(Debug, Clone)]
25pub struct CoverageConfig {
26    /// Coverage granularity
27    pub granularity: Granularity,
28    /// Enable parallel collection
29    pub parallel: bool,
30    /// Enable Jidoka guards
31    pub jidoka_enabled: bool,
32    /// Checkpoint interval (in seconds)
33    pub checkpoint_interval: Option<u64>,
34    /// Maximum blocks to track
35    pub max_blocks: usize,
36}
37
38impl CoverageConfig {
39    /// Create a builder for coverage config
40    #[must_use]
41    pub fn builder() -> CoverageConfigBuilder {
42        CoverageConfigBuilder::default()
43    }
44}
45
46impl Default for CoverageConfig {
47    fn default() -> Self {
48        Self {
49            granularity: Granularity::Function,
50            parallel: false,
51            jidoka_enabled: true,
52            checkpoint_interval: None,
53            max_blocks: 100_000,
54        }
55    }
56}
57
58/// Builder for coverage configuration
59#[derive(Debug, Default)]
60pub struct CoverageConfigBuilder {
61    granularity: Granularity,
62    parallel: bool,
63    jidoka_enabled: bool,
64    checkpoint_interval: Option<u64>,
65    max_blocks: usize,
66}
67
68impl CoverageConfigBuilder {
69    /// Set the coverage granularity
70    #[must_use]
71    pub fn granularity(mut self, granularity: Granularity) -> Self {
72        self.granularity = granularity;
73        self
74    }
75
76    /// Enable parallel collection
77    #[must_use]
78    pub fn parallel(mut self, enabled: bool) -> Self {
79        self.parallel = enabled;
80        self
81    }
82
83    /// Enable Jidoka guards
84    #[must_use]
85    pub fn jidoka_enabled(mut self, enabled: bool) -> Self {
86        self.jidoka_enabled = enabled;
87        self
88    }
89
90    /// Set checkpoint interval
91    #[must_use]
92    pub fn checkpoint_interval(mut self, seconds: u64) -> Self {
93        self.checkpoint_interval = Some(seconds);
94        self
95    }
96
97    /// Set maximum blocks
98    #[must_use]
99    pub fn max_blocks(mut self, max: usize) -> Self {
100        self.max_blocks = max;
101        self
102    }
103
104    /// Build the configuration
105    #[must_use]
106    pub fn build(self) -> CoverageConfig {
107        CoverageConfig {
108            granularity: self.granularity,
109            parallel: self.parallel,
110            jidoka_enabled: self.jidoka_enabled,
111            checkpoint_interval: self.checkpoint_interval,
112            max_blocks: if self.max_blocks == 0 {
113                100_000
114            } else {
115                self.max_blocks
116            },
117        }
118    }
119}
120
121/// Coverage collector for tracking coverage during test execution
122#[derive(Debug)]
123pub struct CoverageCollector {
124    /// Configuration
125    config: CoverageConfig,
126    /// Current session report
127    report: Option<CoverageReport>,
128    /// Current test name
129    current_test: Option<String>,
130    /// Thread-local counters
131    counters: ThreadLocalCounters,
132    /// Session active flag
133    session_active: bool,
134    /// Test active flag
135    test_active: bool,
136}
137
138impl CoverageCollector {
139    /// Create a new collector with the given configuration
140    #[must_use]
141    pub fn new(config: CoverageConfig) -> Self {
142        let max_blocks = config.max_blocks;
143        Self {
144            config,
145            report: None,
146            current_test: None,
147            counters: ThreadLocalCounters::new(max_blocks),
148            session_active: false,
149            test_active: false,
150        }
151    }
152
153    /// Begin a coverage collection session
154    pub fn begin_session(&mut self, name: &str) {
155        let mut report = CoverageReport::new(self.config.max_blocks);
156        report.set_session_name(name);
157        self.report = Some(report);
158        self.session_active = true;
159    }
160
161    /// End the current session and return the report
162    #[must_use]
163    pub fn end_session(&mut self) -> CoverageReport {
164        self.flush_counters();
165        self.session_active = false;
166        self.report.take().unwrap_or_default()
167    }
168
169    /// Begin a test within the session
170    pub fn begin_test(&mut self, name: &str) {
171        self.current_test = Some(name.to_string());
172        self.test_active = true;
173        if let Some(report) = &mut self.report {
174            report.add_test(name);
175        }
176    }
177
178    /// End the current test
179    pub fn end_test(&mut self) {
180        self.flush_counters();
181        self.current_test = None;
182        self.test_active = false;
183    }
184
185    /// Record a hit on a block
186    pub fn record_hit(&mut self, block: BlockId) {
187        self.counters.increment(block);
188    }
189
190    /// Record a violation
191    pub fn record_violation(&mut self, violation: CoverageViolation) {
192        if self.config.jidoka_enabled {
193            let action = violation.action();
194            if let Some(report) = &mut self.report {
195                report.record_violation(violation);
196            }
197
198            // Hard stop would panic here in production
199            if action == JidokaAction::Stop {
200                // In a real implementation, this would trigger the Andon cord
201                // For now, we just record it
202            }
203        }
204    }
205
206    /// Flush thread-local counters to the report
207    fn flush_counters(&mut self) {
208        let counts = self.counters.flush();
209        if let Some(report) = &mut self.report {
210            for (idx, count) in counts.iter().enumerate() {
211                if *count > 0 {
212                    report.record_hits(BlockId::new(idx as u32), *count);
213                }
214            }
215        }
216    }
217
218    /// Check if a session is active
219    #[must_use]
220    pub fn is_session_active(&self) -> bool {
221        self.session_active
222    }
223
224    /// Check if a test is active
225    #[must_use]
226    pub fn is_test_active(&self) -> bool {
227        self.test_active
228    }
229
230    /// Get the current test name
231    #[must_use]
232    pub fn current_test(&self) -> Option<&str> {
233        self.current_test.as_deref()
234    }
235
236    /// Get the configuration
237    #[must_use]
238    pub fn config(&self) -> &CoverageConfig {
239        &self.config
240    }
241}