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}