Skip to main content

rs_teststand/sequence/
sequence.rs

1//! Safe wrapper for a sequence within a sequence file.
2
3use rs_teststand_sys::{Dispatch, Value};
4
5use crate::Error;
6use crate::dispids::sequence;
7use crate::property::PropertyObject;
8
9/// One sequence inside a [`SequenceFile`](crate::SequenceFile).
10///
11/// A sequence owns two of the four variable scopes:
12///
13/// * [`locals`](Self::locals), storage private to one call of this sequence.
14/// * [`parameters`](Self::parameters), values the caller supplies.
15///
16/// The other two live elsewhere: file globals on the sequence file, and station
17/// globals on the engine.
18#[derive(Debug)]
19pub struct Sequence {
20    dispatch: Box<dyn Dispatch>,
21}
22
23impl Sequence {
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 sequence's name (`Sequence.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(sequence::NAME)?.into_string()?)
35    }
36
37    /// Renames the sequence (`Sequence.Name`).
38    ///
39    /// # Errors
40    /// [`Error`] if the COM call fails.
41    pub fn set_name(&self, name: &str) -> Result<(), Error> {
42        self.dispatch
43            .put(sequence::NAME, Value::Str(name.to_owned()))?;
44        Ok(())
45    }
46
47    /// An owned handle to the same sequence, for passing it back.
48    pub(crate) fn duplicate_dispatch(&self) -> Option<Box<dyn Dispatch>> {
49        self.dispatch.duplicate()
50    }
51
52    /// How many steps a group holds (`Sequence.GetNumSteps`).
53    ///
54    /// # Errors
55    /// [`Error`] if the COM call fails or returns an unexpected type.
56    pub fn get_num_steps(&self, group: crate::StepGroup) -> Result<i32, Error> {
57        Ok(self
58            .dispatch
59            .call(sequence::GET_NUM_STEPS, &[Value::I32(group.bits())])?
60            .as_i32()?)
61    }
62
63    /// A step by position within a group (`Sequence.GetStep`).
64    ///
65    /// # Errors
66    /// [`Error`] if the index is out of range or the COM call fails.
67    pub fn get_step(&self, index: i32, group: crate::StepGroup) -> Result<crate::Step, Error> {
68        Ok(crate::Step::new(
69            self.dispatch
70                .call(
71                    sequence::GET_STEP,
72                    &[Value::I32(index), Value::I32(group.bits())],
73                )?
74                .into_object()?,
75        ))
76    }
77
78    /// Places a step in a group at a position (`Sequence.InsertStep`).
79    ///
80    /// # Errors
81    /// [`Error`] if the index is out of range, the step is not a live object,
82    /// or the COM call fails.
83    pub fn insert_step(
84        &self,
85        step: &crate::Step,
86        index: i32,
87        group: crate::StepGroup,
88    ) -> Result<(), Error> {
89        let handle = step.duplicate_dispatch().ok_or(Error::UnexpectedType {
90            expected: "a live step object",
91            actual: "a test fake with no COM identity",
92        })?;
93        self.dispatch.call(
94            sequence::INSERT_STEP,
95            &[
96                Value::Object(handle),
97                Value::I32(index),
98                Value::I32(group.bits()),
99            ],
100        )?;
101        Ok(())
102    }
103
104    /// Inserts a copy of a template step and returns the copy
105    /// (`PropertyObject.Clone` + `Sequence.InsertStep`).
106    ///
107    /// Composed rather than a COM member of its own, because the raw sequence
108    /// has a trap in it. `Clone` lives on `PropertyObject`, so a copied step
109    /// arrives on the `PropertyObject` interface, and the `Step` interface
110    /// shares none of its dispatch identifiers. Reading `Name` off such a copy
111    /// would dispatch a step identifier against a property-object interface and
112    /// take the process down rather than fail. Inserting first and reading the
113    /// step back from the sequence is the route that stays on the right
114    /// interface, so this returns the inserted step and never exposes the
115    /// intermediate.
116    ///
117    /// The template itself is untouched and can be inserted any number of
118    /// times. Each copy still carries the template's step ID until
119    /// [`Step::create_new_unique_step_id`](crate::Step::create_new_unique_step_id)
120    /// is called on it.
121    ///
122    /// # Errors
123    /// [`Error`] if the template is not a live object, the index is out of
124    /// range, or a COM call fails.
125    pub fn insert_step_from_template(
126        &self,
127        template: &PropertyObject,
128        index: i32,
129        group: crate::StepGroup,
130    ) -> Result<crate::Step, Error> {
131        let copy = template.clone_property("", crate::PropertyOptions::NONE.bits())?;
132        let handle = copy.duplicate_dispatch().ok_or(Error::UnexpectedType {
133            expected: "a live template object",
134            actual: "a test fake with no COM identity",
135        })?;
136        self.dispatch.call(
137            sequence::INSERT_STEP,
138            &[
139                Value::Object(handle),
140                Value::I32(index),
141                Value::I32(group.bits()),
142            ],
143        )?;
144        self.get_step(index, group)
145    }
146
147    /// The sequence as a property tree (`Sequence.AsPropertyObject`).
148    ///
149    /// This is also how a sequence becomes a template: a clone taken here is a
150    /// complete, detached copy of the sequence and its steps.
151    ///
152    /// # Errors
153    /// [`Error`] if the COM call fails or returns an unexpected type.
154    pub fn as_property_object(&self) -> Result<PropertyObject, Error> {
155        Ok(PropertyObject::new(
156            self.dispatch
157                .call(sequence::AS_PROPERTY_OBJECT, &[])?
158                .into_object()?,
159        ))
160    }
161
162    /// Re-identifies every step in the sequence
163    /// (`Sequence.CreateNewUniqueStepIds`).
164    ///
165    /// The bulk form of
166    /// [`Step::create_new_unique_step_id`](crate::Step::create_new_unique_step_id),
167    /// and what a sequence cloned from a template needs: every step in the copy
168    /// arrives holding the identity of its counterpart in the original.
169    ///
170    /// # Errors
171    /// [`Error`] if the COM call fails.
172    pub fn create_new_unique_step_ids(&self) -> Result<(), Error> {
173        self.dispatch
174            .call(sequence::CREATE_NEW_UNIQUE_STEP_IDS, &[])?;
175        Ok(())
176    }
177
178    /// The sequence's local variables (`Sequence.Locals`).
179    ///
180    /// Locals are per call: each invocation of the sequence gets its own copy,
181    /// so what this returns at edit time is the definition rather than any
182    /// running instance's values.
183    ///
184    /// # Errors
185    /// [`Error`] if the COM call fails or returns an unexpected type.
186    pub fn locals(&self) -> Result<PropertyObject, Error> {
187        Ok(PropertyObject::new(
188            self.dispatch.get(sequence::LOCALS)?.into_object()?,
189        ))
190    }
191
192    /// The sequence's parameters (`Sequence.Parameters`).
193    ///
194    /// # Errors
195    /// [`Error`] if the COM call fails or returns an unexpected type.
196    pub fn parameters(&self) -> Result<PropertyObject, Error> {
197        Ok(PropertyObject::new(
198            self.dispatch.get(sequence::PARAMETERS)?.into_object()?,
199        ))
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use std::collections::HashMap;
206
207    use rs_teststand_sys::{ComError, Dispatch, Value};
208
209    use super::Sequence;
210    use crate::Error;
211    use crate::dispids::sequence;
212
213    #[derive(Debug)]
214    struct Fake {
215        properties: HashMap<i32, Value>,
216    }
217
218    impl Dispatch for Fake {
219        fn get(&self, dispid: i32) -> Result<Value, ComError> {
220            match self.properties.get(&dispid) {
221                Some(Value::Str(text)) => Ok(Value::Str(text.clone())),
222                _ => Err(ComError::hresult(-17000, "fake: unscripted")),
223            }
224        }
225
226        fn put(&self, _dispid: i32, _value: Value) -> Result<(), ComError> {
227            Ok(())
228        }
229
230        fn call(&self, _dispid: i32, _args: &[Value]) -> Result<Value, ComError> {
231            Err(ComError::hresult(-17000, "fake: unscripted"))
232        }
233    }
234
235    #[test]
236    fn reads_its_name() -> Result<(), Error> {
237        let sequence = Sequence::new(Box::new(Fake {
238            properties: HashMap::from([(sequence::NAME, Value::Str("MainSequence".to_owned()))]),
239        }));
240        assert_eq!(sequence.name()?, "MainSequence");
241        Ok(())
242    }
243
244    #[test]
245    fn locals_and_parameters_are_distinct_dispids() {
246        // Swapping these two is a silent, plausible bug: both return a
247        // PropertyObject, so a mix-up would only show up as variables appearing
248        // in the wrong scope on a live engine.
249        assert_ne!(sequence::LOCALS, sequence::PARAMETERS);
250        assert_eq!(sequence::LOCALS, 0x33);
251        assert_eq!(sequence::PARAMETERS, 0x32);
252    }
253}