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}