Skip to main content

result_list_parse/
result_list_parse.rs

1//! Example: run a sequence and read its results as plain data.
2//!
3//! ```text
4//! cargo run --example result_list_parse
5//! ```
6//!
7//! Getting results out is what turns a headless run into something a report or
8//! a database can consume. The engine records them into a `ResultList` array,
9//! one entry per step whose recording is enabled, and
10//! [`ResultList::parse`](rs_teststand::ResultList::parse) walks that into
11//! ordinary Rust values that outlive the execution.
12//!
13//! The sequence is built here so the example depends on nothing installed. It
14//! deliberately contains a step with recording **disabled**, because that is
15//! the usual reason a parsed report has fewer entries than the sequence has
16//! steps, and it is worth seeing rather than discovering later.
17
18use std::time::{Duration, Instant};
19
20use rs_teststand::{
21    AdapterKeyName, Engine, Error, PropertyOptions, ResultRecordingOption, ResultValue, Sequence,
22    StepGroup, UIMessageCode, pump_thread_messages,
23};
24
25const RUN_DEADLINE: Duration = Duration::from_secs(30);
26
27const fn none() -> i32 {
28    PropertyOptions::NONE.bits()
29}
30
31const fn insert_if_missing() -> i32 {
32    PropertyOptions::INSERT_IF_MISSING.bits()
33}
34
35/// Adds a pass/fail test whose outcome is fixed by its data source.
36fn add_pass_fail(
37    engine: &Engine,
38    sequence: &Sequence,
39    name: &str,
40    source: &str,
41) -> Result<(), Error> {
42    let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "PassFailTest")?;
43    step.set_name(name)?;
44    step.as_property_object()?
45        .set_val_string("DataSource", insert_if_missing(), source)?;
46    sequence.insert_step(
47        &step,
48        sequence.get_num_steps(StepGroup::Main)?,
49        StepGroup::Main,
50    )
51}
52
53/// Adds a numeric limit test with a fixed measurement.
54fn add_numeric(
55    engine: &Engine,
56    sequence: &Sequence,
57    name: &str,
58    source: &str,
59    low: f64,
60    high: f64,
61) -> Result<(), Error> {
62    let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "NumericLimitTest")?;
63    step.set_name(name)?;
64    let properties = step.as_property_object()?;
65    properties.set_val_string("DataSource", insert_if_missing(), source)?;
66    properties.set_val_number("Limits.Low", insert_if_missing(), low)?;
67    properties.set_val_number("Limits.High", insert_if_missing(), high)?;
68    sequence.insert_step(
69        &step,
70        sequence.get_num_steps(StepGroup::Main)?,
71        StepGroup::Main,
72    )
73}
74
75/// Adds a statement step that records nothing, to show the gap it leaves.
76fn add_unrecorded_filler(engine: &Engine, sequence: &Sequence, name: &str) -> Result<(), Error> {
77    let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "Statement")?;
78    step.set_name(name)?;
79    step.set_result_recording_option(ResultRecordingOption::Disabled)?;
80    sequence.insert_step(
81        &step,
82        sequence.get_num_steps(StepGroup::Main)?,
83        StepGroup::Main,
84    )
85}
86
87/// Waits for the run, pumping the thread's messages and draining the engine's.
88fn wait_for_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
89    let started = Instant::now();
90    while started.elapsed() < deadline {
91        if pump_thread_messages() {
92            return Ok(false);
93        }
94        while !engine.is_ui_message_queue_empty()? {
95            let message = engine.get_ui_message()?;
96            let ended = matches!(
97                UIMessageCode::from_bits(message.event()?),
98                Ok(UIMessageCode::EndExecution)
99            );
100            message.acknowledge()?;
101            if ended {
102                return Ok(true);
103            }
104        }
105    }
106    Ok(false)
107}
108
109fn main() -> Result<(), Box<dyn std::error::Error>> {
110    let engine = Engine::new()?;
111    engine.set_ui_message_polling_enabled(true)?;
112
113    let sequence_file = engine.new_sequence_file()?;
114    let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
115
116    add_pass_fail(&engine, &main_sequence, "Verify Power Rail", "True")?;
117    add_unrecorded_filler(&engine, &main_sequence, "Filler (not recorded)")?;
118    add_numeric(
119        &engine,
120        &main_sequence,
121        "Verify Current Draw",
122        "1.5",
123        1.0,
124        2.0,
125    )?;
126    add_pass_fail(&engine, &main_sequence, "Verify Ground Bond", "True")?;
127
128    let step_count = main_sequence.get_num_steps(StepGroup::Main)?;
129    println!("Running {step_count} steps...");
130    let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
131
132    if !wait_for_end(&engine, RUN_DEADLINE)? {
133        println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
134        execution.terminate()?;
135        return Ok(());
136    }
137    println!("Finished with status: {}\n", execution.result_status()?);
138
139    let results = execution.result_list()?;
140    let parsed = results.parse()?;
141
142    println!("{} of {step_count} steps recorded a result:", parsed.len());
143    for (index, result) in parsed.iter().enumerate() {
144        let measurement = match &result.value {
145            Some(ResultValue::Number(number)) => format!("measured {number}"),
146            Some(ResultValue::Text(text)) => format!("measured {text:?}"),
147            None => "no measurement".to_owned(),
148        };
149        println!(
150            "  [{index}] {} ({}) -> {}, {measurement}",
151            result.name, result.step_type, result.status
152        );
153    }
154
155    // The step whose recording was switched off is absent, by design.
156    println!(
157        "\nThe unrecorded step left no entry, which is why {} < {step_count}.",
158        parsed.len()
159    );
160
161    // The parsed results are plain data, so they outlive the run and can be
162    // handed to anything: a report writer, a database, a serde format.
163    let failures: Vec<&str> = parsed
164        .iter()
165        .filter(|result| result.status == "Failed")
166        .map(|result| result.name.as_str())
167        .collect();
168    if failures.is_empty() {
169        println!("No failures.");
170    } else {
171        println!("Failed steps: {}", failures.join(", "));
172    }
173
174    engine.release_sequence_file_ex(sequence_file, none())?;
175    Ok(())
176}