rs_teststand/execution/result_list.rs
1//! Reading an execution's recorded results.
2
3use crate::Error;
4use crate::property::{PropertyObject, PropertyOptions};
5
6/// Where a recorded result keeps the name of the step that produced it.
7const STEP_NAME: &str = "TS.StepName";
8/// Where it keeps that step's type.
9const STEP_TYPE: &str = "TS.StepType";
10/// The pass/fail or error outcome.
11const STATUS: &str = "Status";
12/// A numeric measurement, present on test steps only.
13const NUMERIC: &str = "Numeric";
14/// A string measurement, present on string value tests.
15const STRING: &str = "String";
16
17/// One entry from an execution's result list.
18///
19/// Plain data, deliberately: the COM objects behind a result belong to the
20/// execution that produced them, so a host that wants to keep results after the
21/// run must keep values rather than references.
22#[derive(Debug, Clone, PartialEq)]
23pub struct StepResult {
24 /// The step that recorded this result.
25 pub name: String,
26 /// That step's type, for example `NumericLimitTest`.
27 pub step_type: String,
28 /// The outcome the engine recorded: `Passed`, `Failed`, `Done`, `Error`,
29 /// `Skipped`, or a status a sequence set itself.
30 pub status: String,
31 /// The measurement, when the step recorded one.
32 ///
33 /// A numeric limit test yields a number, a string value test a string, and
34 /// an action neither, which is why this is optional rather than defaulted
35 /// to zero or an empty string.
36 pub value: Option<ResultValue>,
37}
38
39/// A measurement carried by a result.
40#[derive(Debug, Clone, PartialEq)]
41pub enum ResultValue {
42 /// A numeric measurement.
43 Number(f64),
44 /// A string measurement.
45 Text(String),
46}
47
48/// An execution's recorded results (`ResultList`).
49///
50/// A thin reader over the array the engine builds as a sequence runs. Only
51/// steps whose recording is enabled appear here, so the list is routinely
52/// shorter than the sequence, and a step that failed early can end a run before
53/// later steps record anything. See
54/// [`ResultRecordingOption`](crate::ResultRecordingOption).
55#[derive(Debug)]
56pub struct ResultList {
57 results: PropertyObject,
58}
59
60impl ResultList {
61 /// Reads results from anything that exposes a `ResultList` property.
62 ///
63 /// Accepts the results tree from
64 /// [`Execution::result_object`](crate::Execution::result_object), which is
65 /// where a headless caller finds them.
66 ///
67 /// # Errors
68 /// [`Error`] if the object holds no `ResultList`, or a COM call fails.
69 pub fn from_result_object(result_object: &PropertyObject) -> Result<Self, Error> {
70 let none = PropertyOptions::NONE.bits();
71 if !result_object.exists("ResultList", none)? {
72 return Err(Error::UnexpectedType {
73 expected: "an object carrying a ResultList",
74 actual: "no ResultList property",
75 });
76 }
77 Ok(Self {
78 results: result_object.get_property_object("ResultList", none)?,
79 })
80 }
81
82 /// How many results were recorded.
83 ///
84 /// # Errors
85 /// [`Error`] if the COM call fails.
86 pub fn len(&self) -> Result<i32, Error> {
87 self.results.get_num_elements()
88 }
89
90 /// Whether nothing was recorded.
91 ///
92 /// # Errors
93 /// [`Error`] if the COM call fails.
94 pub fn is_empty(&self) -> Result<bool, Error> {
95 Ok(self.len()? == 0)
96 }
97
98 /// Reads every result into plain data.
99 ///
100 /// Each field is optional in the tree, and a missing one is normal rather
101 /// than an error: an action step records no measurement, and a result
102 /// written by a custom step type may carry neither name nor type. Missing
103 /// text becomes empty, a missing measurement becomes `None`.
104 ///
105 /// # Errors
106 /// [`Error`] if the list cannot be walked.
107 pub fn parse(&self) -> Result<Vec<StepResult>, Error> {
108 let none = PropertyOptions::NONE.bits();
109 let mut parsed = Vec::new();
110
111 for index in 0..self.len()? {
112 let entry = self.results.get_property_object_by_offset(index, none)?;
113 let text = |path: &str| entry.get_val_string(path, none).unwrap_or_default();
114
115 // Numeric first: a numeric limit test carries both a number and, on
116 // some step types, an empty string, and the number is the reading.
117 let value = entry
118 .get_val_number(NUMERIC, none)
119 .ok()
120 .map(ResultValue::Number)
121 .or_else(|| {
122 entry
123 .get_val_string(STRING, none)
124 .ok()
125 .filter(|found| !found.is_empty())
126 .map(ResultValue::Text)
127 });
128
129 parsed.push(StepResult {
130 name: text(STEP_NAME),
131 step_type: text(STEP_TYPE),
132 status: text(STATUS),
133 value,
134 });
135 }
136 Ok(parsed)
137 }
138
139 /// The underlying array, for a caller that wants to walk it itself.
140 #[must_use]
141 pub const fn as_property_object(&self) -> &PropertyObject {
142 &self.results
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::{ResultValue, StepResult};
149
150 #[test]
151 fn a_result_without_a_measurement_is_distinguishable_from_a_zero() {
152 // An action step records no measurement. Defaulting that to 0.0 would
153 // make it indistinguishable from a test that genuinely measured zero.
154 let action = StepResult {
155 name: "Initialize".to_owned(),
156 step_type: "Action".to_owned(),
157 status: "Done".to_owned(),
158 value: None,
159 };
160 let measured = StepResult {
161 value: Some(ResultValue::Number(0.0)),
162 ..action.clone()
163 };
164 assert_ne!(action.value, measured.value);
165 assert!(action.value.is_none());
166 }
167
168 #[test]
169 fn a_text_measurement_is_kept_as_text() {
170 assert_eq!(
171 ResultValue::Text("SN-001".to_owned()),
172 ResultValue::Text("SN-001".to_owned())
173 );
174 assert_ne!(
175 ResultValue::Text("1.5".to_owned()),
176 ResultValue::Number(1.5)
177 );
178 }
179}