Skip to main content

cu_profiler_core/
scenario.rs

1//! The first-class [`Scenario`] type.
2//!
3//! A scenario is not merely a test — it is a reproducible compute benchmark with
4//! an expected outcome, a budget policy, and metadata used for fingerprinting.
5
6use serde::{Deserialize, Serialize};
7
8use crate::budget::BudgetPolicy;
9
10/// How important a scenario is. Drives diagnostic severity and `--strict` gating.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
12#[serde(rename_all = "lowercase")]
13pub enum Criticality {
14    /// Failure must block CI.
15    Critical,
16    /// Notable but non-blocking by default.
17    #[default]
18    Normal,
19    /// Informational only.
20    Low,
21}
22
23/// What a scenario is expected to do. Failure paths are first-class: a failing
24/// instruction that burns CU is relevant for both performance and security.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
26#[serde(rename_all = "lowercase")]
27pub enum ExpectedResult {
28    /// The transaction is expected to succeed.
29    #[default]
30    Success,
31    /// The transaction is expected to fail (a measured failure path).
32    Failure,
33}
34
35/// A reproducible compute benchmark.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct Scenario {
38    /// Stable, hierarchical name, e.g. `swap/referral_enabled`.
39    pub name: String,
40    /// Human description of what the scenario exercises.
41    #[serde(default)]
42    pub description: String,
43    /// Free-form tags used for filtering (`--tag`).
44    #[serde(default)]
45    pub tags: Vec<String>,
46    /// How critical the scenario is.
47    #[serde(default)]
48    pub criticality: Criticality,
49    /// Optional owner (team or person) for triage.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub owner: Option<String>,
52    /// Expected outcome.
53    #[serde(default)]
54    pub expected: ExpectedResult,
55    /// The budget policy applied to this scenario.
56    #[serde(default)]
57    pub budget: BudgetPolicy,
58    /// How many samples to take when measuring (>= 1).
59    #[serde(default = "default_samples")]
60    pub samples: u32,
61}
62
63fn default_samples() -> u32 {
64    1
65}
66
67impl Scenario {
68    /// A minimal scenario with the given name and default policy.
69    #[must_use]
70    pub fn new(name: impl Into<String>) -> Self {
71        Self {
72            name: name.into(),
73            description: String::new(),
74            tags: Vec::new(),
75            criticality: Criticality::Normal,
76            owner: None,
77            expected: ExpectedResult::Success,
78            budget: BudgetPolicy::default(),
79            samples: 1,
80        }
81    }
82
83    /// Does this scenario carry the given tag?
84    #[must_use]
85    pub fn has_tag(&self, tag: &str) -> bool {
86        self.tags.iter().any(|t| t == tag)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn defaults_are_sane() {
96        let s = Scenario::new("swap/happy_path");
97        assert_eq!(s.samples, 1);
98        assert_eq!(s.expected, ExpectedResult::Success);
99        assert_eq!(s.criticality, Criticality::Normal);
100    }
101
102    #[test]
103    fn tag_filtering() {
104        let mut s = Scenario::new("swap/large_pool");
105        s.tags = vec!["swap".into(), "hot-path".into()];
106        assert!(s.has_tag("hot-path"));
107        assert!(!s.has_tag("admin"));
108    }
109}