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). **Reserved**: the
59    /// recorded backend is deterministic so it ignores this; it will apply to
60    /// live backends that exhibit run-to-run variance (see the roadmap).
61    #[serde(default = "default_samples")]
62    pub samples: u32,
63}
64
65fn default_samples() -> u32 {
66    1
67}
68
69impl Scenario {
70    /// A minimal scenario with the given name and default policy.
71    #[must_use]
72    pub fn new(name: impl Into<String>) -> Self {
73        Self {
74            name: name.into(),
75            description: String::new(),
76            tags: Vec::new(),
77            criticality: Criticality::Normal,
78            owner: None,
79            expected: ExpectedResult::Success,
80            budget: BudgetPolicy::default(),
81            samples: 1,
82        }
83    }
84
85    /// Does this scenario carry the given tag?
86    #[must_use]
87    pub fn has_tag(&self, tag: &str) -> bool {
88        self.tags.iter().any(|t| t == tag)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn defaults_are_sane() {
98        let s = Scenario::new("swap/happy_path");
99        assert_eq!(s.samples, 1);
100        assert_eq!(s.expected, ExpectedResult::Success);
101        assert_eq!(s.criticality, Criticality::Normal);
102    }
103
104    #[test]
105    fn tag_filtering() {
106        let mut s = Scenario::new("swap/large_pool");
107        s.tags = vec!["swap".into(), "hot-path".into()];
108        assert!(s.has_tag("hot-path"));
109        assert!(!s.has_tag("admin"));
110    }
111}