Skip to main content

rs_teststand/sequence/
step.rs

1//! A single step in a sequence.
2
3use rs_teststand_sys::{Dispatch, Value};
4
5use crate::Error;
6use crate::dispids::step;
7use crate::property::PropertyObject;
8
9/// One step of a sequence (`Step`).
10///
11/// Built by [`Engine::new_step`](crate::Engine::new_step) and placed with
12/// [`Sequence::insert_step`](crate::Sequence::insert_step).
13///
14/// This type carries the properties every step has, whatever its type. Anything
15/// specific to a step type, a numeric limit test's limits, for instance, /// lives in the property tree reached through
16/// [`as_property_object`](Self::as_property_object).
17#[derive(Debug)]
18pub struct Step {
19    dispatch: Box<dyn Dispatch>,
20}
21
22impl Step {
23    /// Wraps a dispatch handle returned by the engine.
24    pub(crate) fn new(dispatch: Box<dyn Dispatch>) -> Self {
25        Self { dispatch }
26    }
27
28    /// The step's name (`Step.Name`).
29    ///
30    /// # Errors
31    /// [`Error`] if the COM call fails or returns an unexpected type.
32    pub fn name(&self) -> Result<String, Error> {
33        Ok(self.dispatch.get(step::NAME)?.into_string()?)
34    }
35
36    /// Sets the step's name (`Step.Name`).
37    ///
38    /// # Errors
39    /// [`Error`] if the COM call fails.
40    pub fn set_name(&self, name: &str) -> Result<(), Error> {
41        self.dispatch.put(step::NAME, Value::Str(name.to_owned()))?;
42        Ok(())
43    }
44
45    /// The expression deciding whether the step runs (`Step.Precondition`).
46    ///
47    /// An empty precondition means the step always runs.
48    ///
49    /// # Errors
50    /// [`Error`] if the COM call fails or returns an unexpected type.
51    pub fn precondition(&self) -> Result<String, Error> {
52        Ok(self.dispatch.get(step::PRECONDITION)?.into_string()?)
53    }
54
55    /// Sets the precondition expression (`Step.Precondition`).
56    ///
57    /// The text is not checked here; a precondition that does not parse fails
58    /// when the sequence runs, not when it is set.
59    ///
60    /// # Errors
61    /// [`Error`] if the COM call fails.
62    pub fn set_precondition(&self, expression: &str) -> Result<(), Error> {
63        self.dispatch
64            .put(step::PRECONDITION, Value::Str(expression.to_owned()))?;
65        Ok(())
66    }
67
68    /// What the engine does with the step when it reaches it (`Step.RunMode`).
69    ///
70    /// `None` means the engine reported a mode this build does not name, which
71    /// is worth telling apart from a failure to read it at all.
72    ///
73    /// # Errors
74    /// [`Error`] if the COM call fails or returns an unexpected type.
75    pub fn run_mode(&self) -> Result<Option<crate::RunMode>, Error> {
76        let raw = self.dispatch.get(step::RUN_MODE)?.into_string()?;
77        Ok(crate::RunMode::from_value(&raw))
78    }
79
80    /// Sets the run mode (`Step.RunMode`).
81    ///
82    /// # Errors
83    /// [`Error`] if the COM call fails.
84    pub fn set_run_mode(&self, mode: crate::RunMode) -> Result<(), Error> {
85        self.dispatch
86            .put(step::RUN_MODE, Value::Str(mode.as_str().to_owned()))?;
87        Ok(())
88    }
89
90    /// The adapter the step calls its code module through
91    /// (`Step.AdapterKeyName`).
92    ///
93    /// `None` means the engine reported a key this build does not name, or the
94    /// step calls no code module at all.
95    ///
96    /// # Errors
97    /// [`Error`] if the COM call fails or returns an unexpected type.
98    pub fn adapter_key_name(&self) -> Result<Option<crate::AdapterKeyName>, Error> {
99        let raw = self.dispatch.get(step::ADAPTER_KEY_NAME)?.into_string()?;
100        Ok(crate::AdapterKeyName::from_key(&raw))
101    }
102
103    /// The expression evaluated after the step runs (`Step.PostExpression`).
104    ///
105    /// An empty expression means nothing runs afterwards.
106    ///
107    /// # Errors
108    /// [`Error`] if the COM call fails or returns an unexpected type.
109    pub fn post_expression(&self) -> Result<String, Error> {
110        Ok(self.dispatch.get(step::POST_EXPRESSION)?.into_string()?)
111    }
112
113    /// Sets the post expression (`Step.PostExpression`).
114    ///
115    /// Like a precondition, the text is not checked here: an expression that
116    /// does not parse fails when the sequence runs, not when it is set.
117    ///
118    /// # Errors
119    /// [`Error`] if the COM call fails.
120    pub fn set_post_expression(&self, expression: &str) -> Result<(), Error> {
121        self.dispatch
122            .put(step::POST_EXPRESSION, Value::Str(expression.to_owned()))?;
123        Ok(())
124    }
125
126    /// Gives the step a fresh unique identity (`Step.CreateNewUniqueStepId`).
127    ///
128    /// A copy of a step carries the original's step ID, so a sequence built by
129    /// cloning a prototype ends up with several steps claiming the same
130    /// identity. Anything that refers to a step by ID, a result, a report
131    /// entry, a `GoTo`, then cannot tell them apart. Call this on each copy.
132    ///
133    /// # Errors
134    /// [`Error`] if the COM call fails.
135    pub fn create_new_unique_step_id(&self) -> Result<(), Error> {
136        self.dispatch.call(step::CREATE_NEW_UNIQUE_STEP_ID, &[])?;
137        Ok(())
138    }
139
140    /// Whether this step contributes an entry to the result list
141    /// (`Step.ResultRecordingOption`).
142    ///
143    /// Distinct from [`record_result`](Self::record_result), the plain on/off
144    /// switch: this one can also say "record even when the sequence says not
145    /// to". A step set to [`Disabled`](crate::ResultRecordingOption::Disabled)
146    /// leaves no entry in `ResultList`, which is the usual reason a parsed
147    /// report is shorter than the sequence that produced it.
148    ///
149    /// # Errors
150    /// [`Error`] if the COM call fails or the engine reports an unnamed value.
151    pub fn result_recording_option(&self) -> Result<crate::ResultRecordingOption, Error> {
152        crate::ResultRecordingOption::from_bits(
153            self.dispatch.get(step::RESULT_RECORDING_OPTION)?.as_i32()?,
154        )
155    }
156
157    /// Sets whether this step records a result (`Step.ResultRecordingOption`).
158    ///
159    /// # Errors
160    /// [`Error`] if the COM call fails.
161    pub fn set_result_recording_option(
162        &self,
163        option: crate::ResultRecordingOption,
164    ) -> Result<(), Error> {
165        self.dispatch
166            .put(step::RESULT_RECORDING_OPTION, Value::I32(option as i32))?;
167        Ok(())
168    }
169
170    /// Whether the step's result is recorded (`Step.RecordResult`).
171    ///
172    /// # Errors
173    /// [`Error`] if the COM call fails or returns an unexpected type.
174    pub fn record_result(&self) -> Result<bool, Error> {
175        Ok(self.dispatch.get(step::RECORD_RESULT)?.as_bool()?)
176    }
177
178    /// Sets whether the step's result is recorded (`Step.RecordResult`).
179    ///
180    /// # Errors
181    /// [`Error`] if the COM call fails.
182    pub fn set_record_result(&self, record: bool) -> Result<(), Error> {
183        self.dispatch
184            .put(step::RECORD_RESULT, Value::Bool(record))?;
185        Ok(())
186    }
187
188    /// The step as a property tree (`Step.AsPropertyObject`).
189    ///
190    /// Type-specific settings live here, addressed by lookup path, /// `Limits.High` on a numeric limit test, for instance.
191    ///
192    /// # Errors
193    /// [`Error`] if the COM call fails or returns an unexpected type.
194    pub fn as_property_object(&self) -> Result<PropertyObject, Error> {
195        Ok(PropertyObject::new(
196            self.dispatch
197                .call(step::AS_PROPERTY_OBJECT, &[])?
198                .into_object()?,
199        ))
200    }
201
202    /// The step's type definition (`Step.StepType`).
203    ///
204    /// # Errors
205    /// [`Error`] if the COM call fails or returns an unexpected type.
206    pub fn step_type(&self) -> Result<PropertyObject, Error> {
207        Ok(PropertyObject::new(
208            self.dispatch.get(step::STEP_TYPE)?.into_object()?,
209        ))
210    }
211
212    /// An owned handle to the same step, for passing it back to the engine.
213    pub(crate) fn duplicate_dispatch(&self) -> Option<Box<dyn Dispatch>> {
214        self.dispatch.duplicate()
215    }
216    /// Whether this step carries a breakpoint (`Step.BreakOnStep`).
217    ///
218    /// Reads the step itself. To ask about one run instead, use
219    /// [`break_on_step_for`](Self::break_on_step_for).
220    ///
221    /// True here does not mean a run will stop. Breakpoints are only honored
222    /// while they are switched on, which
223    /// [`Engine::breakpoints_enabled`](crate::Engine::breakpoints_enabled)
224    /// controls for the session.
225    ///
226    /// # Errors
227    /// [`Error`] if the COM call fails or returns an unexpected type.
228    pub fn break_on_step(&self) -> Result<bool, Error> {
229        Ok(self.dispatch.get(step::BREAK_ON_STEP)?.as_bool()?)
230    }
231
232    /// Whether this step carries a breakpoint in the given scope
233    /// (`Step.GetBreakOnStepEx`).
234    ///
235    /// # Errors
236    /// [`Error`] if the COM call fails or returns an unexpected type.
237    pub fn break_on_step_for(&self, scope: crate::BreakpointScope<'_>) -> Result<bool, Error> {
238        Ok(self
239            .dispatch
240            .call(step::GET_BREAK_ON_STEP_EX, &[scope.argument()])?
241            .as_bool()?)
242    }
243
244    /// Sets or clears the breakpoint on this step (`Step.SetBreakOnStepEx`).
245    ///
246    /// The scope decides how long it lasts.
247    /// [`BreakpointScope::Step`](crate::BreakpointScope::Step) writes it into
248    /// the step, so it survives the run and is saved with the sequence file.
249    /// [`BreakpointScope::Execution`](crate::BreakpointScope::Execution) scopes
250    /// it to one run and leaves the file alone, which is what a host debugging
251    /// for a remote panel should use.
252    ///
253    /// A stop announces itself as
254    /// [`UIMessageCode::BreakOnBreakpoint`](crate::UIMessageCode::BreakOnBreakpoint),
255    /// which arrived about 300 ms after the run started in a live measurement.
256    /// Continue with [`Execution::resume`](crate::Execution::resume), not
257    /// `Thread::resume`, which does not release a breakpoint stop.
258    ///
259    /// # Errors
260    /// [`Error`] if the COM call fails.
261    pub fn set_break_on_step(
262        &self,
263        enabled: bool,
264        scope: crate::BreakpointScope<'_>,
265    ) -> Result<(), Error> {
266        self.dispatch.call(
267            step::SET_BREAK_ON_STEP_EX,
268            &[Value::Bool(enabled), scope.argument()],
269        )?;
270        Ok(())
271    }
272
273    /// Sets a breakpoint together with its pass count and condition
274    /// (`Step.SetBreakSettings`).
275    ///
276    /// `is_set` places or removes the breakpoint and `enabled` decides whether
277    /// it is armed, so a breakpoint can stay in place while switched off.
278    /// `pass_count` stops on the nth arrival rather than the first.
279    /// `condition` is an expression the engine evaluates when it arrives; an
280    /// empty string means stop unconditionally.
281    ///
282    /// Reading these back needs `Step.GetBreakSettings`, which returns
283    /// everything through `[out]` parameters and is not wrapped yet.
284    ///
285    /// # Errors
286    /// [`Error`] if the COM call fails.
287    pub fn set_break_settings(
288        &self,
289        is_set: bool,
290        enabled: bool,
291        pass_count: i32,
292        condition: &str,
293        scope: crate::BreakpointScope<'_>,
294    ) -> Result<(), Error> {
295        self.dispatch.call(
296            step::SET_BREAK_SETTINGS,
297            &[
298                Value::Bool(is_set),
299                Value::Bool(enabled),
300                Value::I32(pass_count),
301                Value::Str(condition.to_owned()),
302                scope.argument(),
303            ],
304        )?;
305        Ok(())
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use std::cell::RefCell;
312    use std::collections::HashMap;
313    use std::rc::Rc;
314
315    use rs_teststand_sys::{ComError, Dispatch, Value};
316
317    use super::Step;
318    use crate::BreakpointScope;
319    use crate::dispids::step as dispid;
320    use crate::error::Error;
321
322    /// Shared with the test, because `Step` takes the dispatch by value.
323    type Sent = Rc<RefCell<Vec<(i32, usize)>>>;
324
325    /// Answers reads from a script and records every call.
326    #[derive(Debug)]
327    struct FakeDispatch {
328        reads: HashMap<i32, bool>,
329        sent: Sent,
330    }
331
332    impl Dispatch for FakeDispatch {
333        fn get(&self, dispid: i32) -> Result<Value, ComError> {
334            self.reads.get(&dispid).map_or_else(
335                || Err(ComError::hresult(0, "fake: unscripted")),
336                |flag| Ok(Value::Bool(*flag)),
337            )
338        }
339
340        fn put(&self, _dispid: i32, _value: Value) -> Result<(), ComError> {
341            Err(ComError::hresult(0, "fake: put not scripted"))
342        }
343
344        fn call(&self, dispid: i32, args: &[Value]) -> Result<Value, ComError> {
345            self.sent.borrow_mut().push((dispid, args.len()));
346            Ok(Value::Bool(true))
347        }
348    }
349
350    fn step_recording(reads: HashMap<i32, bool>) -> (Step, Sent) {
351        let sent: Sent = Rc::default();
352        let dispatch = FakeDispatch {
353            reads,
354            sent: Rc::clone(&sent),
355        };
356        (Step::new(Box::new(dispatch)), sent)
357    }
358
359    #[test]
360    fn break_on_step_reads_the_property() -> Result<(), Error> {
361        let (step, _) = step_recording(std::iter::once((dispid::BREAK_ON_STEP, true)).collect());
362        assert!(step.break_on_step()?);
363        Ok(())
364    }
365
366    #[test]
367    fn setting_a_breakpoint_sends_the_flag_and_the_scope() -> Result<(), Error> {
368        // Two arguments, always. The scope goes even when it is absent, because
369        // the engine reads an omitted execution differently from a null one.
370        let (step, sent) = step_recording(HashMap::new());
371        step.set_break_on_step(true, BreakpointScope::Step)?;
372        assert_eq!(
373            sent.borrow().as_slice(),
374            [(dispid::SET_BREAK_ON_STEP_EX, 2)],
375            "expected one call carrying the flag and the scope"
376        );
377        Ok(())
378    }
379
380    #[test]
381    fn break_settings_sends_all_five_arguments() -> Result<(), Error> {
382        // A short count is DISP_E_BADPARAMCOUNT on a live engine, which is the
383        // failure this pins.
384        let (step, sent) = step_recording(HashMap::new());
385        step.set_break_settings(true, true, 3, "Locals.Counter == 2", BreakpointScope::Step)?;
386        assert_eq!(
387            sent.borrow().as_slice(),
388            [(dispid::SET_BREAK_SETTINGS, 5)],
389            "the engine declares five input parameters"
390        );
391        Ok(())
392    }
393}