#![doc=include_str!( "../README.md")]
#![deny(unsafe_code)]
#![deny(clippy::all)]
use anyhow::Context;
use fetch_data::{FetchData, ctor};
use fmi::{schema::MajorVersion, traits::FmiImport};
use std::{
fs::File,
io::{Cursor, Read},
};
use tempfile::NamedTempFile;
pub const REF_FMU_VERSION: &str = "0.0.39";
pub const REF_ARCHIVE: &str = const_format::concatcp!("Reference-FMUs-", REF_FMU_VERSION, ".zip");
pub const REF_URL: &str = const_format::concatcp!(
"https://github.com/modelica/Reference-FMUs/releases/download/v",
REF_FMU_VERSION,
"/"
);
#[ctor]
static STATIC_FETCH_DATA: FetchData = FetchData::new(
include_str!("registry.txt"),
REF_URL,
"FMU_DATA_DIR",
"org",
"modelica",
"reference-fmus",
);
pub struct ReferenceFmus {
archive: zip::ZipArchive<File>,
}
impl std::fmt::Debug for ReferenceFmus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReferenceFmus")
.field("archive", &self.archive.comment())
.finish()
}
}
impl ReferenceFmus {
pub fn new() -> anyhow::Result<Self> {
let path = STATIC_FETCH_DATA
.fetch_file(REF_ARCHIVE)
.context(format!("Fetch {REF_ARCHIVE}"))?;
let f = std::fs::File::open(&path).context(format!("Open {path:?}"))?;
let archive = zip::ZipArchive::new(f)?;
Ok(Self { archive })
}
pub fn get_reference_fmu<Imp: FmiImport>(&mut self, name: &str) -> anyhow::Result<Imp> {
let version = Imp::MAJOR_VERSION.to_string();
let mut f = self.archive.by_name(&format!("{version}/{name}.fmu"))?;
let mut buf = Vec::new();
f.read_to_end(buf.as_mut())?;
Ok(fmi::import::new(Cursor::new(buf))?)
}
pub fn extract_reference_fmu(
&mut self,
name: &str,
version: MajorVersion,
) -> anyhow::Result<NamedTempFile> {
let version = version.to_string();
let filename = format!("{version}/{name}.fmu");
let mut fin = self
.archive
.by_name(&filename)
.context(format!("Open {filename}"))?;
let mut fout = tempfile::NamedTempFile::new()?;
std::io::copy(fin.by_ref(), fout.as_file_mut())
.context(format!("Extracting {filename} to tempfile"))?;
Ok(fout)
}
pub fn list_available_fmus(&mut self) -> anyhow::Result<Vec<String>> {
let mut fmus = Vec::new();
for i in 0..self.archive.len() {
let file = self.archive.by_index(i)?;
let name = file.name();
if name.ends_with(".fmu") {
if let Some(filename) = name.rsplit('/').next() {
if let Some(base_name) = filename.strip_suffix(".fmu") {
fmus.push(base_name.to_string());
}
}
}
}
fmus.sort();
fmus.dedup();
Ok(fmus)
}
pub fn version() -> &'static str {
REF_FMU_VERSION
}
}
#[cfg(test)]
mod tests {
use super::*;
use fmi::traits::FmiImport;
#[test]
fn test_reference_fmus_basic() {
let mut reference_fmus = ReferenceFmus::new().unwrap();
let fmu: fmi::fmi2::import::Fmi2Import =
reference_fmus.get_reference_fmu("BouncingBall").unwrap();
assert_eq!(fmu.model_description().fmi_version, "2.0");
assert_eq!(fmu.model_description().model_name, "BouncingBall");
let fmu: fmi::fmi3::import::Fmi3Import =
reference_fmus.get_reference_fmu("BouncingBall").unwrap();
assert_eq!(fmu.model_description().fmi_version, "3.0");
assert_eq!(fmu.model_description().model_name, "BouncingBall");
}
#[test]
fn test_version_constant() {
assert_eq!(ReferenceFmus::version(), "0.0.39");
assert!(REF_ARCHIVE.contains("0.0.39"));
assert!(REF_URL.contains("v0.0.39"));
}
#[test]
fn test_list_available_fmus() {
let mut reference_fmus = ReferenceFmus::new().unwrap();
let fmus = reference_fmus.list_available_fmus().unwrap();
assert!(fmus.contains(&"BouncingBall".to_string()));
assert!(fmus.contains(&"Dahlquist".to_string()));
assert!(fmus.contains(&"VanDerPol".to_string()));
let mut sorted_fmus = fmus.clone();
sorted_fmus.sort();
assert_eq!(fmus, sorted_fmus);
}
#[test]
fn test_extract_reference_fmu() {
let mut reference_fmus = ReferenceFmus::new().unwrap();
let temp_file = reference_fmus
.extract_reference_fmu("BouncingBall", MajorVersion::FMI3)
.unwrap();
assert!(temp_file.path().exists());
let metadata = std::fs::metadata(temp_file.path()).unwrap();
assert!(metadata.len() > 0);
}
#[test]
fn test_feedthrough_fmu() {
let mut reference_fmus = ReferenceFmus::new().unwrap();
let fmu_v2: fmi::fmi2::import::Fmi2Import =
reference_fmus.get_reference_fmu("Feedthrough").unwrap();
assert_eq!(fmu_v2.model_description().model_name, "Feedthrough");
let fmu_v3: fmi::fmi3::import::Fmi3Import =
reference_fmus.get_reference_fmu("Feedthrough").unwrap();
assert_eq!(fmu_v3.model_description().model_name, "Feedthrough");
}
#[test]
fn test_nonexistent_fmu() {
let mut reference_fmus = ReferenceFmus::new().unwrap();
let result: anyhow::Result<fmi::fmi3::import::Fmi3Import> =
reference_fmus.get_reference_fmu("NonExistentFMU");
assert!(result.is_err());
}
#[cfg(false)]
#[test]
fn print_registry_contents() {
let registry_contents = STATIC_FETCH_DATA
.gen_registry_contents([REF_ARCHIVE])
.unwrap();
println!("{registry_contents}");
}
#[cfg(false)]
#[test]
fn print_all_available_fmus() {
let mut reference_fmus = ReferenceFmus::new().unwrap();
let fmus = reference_fmus.list_available_fmus().unwrap();
println!("Available FMUs ({} total):", fmus.len());
for fmu in fmus {
println!(" - {}", fmu);
}
}
}