1#![doc=include_str!( "../README.md")]
2#![deny(unsafe_code)]
3#![deny(clippy::all)]
4
5use anyhow::Context;
6use fetch_data::{FetchData, ctor};
7use fmi::{schema::MajorVersion, traits::FmiImport};
8use std::{
9 fs::File,
10 io::{Cursor, Read},
11};
12use tempfile::NamedTempFile;
13
14pub const REF_FMU_VERSION: &str = "0.0.39";
16
17pub const REF_ARCHIVE: &str = const_format::concatcp!("Reference-FMUs-", REF_FMU_VERSION, ".zip");
19
20pub const REF_URL: &str = const_format::concatcp!(
22 "https://github.com/modelica/Reference-FMUs/releases/download/v",
23 REF_FMU_VERSION,
24 "/"
25);
26
27#[ctor]
28static STATIC_FETCH_DATA: FetchData = FetchData::new(
29 include_str!("registry.txt"),
30 REF_URL,
31 "FMU_DATA_DIR",
32 "org",
33 "modelica",
34 "reference-fmus",
35);
36
37pub struct ReferenceFmus {
60 archive: zip::ZipArchive<File>,
61}
62
63impl std::fmt::Debug for ReferenceFmus {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("ReferenceFmus")
66 .field("archive", &self.archive.comment())
67 .finish()
68 }
69}
70
71impl ReferenceFmus {
72 pub fn new() -> anyhow::Result<Self> {
84 let path = STATIC_FETCH_DATA
85 .fetch_file(REF_ARCHIVE)
86 .context(format!("Fetch {REF_ARCHIVE}"))?;
87 let f = std::fs::File::open(&path).context(format!("Open {path:?}"))?;
88 let archive = zip::ZipArchive::new(f)?;
89 Ok(Self { archive })
90 }
91
92 pub fn get_reference_fmu<Imp: FmiImport>(&mut self, name: &str) -> anyhow::Result<Imp> {
116 let version = Imp::MAJOR_VERSION.to_string();
117 let mut f = self.archive.by_name(&format!("{version}/{name}.fmu"))?;
118 let mut buf = Vec::new();
120 f.read_to_end(buf.as_mut())?;
121 Ok(fmi::import::new(Cursor::new(buf))?)
122 }
123
124 pub fn extract_reference_fmu(
139 &mut self,
140 name: &str,
141 version: MajorVersion,
142 ) -> anyhow::Result<NamedTempFile> {
143 let version = version.to_string();
144 let filename = format!("{version}/{name}.fmu");
145 let mut fin = self
146 .archive
147 .by_name(&filename)
148 .context(format!("Open {filename}"))?;
149 let mut fout = tempfile::NamedTempFile::new()?;
150 std::io::copy(fin.by_ref(), fout.as_file_mut())
151 .context(format!("Extracting {filename} to tempfile"))?;
152 Ok(fout)
153 }
154
155 pub fn list_available_fmus(&mut self) -> anyhow::Result<Vec<String>> {
173 let mut fmus = Vec::new();
174 for i in 0..self.archive.len() {
175 let file = self.archive.by_index(i)?;
176 let name = file.name();
177 if name.ends_with(".fmu") {
178 if let Some(filename) = name.rsplit('/').next() {
180 if let Some(base_name) = filename.strip_suffix(".fmu") {
181 fmus.push(base_name.to_string());
182 }
183 }
184 }
185 }
186 fmus.sort();
187 fmus.dedup();
188 Ok(fmus)
189 }
190
191 pub fn version() -> &'static str {
195 REF_FMU_VERSION
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use fmi::traits::FmiImport;
203
204 #[test]
205 fn test_reference_fmus_basic() {
206 let mut reference_fmus = ReferenceFmus::new().unwrap();
207
208 let fmu: fmi::fmi2::import::Fmi2Import =
210 reference_fmus.get_reference_fmu("BouncingBall").unwrap();
211 assert_eq!(fmu.model_description().fmi_version, "2.0");
212 assert_eq!(fmu.model_description().model_name, "BouncingBall");
213
214 let fmu: fmi::fmi3::import::Fmi3Import =
216 reference_fmus.get_reference_fmu("BouncingBall").unwrap();
217 assert_eq!(fmu.model_description().fmi_version, "3.0");
218 assert_eq!(fmu.model_description().model_name, "BouncingBall");
219 }
220
221 #[test]
222 fn test_version_constant() {
223 assert_eq!(ReferenceFmus::version(), "0.0.39");
224 assert!(REF_ARCHIVE.contains("0.0.39"));
225 assert!(REF_URL.contains("v0.0.39"));
226 }
227
228 #[test]
229 fn test_list_available_fmus() {
230 let mut reference_fmus = ReferenceFmus::new().unwrap();
231 let fmus = reference_fmus.list_available_fmus().unwrap();
232
233 assert!(fmus.contains(&"BouncingBall".to_string()));
235 assert!(fmus.contains(&"Dahlquist".to_string()));
236 assert!(fmus.contains(&"VanDerPol".to_string()));
237
238 let mut sorted_fmus = fmus.clone();
240 sorted_fmus.sort();
241 assert_eq!(fmus, sorted_fmus);
242 }
243
244 #[test]
245 fn test_extract_reference_fmu() {
246 let mut reference_fmus = ReferenceFmus::new().unwrap();
247
248 let temp_file = reference_fmus
250 .extract_reference_fmu("BouncingBall", MajorVersion::FMI3)
251 .unwrap();
252
253 assert!(temp_file.path().exists());
255 let metadata = std::fs::metadata(temp_file.path()).unwrap();
256 assert!(metadata.len() > 0);
257 }
258
259 #[test]
260 fn test_feedthrough_fmu() {
261 let mut reference_fmus = ReferenceFmus::new().unwrap();
262
263 let fmu_v2: fmi::fmi2::import::Fmi2Import =
265 reference_fmus.get_reference_fmu("Feedthrough").unwrap();
266 assert_eq!(fmu_v2.model_description().model_name, "Feedthrough");
267
268 let fmu_v3: fmi::fmi3::import::Fmi3Import =
269 reference_fmus.get_reference_fmu("Feedthrough").unwrap();
270 assert_eq!(fmu_v3.model_description().model_name, "Feedthrough");
271 }
272
273 #[test]
274 fn test_nonexistent_fmu() {
275 let mut reference_fmus = ReferenceFmus::new().unwrap();
276
277 let result: anyhow::Result<fmi::fmi3::import::Fmi3Import> =
279 reference_fmus.get_reference_fmu("NonExistentFMU");
280 assert!(result.is_err());
281 }
282
283 #[cfg(false)]
284 #[test]
285 fn print_registry_contents() {
286 let registry_contents = STATIC_FETCH_DATA
287 .gen_registry_contents([REF_ARCHIVE])
288 .unwrap();
289 println!("{registry_contents}");
290 }
291
292 #[cfg(false)]
293 #[test]
294 fn print_all_available_fmus() {
295 let mut reference_fmus = ReferenceFmus::new().unwrap();
296 let fmus = reference_fmus.list_available_fmus().unwrap();
297 println!("Available FMUs ({} total):", fmus.len());
298 for fmu in fmus {
299 println!(" - {}", fmu);
300 }
301 }
302}