1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use base64::Engine;
6use crate::product::{ProductCertificationRule, ProductStageRunMode, ProductStageSurface};
7use serde::{Deserialize, Serialize, Serializer};
8use serde_json::{Map, Number, Value};
9
10const PREVIEW_LIMIT: usize = 64;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum ReportMode {
14 #[serde(rename = "default")]
15 Default,
16 #[serde(rename = "golden")]
17 Golden,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct ReportHeader {
22 pub k: String,
23 pub v: String,
24 pub inventory_sha256: String,
25 pub suite_sha256: String,
26 pub mode: ReportMode,
27}
28
29impl ReportHeader {
30 pub fn new(inventory_sha256: String, suite_sha256: String, mode: ReportMode) -> Self {
31 Self {
32 k: "observer_report".to_owned(),
33 v: "0".to_owned(),
34 inventory_sha256,
35 suite_sha256,
36 mode,
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ProductReportHeader {
43 pub k: String,
44 pub v: String,
45 pub product_id: String,
46 pub product_sha256: String,
47 pub certification_rule: ProductCertificationRule,
48 pub mode: ReportMode,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub product_label: Option<String>,
51}
52
53impl ProductReportHeader {
54 pub fn new(
55 product_id: String,
56 product_sha256: String,
57 certification_rule: ProductCertificationRule,
58 mode: ReportMode,
59 product_label: Option<String>,
60 ) -> Self {
61 Self {
62 k: "observer_product_report".to_owned(),
63 v: "0".to_owned(),
64 product_id,
65 product_sha256,
66 certification_rule,
67 mode,
68 product_label,
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq)]
74pub enum ReportRecord {
75 Header(ReportHeader),
76 Assert(AssertRecord),
77 Action(ActionRecord),
78 Artifact(ArtifactRecord),
79 Extract(ExtractRecord),
80 Telemetry(TelemetryRecord),
81 Case(CaseRecord),
82 Summary(SummaryRecord),
83}
84
85#[derive(Debug, Clone, PartialEq)]
86pub enum ProductReportRecord {
87 Header(ProductReportHeader),
88 Stage(ProductStageRecord),
89 Summary(ProductSummaryRecord),
90}
91
92impl Serialize for ProductReportRecord {
93 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94 where
95 S: Serializer,
96 {
97 match self {
98 ProductReportRecord::Header(header) => header.serialize(serializer),
99 ProductReportRecord::Stage(record) => TaggedRecord {
100 k: "product_stage",
101 inner: record,
102 }
103 .serialize(serializer),
104 ProductReportRecord::Summary(record) => TaggedRecord {
105 k: "product_summary",
106 inner: record,
107 }
108 .serialize(serializer),
109 }
110 }
111}
112
113impl Serialize for ReportRecord {
114 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115 where
116 S: Serializer,
117 {
118 match self {
119 ReportRecord::Header(header) => header.serialize(serializer),
120 ReportRecord::Assert(record) => TaggedRecord {
121 k: "assert",
122 inner: record,
123 }
124 .serialize(serializer),
125 ReportRecord::Action(record) => TaggedRecord {
126 k: "action",
127 inner: record,
128 }
129 .serialize(serializer),
130 ReportRecord::Artifact(record) => TaggedRecord {
131 k: "artifact",
132 inner: record,
133 }
134 .serialize(serializer),
135 ReportRecord::Extract(record) => TaggedRecord {
136 k: "extract",
137 inner: record,
138 }
139 .serialize(serializer),
140 ReportRecord::Telemetry(record) => TaggedRecord {
141 k: "telemetry",
142 inner: record,
143 }
144 .serialize(serializer),
145 ReportRecord::Case(record) => TaggedRecord {
146 k: "case",
147 inner: record,
148 }
149 .serialize(serializer),
150 ReportRecord::Summary(record) => TaggedRecord {
151 k: "summary",
152 inner: record,
153 }
154 .serialize(serializer),
155 }
156 }
157}
158
159#[derive(Serialize)]
160struct TaggedRecord<'a, T> {
161 k: &'static str,
162 #[serde(flatten)]
163 inner: &'a T,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct AssertRecord {
168 pub case_id: String,
169 pub assert_ix: usize,
170 pub status: Status,
171 pub msg: String,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct ActionRecord {
176 pub case_id: String,
177 pub action_ix: usize,
178 pub action: String,
179 pub status: ActionStatus,
180 pub args: serde_json::Value,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub ok: Option<serde_json::Value>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub fail: Option<ActionFail>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct ActionFail {
189 pub kind: String,
190 pub msg: String,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub code: Option<String>,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct ArtifactRecord {
197 pub case_id: String,
198 pub artifact_ix: usize,
199 pub action_ix: usize,
200 pub name: String,
201 pub event: String,
202 pub kind: String,
203 pub status: ActionStatus,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub location: Option<serde_json::Value>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub fail: Option<ActionFail>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct ExtractRecord {
212 pub case_id: String,
213 pub extract_ix: usize,
214 pub action_ix: usize,
215 pub source_artifact: String,
216 pub format: String,
217 pub status: ActionStatus,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub select: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub summary: Option<serde_json::Value>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub fail: Option<ActionFail>,
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
227pub struct TelemetryRecord {
228 pub case_id: String,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub action_ix: Option<usize>,
231 pub scope: TelemetryScope,
232 #[serde(flatten)]
233 pub entry: TelemetryEntry,
234 pub canonical: bool,
235}
236
237impl TelemetryRecord {
238 pub fn action(case_id: String, action_ix: usize, entry: TelemetryEntry) -> Self {
239 Self {
240 case_id,
241 action_ix: Some(action_ix),
242 scope: TelemetryScope::Action,
243 entry,
244 canonical: false,
245 }
246 }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251pub enum TelemetryScope {
252 Case,
253 Action,
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
257pub struct TelemetryEntry {
258 pub name: String,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub unit: Option<String>,
261 #[serde(flatten)]
262 pub value: TelemetryValue,
263}
264
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266#[serde(tag = "kind", rename_all = "snake_case")]
267pub enum TelemetryValue {
268 Metric { value: Number },
269 Vector { values: Vec<Number> },
270 Tag { value: String },
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274pub struct CaseRecord {
275 pub case_id: String,
276 pub item_id: String,
277 pub test_name: String,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub case_key: Option<String>,
280 pub status: Status,
281 pub assert_pass: usize,
282 pub assert_fail: usize,
283 pub unhandled_action_fail: usize,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287pub struct SummaryRecord {
288 pub case_pass: usize,
289 pub case_fail: usize,
290 pub assert_pass: usize,
291 pub assert_fail: usize,
292 pub exit_code: i32,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296pub struct ProductStageRecord {
297 pub stage_id: String,
298 pub runner_kind: String,
299 pub cwd: String,
300 pub required: bool,
301 pub status: ProductStatus,
302 pub exit_code: i32,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub suite_path: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub surface: Option<ProductStageSurface>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub mode: Option<ProductStageRunMode>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub model_path: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub inventory_path: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub config_path: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub filter: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub child_report_path: Option<String>,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub child_report_sha256: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub child_inventory_sha256: Option<String>,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub child_suite_sha256: Option<String>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub child_model_sha256: Option<String>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub child_case_pass: Option<usize>,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub child_case_fail: Option<usize>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub child_assert_pass: Option<usize>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub child_assert_fail: Option<usize>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub child_check_pass: Option<usize>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub child_check_fail: Option<usize>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub child_check_validation_fail: Option<usize>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub child_check_runner_fail: Option<usize>,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub fail: Option<ActionFail>,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct ProductSummaryRecord {
349 pub product_id: String,
350 pub certification_rule: ProductCertificationRule,
351 pub stage_pass: usize,
352 pub stage_fail: usize,
353 pub stage_runner_error: usize,
354 pub status: ProductStatus,
355 pub exit_code: i32,
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
359#[serde(rename_all = "snake_case")]
360pub enum Status {
361 Pass,
362 Fail,
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(rename_all = "snake_case")]
367pub enum ActionStatus {
368 Ok,
369 Fail,
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(rename_all = "snake_case")]
374pub enum ProductStatus {
375 Pass,
376 Fail,
377 RunnerError,
378}
379
380pub fn serialize_report_json<T: Serialize>(value: &T, mode: ReportMode) -> crate::ObserverResult<String> {
381 match mode {
382 ReportMode::Default => serde_json::to_string(value)
383 .map_err(|error| crate::ObserverError::Normalize(error.to_string())),
384 ReportMode::Golden => {
385 let value = serde_json::to_value(value)
386 .map_err(|error| crate::ObserverError::Normalize(error.to_string()))?;
387 let canonical = canonicalize_json(value);
388 serde_json::to_string(&canonical)
389 .map_err(|error| crate::ObserverError::Normalize(error.to_string()))
390 }
391 }
392}
393
394pub fn derive_case_id(item_id: &str, case_key: &str) -> String {
395 let mut bytes = Vec::with_capacity(item_id.len() + case_key.len() + 1);
396 bytes.extend_from_slice(item_id.as_bytes());
397 bytes.push(0x1f);
398 bytes.extend_from_slice(case_key.as_bytes());
399 URL_SAFE_NO_PAD.encode(bytes)
400}
401
402pub fn preview_bytes(bytes: &[u8]) -> (usize, Option<String>, Option<bool>) {
403 if bytes.is_empty() {
404 return (0, None, None);
405 }
406
407 let preview_len = bytes.len().min(PREVIEW_LIMIT);
408 let preview = URL_SAFE_NO_PAD.encode(&bytes[..preview_len]);
409 let truncated = if bytes.len() > preview_len {
410 Some(true)
411 } else {
412 None
413 };
414 (bytes.len(), Some(preview), truncated)
415}
416
417fn canonicalize_json(value: Value) -> Value {
418 match value {
419 Value::Object(object) => {
420 let mut ordered = Map::new();
421 let mut entries = object.into_iter().collect::<Vec<_>>();
422 entries.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
423 for (key, value) in entries {
424 ordered.insert(key, canonicalize_json(value));
425 }
426 Value::Object(ordered)
427 }
428 Value::Array(items) => Value::Array(items.into_iter().map(canonicalize_json).collect()),
429 other => other,
430 }
431}