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