From 3eb2219454ebb020b486e3392a795bcb56a2fc51 Mon Sep 17 00:00:00 2001 From: Sepehr Date: Sat, 28 Feb 2026 19:37:02 +0100 Subject: [PATCH] style: fix AI code review findings for vendor backend --- .../sprint-status.yaml | 26 +- crates/vendors/src/error.rs | 31 + crates/vendors/src/lib.rs | 28 + crates/vendors/src/vendor_api.rs | 580 ++++++++++++++++++ 4 files changed, 654 insertions(+), 11 deletions(-) create mode 100644 crates/vendors/src/error.rs create mode 100644 crates/vendors/src/lib.rs create mode 100644 crates/vendors/src/vendor_api.rs diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index a3aa011..598ea17 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -1,5 +1,5 @@ # Sprint Status - Entropyk -# Last Updated: 2026-02-24 +# Last Updated: 2026-02-28 # Project: Entropyk # Project Key: NOKEY # Tracking System: file-system @@ -53,6 +53,10 @@ development_status: 1-8-auxiliary-transport-components: done 1-11-flow-junctions-flowsplitter-flowmerger: done 1-12-boundary-conditions-flowsource-flowsink: done + epic-1-retrospective: optional + + # Epic 2: Fluid Properties Backend + epic-2: done 2-1-fluid-backend-trait-abstraction: done 2-2-coolprop-integration-sys-crate: done 2-3-tabular-interpolation-backend: done @@ -61,7 +65,7 @@ development_status: 2-6-critical-point-damping-co2-r744: done 2-7-incompressible-fluids-support: done 2-8-rich-thermodynamic-state-abstraction: done - epic-1-retrospective: optional + epic-2-retrospective: optional # Epic 3: System Topology (Graph) epic-3: done @@ -125,7 +129,7 @@ development_status: epic-8-retrospective: optional # Epic 9: Coherence Corrections (Post-Audit) - epic-9: in-progress + epic-9: done 9-1-circuitid-type-unification: done 9-2-fluidid-type-unification: done 9-3-expansionvalve-energy-methods: done @@ -154,13 +158,13 @@ development_status: 11-2-drum-recirculation-drum: done 11-3-floodedevaporator: done 11-4-floodedcondenser: done - 11-5-bphxexchanger-base: in-progress - 11-6-bphxevaporator: backlog - 11-7-bphxcondenser: backlog - 11-8-correlationselector: backlog - 11-9-movingboundaryhx-zone-discretization: backlog - 11-10-movingboundaryhx-cache-optimization: backlog - 11-11-vendorbackend-trait: backlog + 11-5-bphxexchanger-base: done + 11-6-bphxevaporator: done + 11-7-bphxcondenser: done + 11-8-correlationselector: done + 11-9-movingboundaryhx-zone-discretization: done + 11-10-movingboundaryhx-cache-optimization: done + 11-11-vendorbackend-trait: done 11-12-copeland-parser: ready-for-dev 11-13-swep-parser: ready-for-dev 11-14-danfoss-parser: ready-for-dev @@ -171,7 +175,7 @@ development_status: # Support for ScrewEconomizerCompressor, MCHXCondenserCoil, FloodedEvaporator # with proper internal state variables, CoolProp backend, and controls epic-12: in-progress - 12-1-cli-internal-state-variables: ready-for-dev + 12-1-cli-internal-state-variables: in-progress 12-2-cli-coolprop-backend: ready-for-dev 12-3-cli-screw-compressor-config: ready-for-dev 12-4-cli-mchx-condenser-config: ready-for-dev diff --git a/crates/vendors/src/error.rs b/crates/vendors/src/error.rs new file mode 100644 index 0000000..5e27e47 --- /dev/null +++ b/crates/vendors/src/error.rs @@ -0,0 +1,31 @@ +//! Error types for vendor backend operations. + +/// Errors that can occur during vendor data operations. +#[derive(Debug, thiserror::Error)] +pub enum VendorError { + /// The requested equipment model was not found in this vendor's catalog. + #[error("Model not found: {0}")] + ModelNotFound(String), + + /// The data file has an invalid or unsupported format. + #[error("Invalid data format: {0}")] + InvalidFormat(String), + + /// The expected data file could not be found on disk. + #[error("Data file not found: {0}")] + FileNotFound(String), + + /// JSON parsing failed. + #[error("Parse error: {0}")] + ParseError(#[from] serde_json::Error), + + /// An I/O error occurred while reading vendor data. + #[error("IO error reading {path}: {source}")] + IoError { + /// The path where the IO error occurred. + path: String, + /// The underlying IO error. + #[source] + source: std::io::Error, + }, +} diff --git a/crates/vendors/src/lib.rs b/crates/vendors/src/lib.rs new file mode 100644 index 0000000..1918d42 --- /dev/null +++ b/crates/vendors/src/lib.rs @@ -0,0 +1,28 @@ +//! Vendor equipment data backends for Entropyk. +//! +//! This crate provides a uniform [`VendorBackend`] trait and associated data types +//! for accessing manufacturer equipment catalogs (compressor AHRI 540 coefficients, +//! BPHX parameters, UA curves, etc.) from Copeland, SWEP, Danfoss, and Bitzer. +//! +//! # Architecture +//! +//! - **Standalone crate** — no dependency on `entropyk-core`, `entropyk-fluids`, +//! or `entropyk-components`. Vendor data is pure data structures + I/O. +//! - **Object-safe trait** — `VendorBackend` can be stored as `Box`. +//! - Individual vendor implementations (stories 11.12–11.15) will live in the +//! `compressors` and `heat_exchangers` submodules. + +#![warn(missing_docs)] + +pub mod error; +pub mod vendor_api; + +pub mod compressors; +pub mod heat_exchangers; + +// Public re-exports for convenience +pub use error::VendorError; +pub use vendor_api::{ + BphxParameters, CompressorCoefficients, CompressorValidityRange, UaCalcParams, UaCurve, + VendorBackend, +}; diff --git a/crates/vendors/src/vendor_api.rs b/crates/vendors/src/vendor_api.rs new file mode 100644 index 0000000..ac33340 --- /dev/null +++ b/crates/vendors/src/vendor_api.rs @@ -0,0 +1,580 @@ +//! VendorBackend trait and associated data types. +//! +//! This module defines the uniform API for accessing manufacturer equipment data +//! (compressor coefficients, heat exchanger parameters) from vendor catalogs. + +use serde::{Deserialize, Serialize}; + +use crate::error::VendorError; + +// --------------------------------------------------------------------------- +// Compressor data types +// --------------------------------------------------------------------------- + +/// AHRI 540 compressor coefficient set. +/// +/// The 10 coefficients follow the polynomial form: +/// ```text +/// C = a₀ + a₁·Ts + a₂·Td + a₃·Ts² + a₄·Ts·Td + a₅·Td² +/// + a₆·Ts³ + a₇·Td·Ts² + a₈·Ts·Td² + a₉·Td³ +/// ``` +/// where `Ts` = suction saturation temperature (°C) and +/// `Td` = discharge saturation temperature (°C). +/// +/// Both `capacity_coeffs` (W) and `power_coeffs` (W) use this convention. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CompressorCoefficients { + /// Equipment model identifier (e.g. `"ZP54KCE-TFD"`). + pub model: String, + /// Manufacturer name (e.g. `"Copeland"`). + pub manufacturer: String, + /// Refrigerant designation (e.g. `"R410A"`). + pub refrigerant: String, + /// 10 AHRI 540 capacity polynomial coefficients (W). + pub capacity_coeffs: [f64; 10], + /// 10 AHRI 540 power polynomial coefficients (W). + pub power_coeffs: [f64; 10], + /// Optional 10 AHRI 540 mass-flow polynomial coefficients (kg/s). + /// Not all vendors provide mass-flow polynomials. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mass_flow_coeffs: Option<[f64; 10]>, + /// Operating envelope for the coefficient set. + pub validity: CompressorValidityRange, +} + +/// Operating envelope for compressor coefficients. +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct CompressorValidityRange { + /// Minimum suction saturation temperature (°C). + pub t_suction_min: f64, + /// Maximum suction saturation temperature (°C). + pub t_suction_max: f64, + /// Minimum discharge saturation temperature (°C). + pub t_discharge_min: f64, + /// Maximum discharge saturation temperature (°C). + pub t_discharge_max: f64, +} + +impl<'de> Deserialize<'de> for CompressorValidityRange { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Inner { + t_suction_min: f64, + t_suction_max: f64, + t_discharge_min: f64, + t_discharge_max: f64, + } + + let inner = Inner::deserialize(deserializer)?; + + if inner.t_suction_min > inner.t_suction_max { + return Err(serde::de::Error::custom(format!( + "Invalid suction temperature range: min ({} °C) > max ({} °C)", + inner.t_suction_min, inner.t_suction_max + ))); + } + + if inner.t_discharge_min > inner.t_discharge_max { + return Err(serde::de::Error::custom(format!( + "Invalid discharge temperature range: min ({} °C) > max ({} °C)", + inner.t_discharge_min, inner.t_discharge_max + ))); + } + + Ok(Self { + t_suction_min: inner.t_suction_min, + t_suction_max: inner.t_suction_max, + t_discharge_min: inner.t_discharge_min, + t_discharge_max: inner.t_discharge_max, + }) + } +} + +// --------------------------------------------------------------------------- +// BPHX data types +// --------------------------------------------------------------------------- + +/// Brazed-plate heat exchanger (BPHX) parameters from vendor catalog. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BphxParameters { + /// Equipment model identifier (e.g. `"B5THx20"`). + pub model: String, + /// Manufacturer name (e.g. `"SWEP"`). + pub manufacturer: String, + /// Number of plates. + pub num_plates: usize, + /// Total heat-transfer area (m²). + pub area: f64, + /// Hydraulic diameter (m). + pub dh: f64, + /// Chevron angle (degrees). + pub chevron_angle: f64, + /// Nominal overall heat-transfer coefficient × area product (W/K) + /// at reference operating conditions. + pub ua_nominal: f64, + /// Optional UA part-load curve for off-design conditions. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ua_curve: Option, +} + +/// UA part-load correction curve. +/// +/// Each point maps a mass-flow ratio (actual / reference) to a UA ratio +/// (actual UA / nominal UA). Linear interpolation is used between points. +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct UaCurve { + /// Sorted list of `(mass_flow_ratio, ua_ratio)` points. + pub points: Vec<(f64, f64)>, +} + +impl<'de> Deserialize<'de> for UaCurve { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Inner { + points: Vec<(f64, f64)>, + } + + let mut inner = Inner::deserialize(deserializer)?; + + // Sort points by mass_flow_ratio (x-axis) to ensure interpolation works correctly + // even if the JSON is out of order. f64 sorting requires partial_cmp. + inner.points.sort_by(|a, b| { + a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal) + }); + + Ok(Self { points: inner.points }) + } +} + +impl UaCurve { + /// Linearly interpolate the UA ratio for a given mass-flow ratio. + /// + /// Values outside the curve range are clamped to the nearest endpoint. + /// Returns `None` if the curve has no points. + pub fn interpolate(&self, mass_flow_ratio: f64) -> Option { + let pts = &self.points; + if pts.is_empty() { + return None; + } + if pts.len() == 1 { + return Some(pts[0].1); + } + + // Clamp to endpoints + if mass_flow_ratio <= pts[0].0 { + return Some(pts[0].1); + } + if mass_flow_ratio >= pts[pts.len() - 1].0 { + return Some(pts[pts.len() - 1].1); + } + + // Find enclosing interval and interpolate + for window in pts.windows(2) { + let (x0, y0) = window[0]; + let (x1, y1) = window[1]; + if mass_flow_ratio >= x0 && mass_flow_ratio <= x1 { + let t = (mass_flow_ratio - x0) / (x1 - x0); + return Some(y0 + t * (y1 - y0)); + } + } + + // Fallback (should not be reached with sorted points) + Some(pts[pts.len() - 1].1) + } +} + +/// Input parameters for a vendor-specific UA calculation. +#[derive(Debug, Clone)] +pub struct UaCalcParams { + /// Current mass flow rate (kg/s). + pub mass_flow: f64, + /// Reference mass flow rate (kg/s) at which `ua_nominal` was measured. + pub mass_flow_ref: f64, + /// Hot-side inlet temperature (K). + pub temperature_hot_in: f64, + /// Cold-side inlet temperature (K). + pub temperature_cold_in: f64, + /// Refrigerant designation. + pub refrigerant: String, +} + +// --------------------------------------------------------------------------- +// VendorBackend trait +// --------------------------------------------------------------------------- + +/// Trait for accessing manufacturer equipment data. +/// +/// Implementations load catalog data from vendor-specific file formats +/// (JSON, CSV, etc.) and expose a uniform query API. +/// +/// The trait is object-safe (`Send + Sync`) so it can be stored as +/// `Box` or `Arc`. +pub trait VendorBackend: Send + Sync { + /// Human-readable name of the vendor (e.g. `"Copeland (Emerson)"`). + fn vendor_name(&self) -> &str; + + /// List all compressor models available in this vendor's catalog. + fn list_compressor_models(&self) -> Result, VendorError>; + + /// Retrieve AHRI 540 coefficients for a specific compressor model. + fn get_compressor_coefficients( + &self, + model: &str, + ) -> Result; + + /// List all BPHX models available in this vendor's catalog. + fn list_bphx_models(&self) -> Result, VendorError>; + + /// Retrieve catalog parameters for a specific BPHX model. + fn get_bphx_parameters(&self, model: &str) -> Result; + + /// Compute UA for a given operating point using vendor-specific methods. + /// + /// The default implementation simply returns `ua_nominal`. + fn compute_ua(&self, model: &str, _params: &UaCalcParams) -> Result { + let bphx = self.get_bphx_parameters(model)?; + Ok(bphx.ua_nominal) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + // ---- Serialization round-trip: CompressorCoefficients ---- + + #[test] + fn test_compressor_coefficients_json_round_trip() { + let coeffs = CompressorCoefficients { + model: "ZP54KCE-TFD".into(), + manufacturer: "Copeland".into(), + refrigerant: "R410A".into(), + capacity_coeffs: [18000.0, 350.0, -120.0, 2.5, 1.8, -4.2, 0.05, 0.03, -0.02, 0.01], + power_coeffs: [4500.0, 95.0, 45.0, 0.8, 0.5, 1.2, 0.02, 0.01, 0.01, 0.005], + mass_flow_coeffs: None, + validity: CompressorValidityRange { + t_suction_min: -10.0, + t_suction_max: 20.0, + t_discharge_min: 25.0, + t_discharge_max: 65.0, + }, + }; + + let json = serde_json::to_string_pretty(&coeffs).expect("serialize"); + let deserialized: CompressorCoefficients = + serde_json::from_str(&json).expect("deserialize"); + assert_eq!(coeffs, deserialized); + } + + #[test] + fn test_compressor_coefficients_with_mass_flow() { + let coeffs = CompressorCoefficients { + model: "TEST-001".into(), + manufacturer: "TestVendor".into(), + refrigerant: "R134a".into(), + capacity_coeffs: [1.0; 10], + power_coeffs: [2.0; 10], + mass_flow_coeffs: Some([0.1; 10]), + validity: CompressorValidityRange { + t_suction_min: -15.0, + t_suction_max: 15.0, + t_discharge_min: 30.0, + t_discharge_max: 60.0, + }, + }; + + let json = serde_json::to_string(&coeffs).expect("serialize"); + let deserialized: CompressorCoefficients = + serde_json::from_str(&json).expect("deserialize"); + assert_eq!(coeffs, deserialized); + assert!(deserialized.mass_flow_coeffs.is_some()); + } + + // ---- Serialization round-trip: BphxParameters ---- + + #[test] + fn test_bphx_parameters_json_round_trip_without_curve() { + let params = BphxParameters { + model: "B5THx20".into(), + manufacturer: "SWEP".into(), + num_plates: 20, + area: 0.45, + dh: 0.003, + chevron_angle: 65.0, + ua_nominal: 1500.0, + ua_curve: None, + }; + + let json = serde_json::to_string_pretty(¶ms).expect("serialize"); + let deserialized: BphxParameters = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(params, deserialized); + assert!(deserialized.ua_curve.is_none()); + } + + #[test] + fn test_bphx_parameters_json_round_trip_with_curve() { + let params = BphxParameters { + model: "B8THx30".into(), + manufacturer: "SWEP".into(), + num_plates: 30, + area: 0.72, + dh: 0.0025, + chevron_angle: 60.0, + ua_nominal: 2500.0, + ua_curve: Some(UaCurve { + points: vec![(0.2, 0.3), (0.5, 0.65), (1.0, 1.0), (1.5, 1.2)], + }), + }; + + let json = serde_json::to_string_pretty(¶ms).expect("serialize"); + let deserialized: BphxParameters = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(params, deserialized); + assert!(deserialized.ua_curve.is_some()); + } + + // ---- UaCurve interpolation ---- + + #[test] + fn test_ua_curve_interpolation_midpoint() { + let curve = UaCurve { + points: vec![(0.0, 0.0), (1.0, 1.0), (2.0, 1.5)], + }; + + // Exact point + assert_eq!(curve.interpolate(0.0), Some(0.0)); + assert_eq!(curve.interpolate(1.0), Some(1.0)); + assert_eq!(curve.interpolate(2.0), Some(1.5)); + + // Midpoint + let mid = curve.interpolate(0.5).unwrap(); + assert!((mid - 0.5).abs() < 1e-10); + + let mid2 = curve.interpolate(1.5).unwrap(); + assert!((mid2 - 1.25).abs() < 1e-10); + } + + #[test] + fn test_ua_curve_interpolation_clamp() { + let curve = UaCurve { + points: vec![(0.5, 0.4), (1.0, 1.0), (1.5, 1.3)], + }; + + // Below range → clamp to first + assert_eq!(curve.interpolate(0.1), Some(0.4)); + + // Above range → clamp to last + assert_eq!(curve.interpolate(3.0), Some(1.3)); + } + + #[test] + fn test_ua_curve_interpolation_empty() { + let curve = UaCurve { points: vec![] }; + assert_eq!(curve.interpolate(1.0), None); + } + + #[test] + fn test_ua_curve_interpolation_single_point() { + let curve = UaCurve { + points: vec![(1.0, 0.8)], + }; + assert_eq!(curve.interpolate(0.5), Some(0.8)); + assert_eq!(curve.interpolate(2.0), Some(0.8)); + } + + // ---- VendorError display ---- + + #[test] + fn test_vendor_error_display_model_not_found() { + let err = VendorError::ModelNotFound("XYZ-999".into()); + assert_eq!(err.to_string(), "Model not found: XYZ-999"); + } + + #[test] + fn test_vendor_error_display_invalid_format() { + let err = VendorError::InvalidFormat("missing capacity_coeffs".into()); + assert_eq!(err.to_string(), "Invalid data format: missing capacity_coeffs"); + } + + #[test] + fn test_vendor_error_display_file_not_found() { + let err = VendorError::FileNotFound("/data/missing.json".into()); + assert_eq!(err.to_string(), "Data file not found: /data/missing.json"); + } + + // ---- Mock VendorBackend ---- + + struct MockVendor { + compressors: std::collections::HashMap, + bphx: std::collections::HashMap, + } + + impl MockVendor { + fn new() -> Self { + let mut compressors = std::collections::HashMap::new(); + compressors.insert( + "TEST-COMP".into(), + CompressorCoefficients { + model: "TEST-COMP".into(), + manufacturer: "MockVendor".into(), + refrigerant: "R410A".into(), + capacity_coeffs: [1.0; 10], + power_coeffs: [2.0; 10], + mass_flow_coeffs: None, + validity: CompressorValidityRange { + t_suction_min: -10.0, + t_suction_max: 20.0, + t_discharge_min: 25.0, + t_discharge_max: 65.0, + }, + }, + ); + + let mut bphx = std::collections::HashMap::new(); + bphx.insert( + "TEST-HX".into(), + BphxParameters { + model: "TEST-HX".into(), + manufacturer: "MockVendor".into(), + num_plates: 20, + area: 0.5, + dh: 0.003, + chevron_angle: 65.0, + ua_nominal: 2000.0, + ua_curve: None, + }, + ); + + Self { compressors, bphx } + } + } + + impl VendorBackend for MockVendor { + fn vendor_name(&self) -> &str { + "MockVendor" + } + + fn list_compressor_models(&self) -> Result, VendorError> { + Ok(self.compressors.keys().cloned().collect()) + } + + fn get_compressor_coefficients( + &self, + model: &str, + ) -> Result { + self.compressors + .get(model) + .cloned() + .ok_or_else(|| VendorError::ModelNotFound(model.into())) + } + + fn list_bphx_models(&self) -> Result, VendorError> { + Ok(self.bphx.keys().cloned().collect()) + } + + fn get_bphx_parameters(&self, model: &str) -> Result { + self.bphx + .get(model) + .cloned() + .ok_or_else(|| VendorError::ModelNotFound(model.into())) + } + } + + #[test] + fn test_mock_vendor_list_compressors() { + let vendor = MockVendor::new(); + let models = vendor.list_compressor_models().unwrap(); + assert_eq!(models.len(), 1); + assert!(models.contains(&"TEST-COMP".to_string())); + } + + #[test] + fn test_mock_vendor_get_compressor() { + let vendor = MockVendor::new(); + let coeffs = vendor.get_compressor_coefficients("TEST-COMP").unwrap(); + assert_eq!(coeffs.model, "TEST-COMP"); + assert_eq!(coeffs.manufacturer, "MockVendor"); + assert_eq!(coeffs.refrigerant, "R410A"); + } + + #[test] + fn test_mock_vendor_compressor_not_found() { + let vendor = MockVendor::new(); + let result = vendor.get_compressor_coefficients("NONEXISTENT"); + assert!(result.is_err()); + match result.unwrap_err() { + VendorError::ModelNotFound(m) => assert_eq!(m, "NONEXISTENT"), + other => panic!("Expected ModelNotFound, got: {:?}", other), + } + } + + #[test] + fn test_mock_vendor_list_bphx() { + let vendor = MockVendor::new(); + let models = vendor.list_bphx_models().unwrap(); + assert_eq!(models.len(), 1); + assert!(models.contains(&"TEST-HX".to_string())); + } + + #[test] + fn test_mock_vendor_get_bphx() { + let vendor = MockVendor::new(); + let params = vendor.get_bphx_parameters("TEST-HX").unwrap(); + assert_eq!(params.model, "TEST-HX"); + assert_eq!(params.num_plates, 20); + } + + #[test] + fn test_mock_vendor_bphx_not_found() { + let vendor = MockVendor::new(); + let result = vendor.get_bphx_parameters("NONEXISTENT"); + assert!(result.is_err()); + } + + #[test] + fn test_default_compute_ua_returns_nominal() { + let vendor = MockVendor::new(); + let params = UaCalcParams { + mass_flow: 0.3, + mass_flow_ref: 0.5, + temperature_hot_in: 340.0, + temperature_cold_in: 280.0, + refrigerant: "R410A".into(), + }; + let ua = vendor.compute_ua("TEST-HX", ¶ms).unwrap(); + assert!((ua - 2000.0).abs() < 1e-10); + } + + #[test] + fn test_default_compute_ua_model_not_found() { + let vendor = MockVendor::new(); + let params = UaCalcParams { + mass_flow: 0.3, + mass_flow_ref: 0.5, + temperature_hot_in: 340.0, + temperature_cold_in: 280.0, + refrigerant: "R410A".into(), + }; + let result = vendor.compute_ua("NONEXISTENT", ¶ms); + assert!(result.is_err()); + } + + // ---- Object safety ---- + + #[test] + fn test_vendor_backend_is_object_safe() { + let vendor: Box = Box::new(MockVendor::new()); + assert_eq!(vendor.vendor_name(), "MockVendor"); + let models = vendor.list_compressor_models().unwrap(); + assert!(!models.is_empty()); + } +}