use std::{
fs,
path::{Path, PathBuf},
str::FromStr,
};
use gled_model::{
asset::{
Asset, AssetTrait, animation::Animation, curve::Curve, midi_controller::MidiController,
output_device::OutputDevice, palette::Palette, project::Project, scene::Scene,
},
asset_id::AssetId,
};
use serde_json::Value;
use uuid::Uuid;
const LEGACY_FIELDS: &[&str] = &[
"external_index",
"palette_rgb_overwrite",
"artnet_grid",
"artnet_grid_width",
"artnet_grid_height",
"artnet_scan_index",
];
fn assert_contained(original: &Value, saved: &Value, path: &str) {
match (original, saved) {
(Value::Object(original), Value::Object(saved)) => {
for (key, value) in original {
let Some(saved_value) = saved.get(key) else {
assert!(
LEGACY_FIELDS.contains(&key.as_str()),
"{path}.{key} was dropped when saving"
);
continue;
};
assert_contained(value, saved_value, &format!("{path}.{key}"));
}
}
(Value::Array(original), Value::Array(saved)) => {
assert_eq!(original.len(), saved.len(), "{path} changed its length");
for (index, (value, saved_value)) in original.iter().zip(saved).enumerate() {
assert_contained(value, saved_value, &format!("{path}[{index}]"));
}
}
(Value::Number(original), Value::Number(saved)) => {
let (Some(original), Some(saved)) = (original.as_f64(), saved.as_f64()) else {
panic!("{path} is not a number any more");
};
assert!(
(original - saved).abs() <= 1e-6 * original.abs().max(1.0),
"{path} changed from {original} to {saved}"
);
}
_ => assert_eq!(original, saved, "{path} changed"),
}
}
fn round_trip<T: AssetTrait>(dir: &Path) -> usize {
let Ok(entries) = fs::read_dir(dir.join(T::DIR_NAME)) else {
return 0;
};
let mut count = 0;
for entry in entries {
let path = entry.expect("directory entry").path();
if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
continue;
}
let stem = path
.file_stem()
.and_then(|stem| stem.to_str())
.expect("file name");
let id = AssetId::<T>::from_uuid(Uuid::from_str(stem).expect("file named by uuid"));
let text = fs::read_to_string(&path).expect("readable asset");
let asset = Asset::<T>::read(text.as_bytes(), id)
.unwrap_or_else(|err| panic!("{} does not load: {err}", path.display()));
let saved = (*asset).clone().into_json().expect("serializes");
let again = Asset::<T>::read(saved.as_bytes(), id).expect("saved asset loads");
assert_eq!(asset, again, "{} changed in a save", path.display());
let original: Value = serde_json::from_str(&text).expect("json");
let saved: Value = serde_json::from_str(&saved).expect("json");
assert_contained(&original, &saved, &path.display().to_string());
count += 1;
}
count
}
fn round_trip_all(dir: &Path) -> usize {
round_trip::<Animation>(dir)
+ round_trip::<Curve>(dir)
+ round_trip::<MidiController>(dir)
+ round_trip::<OutputDevice>(dir)
+ round_trip::<Palette>(dir)
+ round_trip::<Project>(dir)
+ round_trip::<Scene>(dir)
}
#[test]
fn fixtures_round_trip() {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
assert_eq!(round_trip_all(&dir), 8);
}
#[test]
#[ignore = "reads the local asset repository"]
fn local_asset_repository_round_trips() {
let dir = std::env::var_os("GLED_ASSETS_DIR")
.map(PathBuf::from)
.or_else(|| {
std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/share/gled2"))
})
.expect("GLED_ASSETS_DIR or HOME");
let count = round_trip_all(&dir);
println!("{count} assets round-trip in {}", dir.display());
assert!(count > 0, "no assets in {}", dir.display());
}