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    /// Whether each loop iteration gets its own result (`Step.RecordLoopIterationResults`).
239    ///
240    /// # Errors
241    /// [`Error`] if the COM call fails or returns an unexpected type.
242    pub fn record_loop_iteration_results(&self) -> Result<bool, Error> {
243        Ok(self
244            .dispatch
245            .get(step::RECORD_LOOP_ITERATION_RESULTS)?
246            .as_bool()?)
247    }
248
249    /// Sets whether each loop iteration gets its own result
250    /// (`Step.RecordLoopIterationResults`).
251    ///
252    /// # Errors
253    /// [`Error`] if the COM call fails.
254    pub fn set_record_loop_iteration_results(&self, record: bool) -> Result<(), Error> {
255        self.dispatch
256            .put(step::RECORD_LOOP_ITERATION_RESULTS, Value::Bool(record))?;
257        Ok(())
258    }
259
260    /// The step as a property tree (`Step.AsPropertyObject`).
261    ///
262    /// Type-specific settings live here, addressed by lookup path, /// `Limits.High` on a numeric limit test, for instance.
263    ///
264    /// # Errors
265    /// [`Error`] if the COM call fails or returns an unexpected type.
266    pub fn as_property_object(&self) -> Result<PropertyObject, Error> {
267        Ok(PropertyObject::new(
268            self.dispatch
269                .call(step::AS_PROPERTY_OBJECT, &[])?
270                .into_object()?,
271        ))
272    }
273
274    /// The step's type definition (`Step.StepType`).
275    ///
276    /// # Errors
277    /// [`Error`] if the COM call fails or returns an unexpected type.
278    pub fn step_type(&self) -> Result<PropertyObject, Error> {
279        Ok(PropertyObject::new(
280            self.dispatch.get(step::STEP_TYPE)?.into_object()?,
281        ))
282    }
283
284    /// An owned handle to the same step, for passing it back to the engine.
285    pub(crate) fn duplicate_dispatch(&self) -> Option<Box<dyn Dispatch>> {
286        self.dispatch.duplicate()
287    }
288    /// Whether this step carries a breakpoint (`Step.BreakOnStep`).
289    ///
290    /// Reads the step itself. To ask about one run instead, use
291    /// [`break_on_step_for`](Self::break_on_step_for).
292    ///
293    /// True here does not mean a run will stop. Breakpoints are only honored
294    /// while they are switched on, which
295    /// [`Engine::breakpoints_enabled`](crate::Engine::breakpoints_enabled)
296    /// controls for the session.
297    ///
298    /// # Errors
299    /// [`Error`] if the COM call fails or returns an unexpected type.
300    pub fn break_on_step(&self) -> Result<bool, Error> {
301        Ok(self.dispatch.get(step::BREAK_ON_STEP)?.as_bool()?)
302    }
303
304    /// Whether this step carries a breakpoint in the given scope
305    /// (`Step.GetBreakOnStepEx`).
306    ///
307    /// # Errors
308    /// [`Error`] if the COM call fails or returns an unexpected type.
309    pub fn break_on_step_for(&self, scope: BreakpointScope<'_>) -> Result<bool, Error> {
310        Ok(self
311            .dispatch
312            .call(step::GET_BREAK_ON_STEP_EX, &[scope.argument()])?
313            .as_bool()?)
314    }
315
316    /// Sets or clears the breakpoint on this step (`Step.SetBreakOnStepEx`).
317    ///
318    /// The scope decides how long it lasts.
319    /// [`BreakpointScope::Step`] writes it into
320    /// the step, so it survives the run and is saved with the sequence file.
321    /// [`BreakpointScope::Execution`] scopes
322    /// it to one run and leaves the file alone, which is what a host debugging
323    /// for a remote panel should use.
324    ///
325    /// A stop announces itself as
326    /// [`UIMessageCode::BreakOnBreakpoint`](crate::UIMessageCode::BreakOnBreakpoint),
327    /// which arrived about 300 ms after the run started in a live measurement.
328    /// Continue with [`Execution::resume`](crate::Execution::resume), not
329    /// `Thread::resume`, which does not release a breakpoint stop.
330    ///
331    /// # Errors
332    /// [`Error`] if the COM call fails.
333    pub fn set_break_on_step(
334        &self,
335        enabled: bool,
336        scope: BreakpointScope<'_>,
337    ) -> Result<(), Error> {
338        self.dispatch.call(
339            step::SET_BREAK_ON_STEP_EX,
340            &[Value::Bool(enabled), scope.argument()],
341        )?;
342        Ok(())
343    }
344
345    /// Sets a breakpoint together with its pass count and condition
346    /// (`Step.SetBreakSettings`).
347    ///
348    /// `is_set` places or removes the breakpoint and `enabled` decides whether
349    /// it is armed, so a breakpoint can stay in place while switched off.
350    /// `pass_count` stops on the nth arrival rather than the first.
351    /// `condition` is an expression the engine evaluates when it arrives; an
352    /// empty string means stop unconditionally.
353    ///
354    /// Reading these back needs `Step.GetBreakSettings`, which returns
355    /// everything through `[out]` parameters and is not wrapped yet.
356    ///
357    /// # Errors
358    /// [`Error`] if the COM call fails.
359    pub fn set_break_settings(
360        &self,
361        is_set: bool,
362        enabled: bool,
363        pass_count: i32,
364        condition: &str,
365        scope: BreakpointScope<'_>,
366    ) -> Result<(), Error> {
367        self.dispatch.call(
368            step::SET_BREAK_SETTINGS,
369            &[
370                Value::Bool(is_set),
371                Value::Bool(enabled),
372                Value::I32(pass_count),
373                Value::Str(condition.to_owned()),
374                scope.argument(),
375            ],
376        )?;
377        Ok(())
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use std::cell::RefCell;
384    use std::collections::HashMap;
385    use std::rc::Rc;
386
387    use rs_teststand_sys::{ComError, Dispatch, Value};
388
389    use super::{BreakpointScope, Step};
390    use crate::dispids::step as dispid;
391    use crate::error::Error;
392
393    /// Shared with the test, because `Step` takes the dispatch by value.
394    type Sent = Rc<RefCell<Vec<(i32, usize)>>>;
395
396    /// Answers reads from a script and records every call.
397    #[derive(Debug)]
398    struct FakeDispatch {
399        reads: HashMap<i32, bool>,
400        sent: Sent,
401    }
402
403    impl Dispatch for FakeDispatch {
404        fn get(&self, dispid: i32) -> Result<Value, ComError> {
405            self.reads.get(&dispid).map_or_else(
406                || Err(ComError::hresult(0, "fake: unscripted")),
407                |flag| Ok(Value::Bool(*flag)),
408            )
409        }
410
411        fn put(&self, _dispid: i32, _value: Value) -> Result<(), ComError> {
412            Err(ComError::hresult(0, "fake: put not scripted"))
413        }
414
415        fn call(&self, dispid: i32, args: &[Value]) -> Result<Value, ComError> {
416            self.sent.borrow_mut().push((dispid, args.len()));
417            Ok(Value::Bool(true))
418        }
419    }
420
421    fn step_recording(reads: HashMap<i32, bool>) -> (Step, Sent) {
422        let sent: Sent = Rc::default();
423        let dispatch = FakeDispatch {
424            reads,
425            sent: Rc::clone(&sent),
426        };
427        (Step::new(Box::new(dispatch)), sent)
428    }
429
430    #[test]
431    fn setting_a_run_mode_sends_the_mode_and_the_scope() -> Result<(), Error> {
432        // Two arguments, like the breakpoint pair, and for the same reason: an
433        // omitted execution is what tells the engine to edit the step itself.
434        let (step, sent) = step_recording(HashMap::new());
435        step.set_run_mode_ex(crate::RunMode::Skip, BreakpointScope::Step)?;
436        assert_eq!(
437            sent.borrow().as_slice(),
438            [(dispid::SET_RUN_MODE_EX, 2)],
439            "expected one call carrying the mode and the scope"
440        );
441        Ok(())
442    }
443
444    #[test]
445    fn reading_a_run_mode_sends_only_the_scope() {
446        let (step, sent) = step_recording(HashMap::new());
447        // The fake answers a bool, so decoding fails; the call is what matters.
448        let _ = step.run_mode_ex(BreakpointScope::Step);
449        assert_eq!(
450            sent.borrow().as_slice(),
451            [(dispid::GET_RUN_MODE_EX, 1)],
452            "the getter takes the scope and nothing else"
453        );
454    }
455
456    #[test]
457    fn break_on_step_reads_the_property() -> Result<(), Error> {
458        let (step, _) = step_recording(std::iter::once((dispid::BREAK_ON_STEP, true)).collect());
459        assert!(step.break_on_step()?);
460        Ok(())
461    }
462
463    #[test]
464    fn setting_a_breakpoint_sends_the_flag_and_the_scope() -> Result<(), Error> {
465        // Two arguments, always. The scope goes even when it is absent, because
466        // the engine reads an omitted execution differently from a null one.
467        let (step, sent) = step_recording(HashMap::new());
468        step.set_break_on_step(true, BreakpointScope::Step)?;
469        assert_eq!(
470            sent.borrow().as_slice(),
471            [(dispid::SET_BREAK_ON_STEP_EX, 2)],
472            "expected one call carrying the flag and the scope"
473        );
474        Ok(())
475    }
476
477    #[test]
478    fn break_settings_sends_all_five_arguments() -> Result<(), Error> {
479        // A short count is DISP_E_BADPARAMCOUNT on a live engine, which is the
480        // failure this pins.
481        let (step, sent) = step_recording(HashMap::new());
482        step.set_break_settings(true, true, 3, "Locals.Counter == 2", BreakpointScope::Step)?;
483        assert_eq!(
484            sent.borrow().as_slice(),
485            [(dispid::SET_BREAK_SETTINGS, 5)],
486            "the engine declares five input parameters"
487        );
488        Ok(())
489    }
490}