confium_test_harness/
report.rs1use serde::{Deserialize, Serialize};
15
16use crate::Result;
17use crate::TestResult;
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct ReportEntry {
22 pub scheme_name: String,
23 pub scheme_version: String,
24 pub parties: u32,
25 pub threshold: u32,
26 pub outcome: String,
27 pub output_hex: String,
28 pub messages_exchanged: u64,
29 pub bytes_exchanged: u64,
30 pub rounds: u8,
31 pub elapsed_nanos: u128,
32 pub note: Option<String>,
33}
34
35impl From<&TestResult> for ReportEntry {
36 fn from(r: &TestResult) -> Self {
37 ReportEntry {
38 scheme_name: r.scheme_name.clone(),
39 scheme_version: r.scheme_version.clone(),
40 parties: r.parties,
41 threshold: r.threshold,
42 outcome: r.outcome.as_str().to_string(),
43 output_hex: to_hex(&r.output),
44 messages_exchanged: r.messages_exchanged,
45 bytes_exchanged: r.bytes_exchanged,
46 rounds: r.rounds,
47 elapsed_nanos: r.elapsed.as_nanos(),
48 note: r.note.clone(),
49 }
50 }
51}
52
53#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub struct Report {
56 pub entries: Vec<ReportEntry>,
57}
58
59impl Report {
60 pub fn new() -> Self {
61 Report::default()
62 }
63
64 pub fn from_results(results: &[TestResult]) -> Self {
65 Report {
66 entries: results.iter().map(ReportEntry::from).collect(),
67 }
68 }
69
70 pub fn push(&mut self, entry: ReportEntry) {
71 self.entries.push(entry);
72 }
73
74 pub fn to_json(&self) -> Result<String> {
77 Ok(serde_json::to_string(self)?)
78 }
79
80 pub fn to_json_pretty(&self) -> Result<String> {
82 Ok(serde_json::to_string_pretty(self)?)
83 }
84}
85
86fn to_hex(bytes: &[u8]) -> String {
88 let mut out = String::with_capacity(bytes.len() * 2);
89 for b in bytes {
90 out.push_str(&format!("{b:02x}"));
91 }
92 out
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::Outcome;
99 use std::time::Duration;
100
101 fn fake_result(outcome: Outcome) -> TestResult {
102 TestResult {
103 scheme_name: "FROST-ed25519".into(),
104 scheme_version: "draft-irtf-cfrg-frost-13".into(),
105 parties: 5,
106 threshold: 3,
107 outcome,
108 output: vec![0xDE, 0xAD],
109 messages_exchanged: 12,
110 bytes_exchanged: 1024,
111 rounds: 3,
112 elapsed: Duration::from_micros(12_345),
113 note: None,
114 }
115 }
116
117 #[test]
118 fn entry_serializes_to_expected_fields() {
119 let r = fake_result(Outcome::Pass);
120 let entry = ReportEntry::from(&r);
121 let json = serde_json::to_string(&entry).unwrap();
122 assert!(json.contains("\"scheme_name\":\"FROST-ed25519\""));
123 assert!(json.contains("\"outcome\":\"pass\""));
124 assert!(json.contains("\"output_hex\":\"dead\""));
125 assert!(json.contains("\"messages_exchanged\":12"));
126 assert!(json.contains("\"rounds\":3"));
127 }
128
129 #[test]
130 fn report_round_trips_through_json() {
131 let report =
132 Report::from_results(&[fake_result(Outcome::Pass), fake_result(Outcome::Fail)]);
133 let json = report.to_json().unwrap();
134 let reparsed: Report = serde_json::from_str(&json).unwrap();
135 assert_eq!(reparsed.entries.len(), 2);
136 assert_eq!(reparsed.entries[0].outcome, "pass");
137 assert_eq!(reparsed.entries[1].outcome, "fail");
138 }
139
140 #[test]
141 fn pretty_json_is_multiline() {
142 let report = Report::from_results(&[fake_result(Outcome::Pass)]);
143 let pretty = report.to_json_pretty().unwrap();
144 assert!(pretty.contains('\n'));
145 }
146
147 #[test]
148 fn to_hex_lowercase_no_prefix() {
149 assert_eq!(to_hex(&[0xDE, 0xAD, 0xBE, 0xEF]), "deadbeef");
150 assert_eq!(to_hex(&[]), "");
151 }
152}