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    /// Adds a sequence to the file (`SequenceFile.InsertSequence`).
42    ///
43    /// # Errors
44    /// [`Error`] if the sequence is not a live object or the COM call fails.
45    pub fn insert_sequence(&self, sequence: &crate::Sequence) -> Result<(), Error> {
46        let handle = sequence.duplicate_dispatch().ok_or(Error::UnexpectedType {
47            expected: "a live sequence object",
48            actual: "a test fake with no COM identity",
49        })?;
50        self.dispatch
51            .call(sequence_file::INSERT_SEQUENCE, &[Value::Object(handle)])?;
52        Ok(())
53    }
54
55    /// Inserts a copy of a template sequence and returns the copy
56    /// (`PropertyObject.Clone` + `SequenceFile.InsertSequence`).
57    ///
58    /// The sequence-level counterpart of
59    /// [`Sequence::insert_step_from_template`](crate::Sequence::insert_step_from_template),
60    /// and composed for the same reason: a clone arrives on the
61    /// `PropertyObject` interface, which shares no dispatch identifiers with
62    /// `Sequence`. The copy is looked up by name after insertion so the caller
63    /// only ever holds the right interface.
64    ///
65    /// The copy keeps the template's name, so inserting the same template twice
66    /// without renaming the first copy puts two sequences of one name in the
67    /// file. Every step in the copy also still carries the template's step ID, /// see
68    /// [`Sequence::create_new_unique_step_ids`](crate::Sequence::create_new_unique_step_ids).
69    ///
70    /// # Errors
71    /// [`Error`] if the template is not a live object, has no name, or a COM
72    /// call fails.
73    pub fn insert_sequence_from_template(
74        &self,
75        template: &crate::property::PropertyObject,
76    ) -> Result<crate::Sequence, Error> {
77        let name = template.name()?;
78        let copy = template.clone_property("", crate::PropertyOptions::NONE.bits())?;
79        let handle = copy.duplicate_dispatch().ok_or(Error::UnexpectedType {
80            expected: "a live template object",
81            actual: "a test fake with no COM identity",
82        })?;
83        self.dispatch
84            .call(sequence_file::INSERT_SEQUENCE, &[Value::Object(handle)])?;
85        self.get_sequence_by_name(&name)
86    }
87
88    /// The file seen as a property-object file (`AsPropertyObjectFile`).
89    ///
90    /// This is the route to the file's registered types.
91    ///
92    /// # Errors
93    /// [`Error`] if the COM call fails or returns an unexpected type.
94    pub fn as_property_object_file(&self) -> Result<crate::PropertyObjectFile, Error> {
95        Ok(crate::PropertyObjectFile::new(
96            self.dispatch
97                .call(sequence_file::AS_PROPERTY_OBJECT_FILE, &[])?
98                .into_object()?,
99        ))
100    }
101
102    /// The file's edit-time global variables (`FileGlobalsDefaultValues`).
103    ///
104    /// These are the **defaults stored in the file**, which is what an editor
105    /// shows and what this API can change. A running execution works on its own
106    /// run-time copy instead, and edits made there do not travel back here, /// reach that copy through the execution, not through this method.
107    ///
108    /// # Errors
109    /// [`Error`] if the COM call fails or returns an unexpected type.
110    pub fn file_globals_default_values(&self) -> Result<crate::property::PropertyObject, Error> {
111        Ok(crate::property::PropertyObject::new(
112            self.dispatch
113                .get(sequence_file::FILE_GLOBALS_DEFAULT_VALUES)?
114                .into_object()?,
115        ))
116    }
117
118    /// Looks a sequence up by name (`GetSequenceByName`).
119    ///
120    /// # Errors
121    /// [`Error`] if no such sequence exists or the COM call fails.
122    pub fn get_sequence_by_name(&self, name: &str) -> Result<crate::sequence::Sequence, Error> {
123        Ok(crate::sequence::Sequence::new(
124            self.dispatch
125                .call(
126                    sequence_file::GET_SEQUENCE_BY_NAME,
127                    &[Value::Str(name.to_owned())],
128                )?
129                .into_object()?,
130        ))
131    }
132
133    /// Fetches a sequence by position (`GetSequence`).
134    ///
135    /// # Errors
136    /// [`Error`] if the index is out of range or the COM call fails.
137    pub fn get_sequence(&self, index: i32) -> Result<crate::sequence::Sequence, Error> {
138        Ok(crate::sequence::Sequence::new(
139            self.dispatch
140                .call(sequence_file::GET_SEQUENCE, &[Value::I32(index)])?
141                .into_object()?,
142        ))
143    }
144
145    /// Saves the file, optionally to a new path (`Save`).
146    ///
147    /// An empty `path` saves in place.
148    ///
149    /// # Errors
150    /// [`Error`] if the COM call fails.
151    pub fn save(&self, path: &str) -> Result<(), Error> {
152        self.dispatch
153            .call(sequence_file::SAVE, &[Value::Str(path.to_owned())])?;
154        Ok(())
155    }
156
157    /// An owned handle to the same file, for passing it back to the engine.
158    ///
159    /// Unlike [`into_dispatch`](Self::into_dispatch) this leaves the wrapper
160    /// usable: a COM pointer is refcounted, so this shares rather than moves.
161    pub(crate) fn duplicate_dispatch(&self) -> Option<Box<dyn Dispatch>> {
162        self.dispatch.duplicate()
163    }
164
165    /// Surrenders the underlying dispatch handle back to the engine.
166    ///
167    /// Consuming `self` is deliberate: once the handle is handed to
168    /// `ReleaseSequenceFileEx` the wrapper must not be used again.
169    pub(crate) fn into_dispatch(self) -> Box<dyn Dispatch> {
170        self.dispatch
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use rs_teststand_sys::{ComError, Dispatch, Value};
177
178    use super::SequenceFile;
179    use crate::Error;
180
181    #[derive(Debug)]
182    struct Fake {
183        path: &'static str,
184        sequences: i32,
185    }
186
187    impl Dispatch for Fake {
188        fn get(&self, dispid: i32) -> Result<Value, ComError> {
189            match dispid {
190                d if d == crate::dispids::sequence_file::PATH => {
191                    Ok(Value::Str(self.path.to_owned()))
192                }
193                d if d == crate::dispids::sequence_file::NUM_SEQUENCES => {
194                    Ok(Value::I32(self.sequences))
195                }
196                _ => Err(ComError::hresult(-17000, "fake: unscripted")),
197            }
198        }
199
200        fn put(&self, _dispid: i32, _value: Value) -> Result<(), ComError> {
201            Err(ComError::hresult(-17000, "fake: put not scripted"))
202        }
203
204        fn call(&self, _dispid: i32, _args: &[Value]) -> Result<Value, ComError> {
205            Ok(Value::Empty)
206        }
207    }
208
209    fn file() -> SequenceFile {
210        SequenceFile::new(Box::new(Fake {
211            path: r"T:\seq\demo.seq",
212            sequences: 3,
213        }))
214    }
215
216    #[test]
217    fn reads_path() -> Result<(), Error> {
218        assert_eq!(file().path()?, r"T:\seq\demo.seq");
219        Ok(())
220    }
221
222    #[test]
223    fn reads_sequence_count() -> Result<(), Error> {
224        assert_eq!(file().num_sequences()?, 3);
225        Ok(())
226    }
227
228    #[test]
229    fn save_succeeds_in_place() -> Result<(), Error> {
230        file().save("")?;
231        Ok(())
232    }
233}