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}