Skip to main content

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}