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}