Skip to main content

brep_kernel/props/
diagnostics.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
5#[serde(rename_all = "lowercase")]
6pub enum DiagnosticSeverity {
7    Info,
8    Warning,
9    Degraded,
10    Error,
11}
12
13#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum KernelStage {
16    Collect,
17    Classify,
18    Intersect,
19    Refine,
20    Fragment,
21    Select,
22    Sew,
23    Validate,
24    Export,
25}
26
27#[derive(Clone, Debug, Deserialize, Serialize)]
28pub struct DiagnosticEvent {
29    pub severity: DiagnosticSeverity,
30    pub stage: KernelStage,
31    /// Stable machine-readable identifier suitable for tests and UI routing.
32    pub code: String,
33    pub message: String,
34}
35
36#[derive(Clone, Debug, Default, Deserialize, Serialize)]
37pub struct KernelDiagnostics {
38    pub events: Vec<DiagnosticEvent>,
39    pub counters: BTreeMap<String, u64>,
40    pub measurements: BTreeMap<String, f64>,
41}
42
43impl KernelDiagnostics {
44    pub fn event(
45        &mut self,
46        severity: DiagnosticSeverity,
47        stage: KernelStage,
48        code: impl Into<String>,
49        message: impl Into<String>,
50    ) {
51        self.events.push(DiagnosticEvent {
52            severity,
53            stage,
54            code: code.into(),
55            message: message.into(),
56        });
57    }
58
59    pub fn count(&mut self, code: impl Into<String>) {
60        *self.counters.entry(code.into()).or_default() += 1;
61    }
62
63    pub fn count_n(&mut self, code: impl Into<String>, amount: u64) {
64        *self.counters.entry(code.into()).or_default() += amount;
65    }
66
67    pub fn measure_max(&mut self, code: impl Into<String>, value: f64) {
68        let entry = self.measurements.entry(code.into()).or_insert(value);
69        *entry = entry.max(value);
70    }
71
72    pub fn worst(&self) -> Option<DiagnosticSeverity> {
73        self.events.iter().map(|event| event.severity).max()
74    }
75
76    pub fn shippable(&self) -> bool {
77        self.events
78            .iter()
79            .all(|event| event.severity < DiagnosticSeverity::Error)
80    }
81}
82
83#[derive(Clone, Debug, Deserialize, Serialize)]
84pub struct KernelOutcome<T> {
85    pub value: T,
86    pub diagnostics: KernelDiagnostics,
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn diagnostics_are_stable_and_queryable() {
95        let mut diagnostics = KernelDiagnostics::default();
96        diagnostics.count("intersect.pairs");
97        diagnostics.count_n("intersect.pairs", 2);
98        diagnostics.measure_max("validate.edge_error", 0.1);
99        diagnostics.measure_max("validate.edge_error", 0.05);
100        diagnostics.event(
101            DiagnosticSeverity::Warning,
102            KernelStage::Classify,
103            "pair.near_tangent",
104            "dedicated path selected",
105        );
106        assert_eq!(diagnostics.counters["intersect.pairs"], 3);
107        assert_eq!(diagnostics.measurements["validate.edge_error"], 0.1);
108        assert!(diagnostics.shippable());
109    }
110}