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}