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}