Skip to main content

rs_teststand/sequence/
sequence_file.rs

1//! Safe wrapper for a loaded sequence file.
2
3use rs_teststand_sys::{Dispatch, Value};
4
5use crate::Error;
6use crate::dispids::sequence_file;
7
8/// A sequence file held open by the engine.
9///
10/// Obtained from [`crate::Engine::get_sequence_file_ex`]. Dropping this value
11/// releases the wrapper's own reference, but the engine keeps the file in its
12/// cache until it is released explicitly, see
13/// [`crate::Engine::release_sequence_file_ex`].
14#[derive(Debug)]
15pub struct SequenceFile {
16    dispatch: Box<dyn Dispatch>,
17}
18
19impl SequenceFile {
20    /// Wraps a dispatch handle returned by the engine.
21    pub(crate) fn new(dispatch: Box<dyn Dispatch>) -> Self {
22        Self { dispatch }
23    }
24
25    /// The file's path on disk (`Path`).
26    ///
27    /// # Errors
28    /// [`Error`] if the COM call fails or returns an unexpected type.
29    pub fn path(&self) -> Result<String, Error> {
30        Ok(self.dispatch.get(sequence_file::PATH)?.into_string()?)
31    }
32
33    /// How many sequences the file contains (`NumSequences`).
34    ///
35    /// # Errors
36    /// [`Error`] if the COM call fails or returns an unexpected type.
37    pub fn num_sequences(&self) -> Result<i32, Error> {
38        Ok(self.dispatch.get(sequence_file::NUM_SEQUENCES)?.as_i32()?)
39    }
40
41    /// Whether any execution is currently running this file (`IsExecuting`).
42    ///
43    /// A host checks this before unloading or replacing a file: the engine
44    /// still holds it while a run is in flight.
45    ///
46    /// # Errors
47    /// [`Error`] if the COM call fails or returns an unexpected type.
48    pub fn is_executing(&self) -> Result<bool, Error> {
49        Ok(self.dispatch.get(sequence_file::IS_EXECUTING)?.as_bool()?)
50    }
51
52    /// Adds a sequence to the file (`SequenceFile.InsertSequence`).
53    ///
54    /// # Errors
55    /// [`Error`] if the sequence is not a live object or the COM call fails.
56    pub fn insert_sequence(&self, sequence: &crate::Sequence) -> Result<(), Error> {
57        let handle = sequence.duplicate_dispatch().ok_or(Error::UnexpectedType {
58            expected: "a live sequence object",
59            actual: "a test fake with no COM identity",
60        })?;
61        self.dispatch
62            .call(sequence_file::INSERT_SEQUENCE, &[Value::Object(handle)])?;
63        Ok(())
64    }
65
66    /// Inserts a copy of a template sequence and returns the copy
67    /// (`PropertyObject.Clone` + `SequenceFile.InsertSequence`).
68    ///
69    /// The sequence-level counterpart of
70    /// [`Sequence::insert_step_from_template`](crate::Sequence::insert_step_from_template),
71    /// and composed for the same reason: a clone arrives on the
72    /// `PropertyObject` interface, which shares no dispatch identifiers with
73    /// `Sequence`. The copy is looked up by name after insertion so the caller
74    /// only ever holds the right interface.
75    ///
76    /// The copy keeps the template's name, so inserting the same template twice
77    /// without renaming the first copy puts two sequences of one name in the
78    /// file. Every step in the copy also still carries the template's step ID, /// see
79    /// [`Sequence::create_new_unique_step_ids`](crate::Sequence::create_new_unique_step_ids).
80    ///
81    /// # Errors
82    /// [`Error`] if the template is not a live object, has no name, or a COM
83    /// call fails.
84    pub fn insert_sequence_from_template(
85        &self,
86        template: &crate::property::PropertyObject,
87    ) -> Result<crate::Sequence, Error> {
88        let name = template.name()?;
89        let copy = template.clone_property("", crate::PropertyOptions::NONE.bits())?;
90        let handle = copy.duplicate_dispatch().ok_or(Error::UnexpectedType {
91            expected: "a live template object",
92            actual: "a test fake with no COM identity",
93        })?;
94        self.dispatch
95            .call(sequence_file::INSERT_SEQUENCE, &[Value::Object(handle)])?;
96        self.get_sequence_by_name(&name)
97    }
98
99    /// The file seen as a property-object file (`AsPropertyObjectFile`).
100    ///
101    /// This is the route to the file's registered types.
102    ///
103    /// # Errors
104    /// [`Error`] if the COM call fails or returns an unexpected type.
105    pub fn as_property_object_file(&self) -> Result<crate::PropertyObjectFile, Error> {
106        Ok(crate::PropertyObjectFile::new(
107            self.dispatch
108                .call(sequence_file::AS_PROPERTY_OBJECT_FILE, &[])?
109                .into_object()?,
110        ))
111    }
112
113    /// The file's edit-time global variables (`FileGlobalsDefaultValues`).
114    ///
115    /// These are the **defaults stored in the file**, which is what an editor
116    /// shows and what this API can change. A running execution works on its own
117    /// run-time copy instead, and edits made there do not travel back here, /// reach that copy through the execution, not through this method.
118    ///
119    /// # Errors
120    /// [`Error`] if the COM call fails or returns an unexpected type.
121    pub fn file_globals_default_values(&self) -> Result<crate::property::PropertyObject, Error> {
122        Ok(crate::property::PropertyObject::new(
123            self.dispatch
124                .get(sequence_file::FILE_GLOBALS_DEFAULT_VALUES)?
125                .into_object()?,
126        ))
127    }
128
129    /// Looks a sequence up by name (`GetSequenceByName`).
130    ///
131    /// # Errors
132    /// [`Error`] if no such sequence exists or the COM call fails.
133    pub fn get_sequence_by_name(&self, name: &str) -> Result<crate::sequence::Sequence, Error> {
134        Ok(crate::sequence::Sequence::new(
135            self.dispatch
136                .call(
137                    sequence_file::GET_SEQUENCE_BY_NAME,
138                    &[Value::Str(name.to_owned())],
139                )?
140                .into_object()?,
141        ))
142    }
143
144    /// Fetches a sequence by position (`GetSequence`).
145    ///
146    /// # Errors
147    /// [`Error`] if the index is out of range or the COM call fails.
148    pub fn get_sequence(&self, index: i32) -> Result<crate::sequence::Sequence, Error> {
149        Ok(crate::sequence::Sequence::new(
150            self.dispatch
151                .call(sequence_file::GET_SEQUENCE, &[Value::I32(index)])?
152                .into_object()?,
153        ))
154    }
155
156    /// Saves the file, optionally to a new path (`Save`).
157    ///
158    /// An empty `path` saves in place.
159    ///
160    /// # Errors
161    /// [`Error`] if the COM call fails.
162    pub fn save(&self, path: &str) -> Result<(), Error> {
163        self.dispatch
164            .call(sequence_file::SAVE, &[Value::Str(path.to_owned())])?;
165        Ok(())
166    }
167
168    /// An owned handle to the same file, for passing it back to the engine.
169    ///
170    /// Unlike [`into_dispatch`](Self::into_dispatch) this leaves the wrapper
171    /// usable: a COM pointer is refcounted, so this shares rather than moves.
172    pub(crate) fn duplicate_dispatch(&self) -> Option<Box<dyn Dispatch>> {
173        self.dispatch.duplicate()
174    }
175
176    /// Surrenders the underlying dispatch handle back to the engine.
177    ///
178    /// Consuming `self` is deliberate: once the handle is handed to
179    /// `ReleaseSequenceFileEx` the wrapper must not be used again.
180    pub(crate) fn into_dispatch(self) -> Box<dyn Dispatch> {
181        self.dispatch
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use rs_teststand_sys::{ComError, Dispatch, Value};
188
189    use super::SequenceFile;
190    use crate::Error;
191
192    #[derive(Debug)]
193    struct Fake {
194        path: &'static str,
195        sequences: i32,
196        executing: bool,
197    }
198
199    impl Dispatch for Fake {
200        fn get(&self, dispid: i32) -> Result<Value, ComError> {
201            match dispid {
202                d if d == crate::dispids::sequence_file::PATH => {
203                    Ok(Value::Str(self.path.to_owned()))
204                }
205                d if d == crate::dispids::sequence_file::NUM_SEQUENCES => {
206                    Ok(Value::I32(self.sequences))
207                }
208                d if d == crate::dispids::sequence_file::IS_EXECUTING => {
209                    Ok(Value::Bool(self.executing))
210                }
211                _ => Err(ComError::hresult(-17000, "fake: unscripted")),
212            }
213        }
214
215        fn put(&self, _dispid: i32, _value: Value) -> Result<(), ComError> {
216            Err(ComError::hresult(-17000, "fake: put not scripted"))
217        }
218
219        fn call(&self, _dispid: i32, _args: &[Value]) -> Result<Value, ComError> {
220            Ok(Value::Empty)
221        }
222    }
223
224    fn file() -> SequenceFile {
225        SequenceFile::new(Box::new(Fake {
226            path: r"T:\seq\demo.seq",
227            sequences: 3,
228            executing: false,
229        }))
230    }
231
232    #[test]
233    fn reports_whether_the_file_is_still_executing() -> Result<(), Error> {
234        assert!(!file().is_executing()?);
235
236        let busy = SequenceFile::new(Box::new(Fake {
237            path: r"T:\seq\demo.seq",
238            sequences: 3,
239            executing: true,
240        }));
241        assert!(busy.is_executing()?);
242        Ok(())
243    }
244
245    #[test]
246    fn reads_path() -> Result<(), Error> {
247        assert_eq!(file().path()?, r"T:\seq\demo.seq");
248        Ok(())
249    }
250
251    #[test]
252    fn reads_sequence_count() -> Result<(), Error> {
253        assert_eq!(file().num_sequences()?, 3);
254        Ok(())
255    }
256
257    #[test]
258    fn save_succeeds_in_place() -> Result<(), Error> {
259        file().save("")?;
260        Ok(())
261    }
262}