use fastsim_core::si;
use fastsim_core::utils::tracked_state::TrackedState;
use fastsim_proc_macros::{serde_api, HistoryVec};
use serde::{Deserialize, Serialize};
#[serde_api]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, HistoryVec)]
#[history_vec(no_pyo3)]
#[serde(default)]
struct TestDeviceState {
pwr_out: si::Power,
#[si_unit(unitless)]
eff: si::Ratio,
}
#[serde_api]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct TestDevice {
mass: si::Mass,
power: si::Power,
speed: si::Velocity,
duration: si::Time,
area: si::Area,
temperature: si::Temperature,
energy: si::Energy,
secondary_temperature: Option<si::Temperature>,
temperature_readings: Vec<si::Temperature>,
#[si_unit(unitless)]
efficiency: si::Ratio,
tracked_power: TrackedState<si::Power>,
#[si_unit(unitless)]
tracked_efficiency: TrackedState<si::Ratio>,
#[serde(alias = "old_power")]
renamed_power: si::Power,
#[serde(default)]
specific_energy: Option<si::SpecificEnergy>,
#[serde(default)]
state: TestDeviceState,
#[serde(default)]
history: TestDeviceStateHistoryVec,
}
#[test]
fn test_deserialize_all_primary_units() {
let json = r#"{
"mass_kilograms": 2000.0,
"power_watts": 150.0,
"speed_meters_per_second": 30.0,
"duration_seconds": 120.0,
"area_square_meters": 5.0,
"temperature_kelvin": 300.0,
"energy_joules": 1000.0,
"efficiency_ratio": 0.85,
"temperature_readings_kelvin": [290.0, 300.0, 310.0],
"tracked_power_watts": 100.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice =
serde_json::from_str(json).expect("Failed to deserialize with primary units");
assert_eq!(device.mass.get::<si::kilogram>(), 2000.0);
assert_eq!(device.power.get::<si::watt>(), 150.0);
assert_eq!(device.speed.get::<si::meter_per_second>(), 30.0);
assert_eq!(device.duration.get::<si::second>(), 120.0);
assert_eq!(device.area.get::<si::square_meter>(), 5.0);
assert_eq!(device.temperature.get::<si::kelvin_abs>(), 300.0);
assert_eq!(device.energy.get::<si::joule>(), 1000.0);
assert_eq!(device.efficiency.get::<si::ratio>(), 0.85);
assert_eq!(device.secondary_temperature, None);
assert_eq!(device.temperature_readings.len(), 3);
assert_eq!(
device
.tracked_power
.get_fresh(|| "".into())
.unwrap()
.get::<si::watt>(),
100.0
);
assert_eq!(
device
.tracked_efficiency
.get_fresh(|| "".into())
.unwrap()
.get::<si::ratio>(),
0.90
);
}
#[test]
fn test_deserialize_fails_with_missing_required_fields() {
let json = r#"{
"mass_kilograms": 2000.0,
"power_watts": 150.0,
"speed_meters_per_second": 30.0,
"duration_seconds": 120.0,
"area_square_meters": 5.0,
"energy_joules": 1000.0,
"efficiency_ratio": 0.85,
"tracked_power_watts": 100.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 50.0
}"#;
let result: Result<TestDevice, _> = serde_json::from_str(json);
assert!(
result.is_err(),
"Should return Err when required temperature field is missing"
);
}
#[test]
fn test_deserialize_with_alternate_units() {
let json = r#"{
"mass_kilograms": 2000.0,
"power_kilowatts": 0.15,
"speed_kilometers_per_hour": 108.0,
"duration_hours": 2.0,
"area_square_meters": 5.0,
"temperature_kelvin": 300.15,
"energy_kilowatt_hours": 1.0,
"efficiency_percent": 85.0,
"secondary_temperature_degrees_celsius": 25.0,
"temperature_readings_degrees_fahrenheit": [32.0, 68.0, 104.0],
"tracked_power_kilowatts": 0.5,
"tracked_efficiency_percent": 80.0,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice =
serde_json::from_str(json).expect("Failed to deserialize with alternate units");
assert_eq!(device.power.get::<si::watt>(), 150.0); let speed = device.speed.get::<si::meter_per_second>();
assert!((speed - 30.0).abs() < 0.1); assert_eq!(device.energy.get::<si::joule>(), 3_600_000.0); assert_eq!(device.duration.get::<si::second>(), 7_200.0); assert_eq!(device.efficiency.get::<si::ratio>(), 0.85); assert!(
(device
.tracked_power
.get_fresh(|| "".into())
.unwrap()
.get::<si::watt>()
- 500.0)
.abs()
< 1e-9
); assert!(
(device
.tracked_efficiency
.get_fresh(|| "".into())
.unwrap()
.get::<si::ratio>()
- 0.80)
.abs()
< 1e-9
);
assert!(device.secondary_temperature.is_some());
let secondary_temp = device.secondary_temperature.unwrap();
assert!((secondary_temp.get::<si::kelvin_abs>() - 298.15).abs() < 0.01);
assert_eq!(device.temperature_readings.len(), 3);
assert!((device.temperature_readings[0].get::<si::kelvin_abs>() - 273.15).abs() < 0.5);
assert!((device.temperature_readings[1].get::<si::kelvin_abs>() - 293.15).abs() < 0.5);
assert!((device.temperature_readings[2].get::<si::kelvin_abs>() - 313.15).abs() < 0.5);
}
#[test]
fn test_deserialize_time_in_hours() {
let json = r#"{
"mass_kilograms": 2000.0,
"power_watts": 150.0,
"speed_meters_per_second": 30.0,
"duration_hours": 2.0,
"area_square_meters": 5.0,
"temperature_kelvin": 300.0,
"energy_joules": 1000.0,
"efficiency_ratio": 0.85,
"tracked_power_watts": 100.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice =
serde_json::from_str(json).expect("Failed to deserialize with primary units");
assert_eq!(device.duration.get::<si::second>(), 7_200.0); }
#[test]
fn test_deserialize_multiple_si_quantities() {
let json = r#"{
"mass_kilograms": 1500.0,
"power_watts": 200.0,
"speed_meters_per_second": 25.0,
"duration_seconds": 300.0,
"area_square_meters": 10.0,
"temperature_kelvin": 298.15,
"energy_joules": 5000.0,
"efficiency_ratio": 0.90,
"tracked_power_watts": 50.0,
"tracked_efficiency_ratio": 0.88,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice =
serde_json::from_str(json).expect("Failed to deserialize multiple SI quantities");
assert_eq!(device.mass.get::<si::kilogram>(), 1500.0);
assert_eq!(device.power.get::<si::watt>(), 200.0);
assert_eq!(device.speed.get::<si::meter_per_second>(), 25.0);
assert_eq!(device.duration.get::<si::second>(), 300.0);
assert_eq!(device.area.get::<si::square_meter>(), 10.0);
assert_eq!(device.temperature.get::<si::kelvin_abs>(), 298.15);
assert_eq!(device.energy.get::<si::joule>(), 5000.0);
assert_eq!(device.efficiency.get::<si::ratio>(), 0.90);
}
#[test]
fn test_serialize_uses_prescribed_units() {
let json = r#"{
"mass_kilograms": 2000.0,
"power_watts": 150.0,
"speed_meters_per_second": 30.0,
"duration_seconds": 120.0,
"area_square_meters": 5.0,
"temperature_kelvin": 300.0,
"energy_joules": 1000.0,
"efficiency_ratio": 0.85,
"temperature_readings_kelvin": [290.0, 300.0, 310.0],
"tracked_power_watts": 100.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice = serde_json::from_str(json).expect("Failed to deserialize");
let serialized = serde_json::to_string(&device).expect("Failed to serialize");
let value: serde_json::Value =
serde_json::from_str(&serialized).expect("Failed to parse serialized JSON");
assert!(value.get("mass_kilograms").is_some());
assert!(value.get("power_watts").is_some());
assert!(value.get("speed_meters_per_second").is_some());
assert!(value.get("duration_seconds").is_some());
assert!(value.get("area_square_meters").is_some());
assert!(value.get("temperature_kelvin").is_some());
assert!(value.get("energy_joules").is_some());
assert!(value.get("efficiency").is_some()); assert!(value.get("tracked_power_watts").is_some());
assert!(value.get("tracked_efficiency").is_some()); assert!(value.get("renamed_power_watts").is_some());
}
#[test]
fn test_round_trip_conversion_preserves_values() {
let json_input = r#"{
"mass_kilograms": 2000.0,
"power_watts": 150.0,
"speed_meters_per_second": 30.0,
"duration_seconds": 3600.0,
"area_square_meters": 5.0,
"temperature_kelvin": 300.0,
"energy_joules": 1000.0,
"efficiency_ratio": 0.85,
"temperature_readings_kelvin": [290.0, 300.0, 310.0],
"tracked_power_watts": 100.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice = serde_json::from_str(json_input).expect("Failed to deserialize");
let serialized = serde_json::to_string(&device).expect("Failed to serialize");
let value: serde_json::Value =
serde_json::from_str(&serialized).expect("Failed to parse serialized JSON");
assert_eq!(value["mass_kilograms"], 2000.0);
assert_eq!(value["power_watts"], 150.0);
assert_eq!(value["speed_meters_per_second"], 30.0);
assert_eq!(value["duration_seconds"], 3600.0);
assert_eq!(value["area_square_meters"], 5.0);
assert_eq!(value["temperature_kelvin"], 300.0);
assert_eq!(value["energy_joules"], 1000.0);
assert_eq!(value["efficiency"], 0.85); }
#[test]
fn test_deserialize_with_different_values() {
let json = r#"{
"mass_kilograms": 5000.0,
"power_watts": 500.0,
"speed_meters_per_second": 50.0,
"duration_seconds": 600.0,
"area_square_meters": 20.0,
"temperature_kelvin": 400.0,
"energy_joules": 10000.0,
"efficiency_ratio": 0.95,
"temperature_readings_kelvin": [380.0, 400.0, 420.0],
"tracked_power_watts": 200.0,
"tracked_efficiency_ratio": 0.75,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice =
serde_json::from_str(json).expect("Failed to deserialize with different values");
assert_eq!(device.mass.get::<si::kilogram>(), 5000.0);
assert_eq!(device.power.get::<si::watt>(), 500.0);
assert_eq!(device.speed.get::<si::meter_per_second>(), 50.0);
assert_eq!(device.duration.get::<si::second>(), 600.0);
assert_eq!(device.area.get::<si::square_meter>(), 20.0);
assert_eq!(device.temperature.get::<si::kelvin_abs>(), 400.0);
assert_eq!(device.energy.get::<si::joule>(), 10000.0);
}
#[test]
fn test_tracked_state_deserialize_primary_units() {
let json = r#"{
"mass_kilograms": 1000.0,
"power_watts": 100.0,
"speed_meters_per_second": 10.0,
"duration_seconds": 60.0,
"area_square_meters": 2.0,
"temperature_kelvin": 293.0,
"energy_joules": 500.0,
"efficiency_ratio": 0.80,
"tracked_power_watts": 250.0,
"tracked_efficiency_ratio": 0.92,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice =
serde_json::from_str(json).expect("Failed to deserialize TestDevice primary units");
assert_eq!(
device
.tracked_power
.get_fresh(|| "".into())
.unwrap()
.get::<si::watt>(),
250.0
);
assert_eq!(
device
.tracked_efficiency
.get_fresh(|| "".into())
.unwrap()
.get::<si::ratio>(),
0.92
);
}
#[test]
fn test_tracked_state_deserialize_alternate_units() {
let json = r#"{
"mass_kilograms": 1000.0,
"power_watts": 100.0,
"speed_meters_per_second": 10.0,
"duration_seconds": 60.0,
"area_square_meters": 2.0,
"temperature_kelvin": 293.0,
"energy_joules": 500.0,
"efficiency_ratio": 0.80,
"tracked_power_kilowatts": 0.5,
"tracked_efficiency_percent": 85.0,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice =
serde_json::from_str(json).expect("Failed to deserialize TestDevice alternate units");
assert!(
(device
.tracked_power
.get_fresh(|| "".into())
.unwrap()
.get::<si::watt>()
- 500.0)
.abs()
< 1e-9
);
assert!(
(device
.tracked_efficiency
.get_fresh(|| "".into())
.unwrap()
.get::<si::ratio>()
- 0.85)
.abs()
< 1e-9
);
}
#[test]
fn test_tracked_state_deserialize_bare_name() {
let json = r#"{
"mass_kilograms": 1000.0,
"power_watts": 100.0,
"speed_meters_per_second": 10.0,
"duration_seconds": 60.0,
"area_square_meters": 2.0,
"temperature_kelvin": 293.0,
"energy_joules": 500.0,
"efficiency_ratio": 0.80,
"tracked_power": 300.0,
"tracked_efficiency": 0.78,
"renamed_power_watts": 50.0
}"#;
let device: TestDevice =
serde_json::from_str(json).expect("Failed to deserialize TestDevice with bare names");
assert_eq!(
device
.tracked_power
.get_fresh(|| "".into())
.unwrap()
.get::<si::watt>(),
300.0
);
assert_eq!(
device
.tracked_efficiency
.get_fresh(|| "".into())
.unwrap()
.get::<si::ratio>(),
0.78
);
}
#[test]
#[cfg(feature = "csv")]
fn test_cycle_csv_loads_bare_grade_column() {
use fastsim_core::drive_cycle::CycleElement;
let csv = "time_seconds,speed_meters_per_second,grade\n\
0,0,0\n\
1,2.5,0.01\n\
2,5.0,-0.005\n";
let mut rdr = csv::Reader::from_reader(csv.as_bytes());
let elements: Vec<CycleElement> = rdr
.deserialize()
.map(|r| r.expect("Failed to deserialize CycleElement row"))
.collect();
assert_eq!(elements.len(), 3);
assert_eq!(elements[0].time.get::<si::second>(), 0.0);
assert_eq!(elements[1].speed.get::<si::meter_per_second>(), 2.5);
assert!((elements[1].grade.unwrap().get::<si::ratio>() - 0.01).abs() < 1e-10);
assert!((elements[2].grade.unwrap().get::<si::ratio>() - (-0.005)).abs() < 1e-10);
}
#[test]
#[cfg(feature = "csv")]
fn test_cycle_csv_loads_grade_ratio_column() {
use fastsim_core::drive_cycle::CycleElement;
let csv = "time_seconds,speed_meters_per_second,grade_ratio\n\
0,0,0\n\
1,10.0,0.02\n";
let mut rdr = csv::Reader::from_reader(csv.as_bytes());
let elements: Vec<CycleElement> = rdr
.deserialize()
.map(|r| r.expect("Failed to deserialize CycleElement row"))
.collect();
assert_eq!(elements.len(), 2);
assert!((elements[1].grade.unwrap().get::<si::ratio>() - 0.02).abs() < 1e-10);
}
#[test]
#[cfg(feature = "csv")]
fn test_cycle_csv_legacy_aliases() {
use fastsim_core::drive_cycle::CycleElement;
let csv = "cycSecs,cycMps,cycGrade\n\
0,0,0\n\
1,5.0,0.05\n\
2,10.0,-0.02\n";
let mut rdr = csv::Reader::from_reader(csv.as_bytes());
let elements: Vec<CycleElement> = rdr
.deserialize()
.map(|r| r.expect("Failed to deserialize CycleElement row with legacy aliases"))
.collect();
assert_eq!(elements.len(), 3);
assert_eq!(elements[0].time.get::<si::second>(), 0.0);
assert_eq!(elements[1].speed.get::<si::meter_per_second>(), 5.0);
assert!((elements[1].grade.unwrap().get::<si::ratio>() - 0.05).abs() < 1e-10);
assert!((elements[2].grade.unwrap().get::<si::ratio>() - (-0.02)).abs() < 1e-10);
}
#[test]
fn test_alias_prefix_expands_to_all_unit_variants() {
let base_json = r#"{
"mass_kilograms": 1000.0,
"power_watts": 100.0,
"speed_meters_per_second": 10.0,
"duration_seconds": 60.0,
"area_square_meters": 2.0,
"temperature_kelvin": 293.0,
"energy_joules": 500.0,
"efficiency_ratio": 0.80,
"tracked_power_watts": 50.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 0.0
}"#;
let json = base_json.replace(r#""renamed_power_watts": 0.0"#, r#""old_power": 200.0"#);
let d: TestDevice = serde_json::from_str(&json).expect("old_power (bare) should deserialize");
assert_eq!(d.renamed_power.get::<si::watt>(), 200.0);
let json = base_json.replace(
r#""renamed_power_watts": 0.0"#,
r#""old_power_watts": 300.0"#,
);
let d: TestDevice = serde_json::from_str(&json).expect("old_power_watts should deserialize");
assert_eq!(d.renamed_power.get::<si::watt>(), 300.0);
let json = base_json.replace(
r#""renamed_power_watts": 0.0"#,
r#""old_power_kilowatts": 0.5"#,
);
let d: TestDevice =
serde_json::from_str(&json).expect("old_power_kilowatts should deserialize");
assert!((d.renamed_power.get::<si::watt>() - 500.0).abs() < 1e-9);
let json = base_json.replace(
r#""renamed_power_watts": 0.0"#,
r#""old_power_horsepower": 1.0"#,
);
let d: TestDevice =
serde_json::from_str(&json).expect("old_power_horsepower should deserialize");
assert!((d.renamed_power.get::<si::watt>() - 745.7).abs() < 0.1);
let json = base_json.replace(
r#""renamed_power_watts": 0.0"#,
r#""renamed_power_kilowatts": 0.15"#,
);
let d: TestDevice =
serde_json::from_str(&json).expect("renamed_power_kilowatts should deserialize");
assert!((d.renamed_power.get::<si::watt>() - 150.0).abs() < 1e-9);
}
#[test]
fn test_multi_unit_conflict_is_rejected() {
let json = r#"{
"mass_kilograms": 1000.0,
"power_watts": 100.0,
"power_kilowatts": 0.1,
"speed_meters_per_second": 10.0,
"duration_seconds": 60.0,
"area_square_meters": 2.0,
"temperature_kelvin": 293.0,
"energy_joules": 500.0,
"efficiency_ratio": 0.80,
"tracked_power_watts": 50.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 200.0
}"#;
let result: Result<TestDevice, _> = serde_json::from_str(json);
assert!(
result.is_err(),
"deserializing with two unit variants for `power` must return an error"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("power"),
"error message should mention the conflicting field; got: {err_msg}"
);
}
#[test]
fn test_state_history_push_and_round_trip() {
let json = r#"{
"mass_kilograms": 1000.0,
"power_watts": 100.0,
"speed_meters_per_second": 10.0,
"duration_seconds": 60.0,
"area_square_meters": 2.0,
"temperature_kelvin": 293.0,
"energy_joules": 500.0,
"efficiency_ratio": 0.80,
"tracked_power_watts": 50.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 200.0
}"#;
let mut device: TestDevice = serde_json::from_str(json).unwrap();
assert_eq!(device.history.len(), 0);
device.state.pwr_out = si::Power::new::<si::watt>(500.0);
device.state.eff = si::Ratio::new::<si::ratio>(0.9);
device.history.push(device.state.clone());
device.state.pwr_out = si::Power::new::<si::watt>(750.0);
device.state.eff = si::Ratio::new::<si::ratio>(0.85);
device.history.push(device.state.clone());
assert_eq!(device.history.len(), 2);
let json = serde_json::to_string(&device).unwrap();
let rt: TestDevice = serde_json::from_str(&json).expect("round-trip failed");
assert_eq!(rt.history.len(), 2);
assert!((rt.history.pwr_out[0].get::<si::watt>() - 500.0).abs() < 1e-9);
assert!((rt.history.pwr_out[1].get::<si::watt>() - 750.0).abs() < 1e-9);
assert!((rt.history.eff[0].get::<si::ratio>() - 0.9).abs() < 1e-9);
assert!((rt.history.eff[1].get::<si::ratio>() - 0.85).abs() < 1e-9);
}
#[test]
fn test_state_history_deserializes_alternate_units() {
let json = r#"{
"mass_kilograms": 1000.0,
"power_watts": 100.0,
"speed_meters_per_second": 10.0,
"duration_seconds": 60.0,
"area_square_meters": 2.0,
"temperature_kelvin": 293.0,
"energy_joules": 500.0,
"efficiency_ratio": 0.80,
"tracked_power_watts": 50.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 200.0,
"history": {
"pwr_out_kilowatts": [0.5, 0.75],
"eff_percent": [90.0, 85.0]
}
}"#;
let device: TestDevice = serde_json::from_str(json).expect("alternate unit in history failed");
assert_eq!(device.history.len(), 2);
assert!((device.history.pwr_out[0].get::<si::watt>() - 500.0).abs() < 1e-9);
assert!((device.history.pwr_out[1].get::<si::watt>() - 750.0).abs() < 1e-9);
assert!((device.history.eff[0].get::<si::ratio>() - 0.9).abs() < 1e-9);
assert!((device.history.eff[1].get::<si::ratio>() - 0.85).abs() < 1e-9);
}
#[serde_api]
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
struct OverrideDevice {
#[si_unit(kilowatts)]
pwr_out: si::Power,
speed: si::Velocity,
}
#[serde_api]
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
struct UnitlessRatioDevice {
#[si_unit(unitless)]
alt_eff: si::Ratio,
#[si_unit(unitless)]
cop: si::Ratio,
grade: si::Ratio,
}
#[test]
fn test_si_unit_override_changes_serialization_key() {
let device = OverrideDevice {
pwr_out: si::Power::new::<si::watt>(500.0),
speed: si::Velocity::new::<si::meter_per_second>(10.0),
};
let json = serde_json::to_string(&device).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(
value.get("pwr_out_kilowatts").is_some(),
"expected pwr_out_kilowatts key; got: {json}"
);
assert!(
value.get("pwr_out_watts").is_none(),
"unexpected pwr_out_watts key when kilowatts override is set"
);
assert!(value.get("speed_meters_per_second").is_some());
}
const SE_BASE: &str = r#"{
"mass_kilograms": 1000.0,
"power_watts": 100.0,
"speed_meters_per_second": 10.0,
"duration_seconds": 60.0,
"area_square_meters": 2.0,
"temperature_kelvin": 293.15,
"energy_joules": 500.0,
"efficiency_ratio": 0.85,
"tracked_power_watts": 50.0,
"tracked_efficiency_ratio": 0.90,
"renamed_power_watts": 200.0
}"#;
#[test]
fn test_specific_energy_serializes_as_joules_per_kilogram() {
let json = SE_BASE.replace(
"}",
r#", "specific_energy_watt_hours_per_kilogram": 100.0}"#,
);
let device: TestDevice = serde_json::from_str(&json).unwrap();
let serialized = serde_json::to_string(&device).unwrap();
let value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert!(
value.get("specific_energy_joules_per_kilogram").is_some(),
"expected specific_energy_joules_per_kilogram in serialized output; got: {serialized}"
);
let stored = value["specific_energy_joules_per_kilogram"]
.as_f64()
.unwrap();
assert!((stored - 360_000.0).abs() < 1.0);
}
#[test]
fn test_specific_energy_multi_unit_round_trip() {
let expected_j_per_kg = 3_600_000.0_f64;
let cases: &[(&str, f64)] = &[
("specific_energy_joules_per_kilogram", expected_j_per_kg),
(
"specific_energy_kilojoules_per_kilogram",
expected_j_per_kg / 1_000.0,
),
(
"specific_energy_watt_hours_per_kilogram",
expected_j_per_kg / 3_600.0,
),
(
"specific_energy_kilowatt_hours_per_kilogram",
expected_j_per_kg / 3_600_000.0,
),
];
for (key, value) in cases {
let json = SE_BASE.replace("}", &format!(", \"{key}\": {value}}}"));
let device: TestDevice = serde_json::from_str(&json)
.unwrap_or_else(|e| panic!("failed to deserialize {key}: {e}"));
let se = device
.specific_energy
.unwrap_or_else(|| panic!("{key} gave None"));
assert!(
(se.get::<si::joule_per_kilogram>() - expected_j_per_kg).abs() < 1.0,
"{key}={value} → {:.0} J/kg (expected {expected_j_per_kg:.0})",
se.get::<si::joule_per_kilogram>()
);
}
let wh_json = SE_BASE.replace(
"}",
", \"specific_energy_watt_hours_per_kilogram\": 1000.0}",
);
let se = serde_json::from_str::<TestDevice>(&wh_json)
.unwrap()
.specific_energy
.unwrap();
assert!((se.get::<si::watt_hour_per_kilogram>() - 1000.0).abs() < 1e-6);
let kwh_json = SE_BASE.replace(
"}",
", \"specific_energy_kilowatt_hours_per_kilogram\": 0.5}",
);
let se = serde_json::from_str::<TestDevice>(&kwh_json)
.unwrap()
.specific_energy
.unwrap();
assert!((se.get::<si::kilowatt_hour_per_kilogram>() - 0.5).abs() < 1e-9);
}
#[test]
fn test_si_unit_override_round_trips() {
let json = r#"{ "pwr_out_watts": 500.0, "speed_meters_per_second": 10.0 }"#;
let device: OverrideDevice = serde_json::from_str(json).unwrap();
assert!((device.pwr_out.get::<si::watt>() - 500.0).abs() < 1e-9);
let out = serde_json::to_string(&device).unwrap();
let rt: OverrideDevice = serde_json::from_str(&out).unwrap();
assert!((rt.pwr_out.get::<si::watt>() - 500.0).abs() < 1e-9);
}
#[test]
fn test_si_unit_unitless_override_changes_ratio_canonical_name() {
let device = UnitlessRatioDevice {
alt_eff: si::Ratio::new::<si::ratio>(0.9),
cop: si::Ratio::new::<si::ratio>(2.5),
grade: si::Ratio::new::<si::ratio>(0.03),
};
let json = serde_json::to_string(&device).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(
value.get("alt_eff").is_some(),
"expected bare alt_eff in {json}"
);
assert!(value.get("cop").is_some(), "expected bare cop in {json}");
assert!(value.get("alt_eff_ratio").is_none());
assert!(value.get("cop_ratio").is_none());
assert!(
value.get("grade").is_some(),
"expected bare grade in {json}"
);
assert!(value.get("grade_ratio").is_none());
}