Skip to main content

wickra_benchmark_core/
suite.rs

1//! [`Suite`] — the curated collection of cases — and the [`CaseResult`] /
2//! [`SuiteReport`] the runner produces.
3
4use crate::case::BenchmarkCase;
5use crate::error::{Error, Result};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::BTreeSet;
9
10/// A curated suite of cases. The case order in the file is irrelevant — the
11/// runner sorts results by `id` — but every `id` must be unique.
12#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
13pub struct Suite {
14    /// A human-readable suite name (for example `"wickra-benchmark v0.1 core suite"`).
15    #[serde(default)]
16    pub name: String,
17    /// The curated cases.
18    pub cases: Vec<BenchmarkCase>,
19}
20
21impl Suite {
22    /// Parse a `Suite` from JSON.
23    pub fn from_json(s: &str) -> Result<Self> {
24        let suite: Self = serde_json::from_str(s).map_err(|e| Error::BadCase(e.to_string()))?;
25        suite.validate()?;
26        Ok(suite)
27    }
28
29    /// Parse a `Suite` from TOML.
30    pub fn from_toml(s: &str) -> Result<Self> {
31        let suite: Self = toml::from_str(s).map_err(|e| Error::Parse(e.to_string()))?;
32        suite.validate()?;
33        Ok(suite)
34    }
35
36    /// Validate the suite: every case is individually valid and all `id`s are
37    /// unique. A duplicate `id` is a `BadCase` — the ids are the sort and tie
38    /// key, so a collision would make the report order ambiguous.
39    pub(crate) fn validate(&self) -> Result<()> {
40        let mut seen: BTreeSet<&str> = BTreeSet::new();
41        for case in &self.cases {
42            case.validate()?;
43            if !seen.insert(case.id.as_str()) {
44                return Err(Error::BadCase(format!("duplicate case id: {}", case.id)));
45            }
46        }
47        Ok(())
48    }
49
50    /// The case ids, sorted ascending — the deterministic answer to `list_cases`.
51    #[must_use]
52    pub fn case_ids(&self) -> Vec<String> {
53        let mut ids: Vec<String> = self.cases.iter().map(|c| c.id.clone()).collect();
54        ids.sort();
55        ids
56    }
57}
58
59/// The outcome of running one case: whether the recomputed report matches the
60/// expectation (`passed`) and its frozen hash (`hash_match`), plus the recomputed
61/// report and its hash for inspection.
62#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
63pub struct CaseResult {
64    /// The case `id`.
65    pub id: String,
66    /// The recomputed report is byte-exact equal to `case.expected`.
67    pub passed: bool,
68    /// `hash(recomputed) == case.expected_hash`.
69    pub hash_match: bool,
70    /// The freshly recomputed report as JSON (for diffing against the
71    /// expectation). The engine's `BacktestReport` is serialize-only, so it
72    /// travels as a JSON object.
73    pub recomputed: Value,
74    /// The canonical blake3 hash of `recomputed`.
75    pub hash: String,
76}
77
78/// The result of running a whole suite: the per-case results (sorted by `id`)
79/// and the pass/fail tally. A case counts as passing only when it both `passed`
80/// and `hash_match`.
81#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
82pub struct SuiteReport {
83    /// The per-case results, always sorted ascending by `id`.
84    pub results: Vec<CaseResult>,
85    /// The number of cases that both `passed` and `hash_match`.
86    pub passed: usize,
87    /// `results.len() - passed`.
88    pub failed: usize,
89}