Skip to main content

execution_run_test_headless/
execution_run_test_headless.rs

1//! Example: run real pass/fail tests with no user interface and read the numbers back.
2//!
3//! ```text
4//! cargo run --example execution_run_test_headless
5//! ```
6//!
7//! The usual headless goal: drive a sequence with nothing watching, then read
8//! per-step pass/fail and the measured value straight out of the results tree
9//! rather than out of a report file.
10//!
11//! Two things make a test step runnable without a code module:
12//!
13//! * Build it with [`AdapterKeyName::NoneAdapter`]. An **empty** adapter key
14//!   does not mean "no code module", it lets the step type choose, falling
15//!   back to the station default, and a step that ends up on a real adapter
16//!   fails at run time with "module has not yet been specified".
17//! * Set `DataSource`, the expression the step reads its measurement from. It
18//!   defaults to `Step.Result.Numeric`, which is `0` with no code module, so a
19//!   literal stands in here for what an instrument would return.
20//!
21//! The step then compares that value against `Limits.Low` / `Limits.High` and
22//! records Passed or Failed.
23//!
24//! Expect **fewer results than tests**: a failing step ends the run early on a
25//! station configured to go to Cleanup on failure, so the steps after it never
26//! execute and record nothing. That is normal, and a headless host must not
27//! read a short result list as a complete one, which is why this prints how
28//! many of the defined tests actually reported.
29//!
30//! Waiting is done by draining the message queue rather than by
31//! `wait_for_end_ex`, because a headless host owes two duties at once: pump the
32//! thread's Windows messages so COM can deliver into the apartment, and drain
33//! the engine's queue so a synchronous poster is released. A deadline keeps a
34//! stuck run from hanging the process.
35
36use std::time::{Duration, Instant};
37
38use rs_teststand::{
39    AdapterKeyName, Engine, Error, PropertyOptions, Sequence, StepGroup, UIMessageCode,
40    pump_thread_messages,
41};
42
43/// name, measurement expression, low limit, high limit.
44const TESTS: [(&str, &str, f64, f64); 3] = [
45    ("Supply Voltage", "5.0", 4.75, 5.25),
46    ("Bias Current", "9.0", 1.0, 8.0),
47    ("Reference Clock", "10.0", 9.5, 10.5),
48];
49
50/// How long to let the run take before giving up on it.
51const RUN_DEADLINE: Duration = Duration::from_secs(60);
52
53const fn none() -> i32 {
54    PropertyOptions::NONE.bits()
55}
56
57const fn insert_if_missing() -> i32 {
58    PropertyOptions::INSERT_IF_MISSING.bits()
59}
60
61/// Appends a numeric limit test that can run without a code module.
62fn add_numeric_limit_test(
63    engine: &Engine,
64    sequence: &Sequence,
65    name: &str,
66    data_source: &str,
67    low: f64,
68    high: f64,
69) -> Result<(), Error> {
70    let step = engine.new_step(AdapterKeyName::NoneAdapter.as_str(), "NumericLimitTest")?;
71    step.set_name(name)?;
72    step.set_record_result(true)?;
73
74    let properties = step.as_property_object()?;
75    properties.set_val_string("DataSource", insert_if_missing(), data_source)?;
76    properties.set_val_number("Limits.Low", insert_if_missing(), low)?;
77    properties.set_val_number("Limits.High", insert_if_missing(), high)?;
78
79    sequence.insert_step(
80        &step,
81        sequence.get_num_steps(StepGroup::Main)?,
82        StepGroup::Main,
83    )
84}
85
86/// Waits for the run to finish, pumping and draining as it goes.
87///
88/// Returns `false` if the deadline passed first, which a host should treat as a
89/// stuck run rather than a finished one.
90fn wait_for_end(engine: &Engine, deadline: Duration) -> Result<bool, Error> {
91    let started = Instant::now();
92    while started.elapsed() < deadline {
93        if pump_thread_messages() {
94            return Ok(false);
95        }
96        while !engine.is_ui_message_queue_empty()? {
97            let message = engine.get_ui_message()?;
98            let ended = matches!(
99                UIMessageCode::from_bits(message.event()?),
100                Ok(UIMessageCode::EndExecution)
101            );
102            // Acknowledging is what releases a synchronous poster; skipping it
103            // stalls the sequence rather than merely losing a notification.
104            message.acknowledge()?;
105            if ended {
106                return Ok(true);
107            }
108        }
109    }
110    Ok(false)
111}
112
113/// Prints one line per recorded step result.
114fn report(results: &rs_teststand::PropertyObject) -> Result<(), Error> {
115    if !results.exists("ResultList", none())? {
116        println!("No results recorded.");
117        return Ok(());
118    }
119    let result_list = results.get_property_object("ResultList", none())?;
120    let count = result_list.get_num_elements()?;
121    println!(
122        "\n{count} of {} defined test(s) recorded a result:",
123        TESTS.len()
124    );
125    if count < i32::try_from(TESTS.len()).unwrap_or(i32::MAX) {
126        println!("  (a failure ended the run before the rest could execute)");
127    }
128
129    for index in 0..count {
130        let entry = result_list.get_property_object_by_offset(index, none())?;
131        let name = entry
132            .get_val_string("TS.StepName", none())
133            .unwrap_or_else(|_| "<unnamed>".to_owned());
134        let status = entry
135            .get_val_string("Status", none())
136            .unwrap_or_else(|_| "<no status>".to_owned());
137        // Only test steps record a measurement; an Action has no Numeric.
138        match entry.get_val_number("Numeric", none()) {
139            Ok(measured) => println!("  {name}: {status} (measured {measured})"),
140            Err(_) => println!("  {name}: {status}"),
141        }
142    }
143    Ok(())
144}
145
146fn main() -> Result<(), Box<dyn std::error::Error>> {
147    let engine = Engine::new()?;
148    // Nothing reaches the queue until this is on, and without the queue there
149    // is no way to know the run ended.
150    engine.set_ui_message_polling_enabled(true)?;
151
152    let sequence_file = engine.new_sequence_file()?;
153    let main_sequence = sequence_file.get_sequence_by_name("MainSequence")?;
154    for (name, data_source, low, high) in TESTS {
155        add_numeric_limit_test(&engine, &main_sequence, name, data_source, low, high)?;
156    }
157
158    let execution = engine.new_execution(&sequence_file, "MainSequence", None, false, 0)?;
159    println!("Running {} headless...", execution.display_name()?);
160
161    if wait_for_end(&engine, RUN_DEADLINE)? {
162        println!("Finished with status: {}", execution.result_status()?);
163        report(&execution.result_object()?)?;
164    } else {
165        // Reported rather than ignored: a host that assumes success here would
166        // publish results from a run that never finished.
167        println!("The run did not finish within {RUN_DEADLINE:?}; terminating.");
168        execution.terminate()?;
169    }
170
171    engine.release_sequence_file_ex(sequence_file, none())?;
172    Ok(())
173}