fmi_test_data/
lib.rs

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
14/// Version of the Reference FMUs to download
15pub const REF_FMU_VERSION: &str = "0.0.39";
16
17/// Computed archive filename
18pub const REF_ARCHIVE: &str = const_format::concatcp!("Reference-FMUs-", REF_FMU_VERSION, ".zip");
19
20/// Base URL for downloading Reference FMUs
21pub 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
37/// A Rust interface to the Modelica Reference-FMUs, downloaded as an archive using `fetch_data`
38///
39/// This struct provides access to the official Modelica Reference FMUs for testing and validation
40/// purposes. It automatically downloads the FMU archive from the official GitHub repository
41/// and provides methods to access individual FMUs.
42///
43/// # Examples
44///
45/// ```no_run
46/// use fmi_test_data::ReferenceFmus;
47/// use fmi::traits::FmiImport;
48///
49/// let mut reference_fmus = ReferenceFmus::new()?;
50///
51/// // Load a specific FMU
52/// let fmu: fmi::fmi3::import::Fmi3Import = reference_fmus.get_reference_fmu("BouncingBall")?;
53///
54/// // List all available FMUs
55/// let available_fmus = reference_fmus.list_available_fmus()?;
56/// println!("Available FMUs: {:?}", available_fmus);
57/// # Ok::<(), Box<dyn std::error::Error>>(())
58/// ```
59pub 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    /// Fetch the released Modelica Reference-FMUs file
73    ///
74    /// Downloads and caches the Reference FMUs archive from the official GitHub repository.
75    /// The archive is automatically verified using SHA256 checksums.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if:
80    /// - The download fails
81    /// - The file cannot be opened
82    /// - The ZIP archive is corrupted
83    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    /// Get a reference FMU as an import instance
93    ///
94    /// Loads a specific FMU from the archive and returns it as the requested import type.
95    /// The FMU version is automatically determined by the import type.
96    ///
97    /// # Arguments
98    ///
99    /// * `name` - The name of the FMU to load (e.g., "BouncingBall")
100    ///
101    /// # Examples
102    ///
103    /// ```no_run
104    /// # use fmi_test_data::ReferenceFmus;
105    /// # use fmi::traits::FmiImport;
106    /// let mut reference_fmus = ReferenceFmus::new()?;
107    ///
108    /// // Load FMI 2.0 version
109    /// let fmu: fmi::fmi2::import::Fmi2Import = reference_fmus.get_reference_fmu("BouncingBall")?;
110    ///
111    /// // Load FMI 3.0 version  
112    /// let fmu: fmi::fmi3::import::Fmi3Import = reference_fmus.get_reference_fmu("BouncingBall")?;
113    /// # Ok::<(), Box<dyn std::error::Error>>(())
114    /// ```
115    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        // Read f into a Vec<u8> that can be used to create a new Import
119        let mut buf = Vec::new();
120        f.read_to_end(buf.as_mut())?;
121        Ok(fmi::import::new(Cursor::new(buf))?)
122    }
123
124    /// Extract a reference FMU from the reference archive into a temporary file
125    ///
126    /// This method extracts an FMU to a temporary file on disk, which can be useful
127    /// when you need to pass a file path to other tools or libraries.
128    ///
129    /// # Arguments
130    ///
131    /// * `name` - The name of the FMU to extract
132    /// * `version` - The FMI major version to extract
133    ///
134    /// # Returns
135    ///
136    /// A `NamedTempFile` containing the extracted FMU. The file will be automatically
137    /// deleted when the returned value is dropped.
138    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    /// Get a list of all available FMU files in the archive
156    ///
157    /// Returns a sorted list of all FMU names available in the Reference FMUs archive.
158    /// These names can be used with `get_reference_fmu()` or `extract_reference_fmu()`.
159    ///
160    /// # Examples
161    ///
162    /// ```no_run
163    /// # use fmi_test_data::ReferenceFmus;
164    /// let mut reference_fmus = ReferenceFmus::new()?;
165    /// let fmus = reference_fmus.list_available_fmus()?;
166    ///
167    /// for fmu_name in &fmus {
168    ///     println!("Available: {}", fmu_name);
169    /// }
170    /// # Ok::<(), Box<dyn std::error::Error>>(())
171    /// ```
172    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                // Extract just the filename without path and extension
179                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    /// Get the Reference FMU version being used
192    ///
193    /// Returns the version string of the Reference FMUs package that this crate is configured to use.
194    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        // Test FMI 2.0 BouncingBall
209        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        // Test FMI 3.0 BouncingBall
215        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        // Check that we have some common FMUs
234        assert!(fmus.contains(&"BouncingBall".to_string()));
235        assert!(fmus.contains(&"Dahlquist".to_string()));
236        assert!(fmus.contains(&"VanDerPol".to_string()));
237
238        // Ensure the list is sorted and contains no duplicates
239        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        // Test extraction to temporary file
249        let temp_file = reference_fmus
250            .extract_reference_fmu("BouncingBall", MajorVersion::FMI3)
251            .unwrap();
252
253        // Verify the temporary file exists and has content
254        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        // Test the Feedthrough FMU which should exist in both versions
264        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        // This should fail gracefully
278        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}