Files
clawhdf5/crates/clawhdf5-netcdf4/tests/netcdf4_tests.rs
T

757 lines
24 KiB
Rust

//! Comprehensive tests for clawhdf5-netcdf4.
//!
//! We create NetCDF-4 style HDF5 files using clawhdf5's FileBuilder with the
//! appropriate NetCDF-4 conventions (CLASS=DIMENSION_SCALE, _Netcdf4Dimid,
//! _NCProperties, CF attributes, etc.).
use clawhdf5::{AttrValue, FileBuilder};
use clawhdf5_netcdf4::{Dimension, FillValue, NcType, NetCDF4File};
// ---------------------------------------------------------------------------
// Helpers: build NetCDF-4 style HDF5 files in memory
// ---------------------------------------------------------------------------
/// Create a simple NetCDF-4 file with lat/lon dimensions and a temperature variable.
fn make_simple_netcdf4() -> Vec<u8> {
let mut b = FileBuilder::new();
// Root attributes (NetCDF-4 provenance)
b.set_attr(
"_NCProperties",
AttrValue::String("version=2,clawhdf5=0.1.0".into()),
);
b.set_attr("Conventions", AttrValue::String("CF-1.8".into()));
b.set_attr(
"history",
AttrValue::String("created by clawhdf5-netcdf4 test".into()),
);
// Dimension: lat (3 values)
b.create_dataset("lat")
.with_f64_data(&[-30.0, 0.0, 30.0])
.with_shape(&[3])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(0))
.set_attr("units", AttrValue::String("degrees_north".into()))
.set_attr("long_name", AttrValue::String("latitude".into()))
.set_attr("standard_name", AttrValue::String("latitude".into()))
.set_attr("axis", AttrValue::String("Y".into()));
// Dimension: lon (4 values)
b.create_dataset("lon")
.with_f64_data(&[0.0, 90.0, 180.0, 270.0])
.with_shape(&[4])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(1))
.set_attr("units", AttrValue::String("degrees_east".into()))
.set_attr("long_name", AttrValue::String("longitude".into()))
.set_attr("standard_name", AttrValue::String("longitude".into()))
.set_attr("axis", AttrValue::String("X".into()));
// Variable: temperature (3x4)
b.create_dataset("temperature")
.with_f64_data(&[
20.0, 22.0, 25.0, 23.0, 28.0, 30.0, 29.0, 27.0, 15.0, 18.0, 20.0, 17.0,
])
.with_shape(&[3, 4])
.set_attr("units", AttrValue::String("K".into()))
.set_attr("long_name", AttrValue::String("Surface Temperature".into()))
.set_attr(
"standard_name",
AttrValue::String("surface_temperature".into()),
)
.set_attr("_FillValue", AttrValue::F64(-9999.0));
b.finish().unwrap()
}
/// Create a NetCDF-4 file with scale_factor and add_offset for packed data.
fn make_packed_netcdf4() -> Vec<u8> {
let mut b = FileBuilder::new();
b.set_attr(
"_NCProperties",
AttrValue::String("version=2,clawhdf5=0.1.0".into()),
);
// Dimension: time (5 values)
b.create_dataset("time")
.with_f64_data(&[0.0, 1.0, 2.0, 3.0, 4.0])
.with_shape(&[5])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(0))
.set_attr("units", AttrValue::String("days since 2000-01-01".into()))
.set_attr("calendar", AttrValue::String("standard".into()))
.set_attr("axis", AttrValue::String("T".into()));
// Variable: packed_temp with scale_factor and add_offset
// Unpacked = packed * 0.01 + 273.15
b.create_dataset("packed_temp")
.with_f64_data(&[100.0, 200.0, -9999.0, 300.0, 150.0])
.with_shape(&[5])
.set_attr("scale_factor", AttrValue::F64(0.01))
.set_attr("add_offset", AttrValue::F64(273.15))
.set_attr("_FillValue", AttrValue::F64(-9999.0))
.set_attr("missing_value", AttrValue::F64(-9999.0))
.set_attr("units", AttrValue::String("K".into()))
.set_attr("long_name", AttrValue::String("Packed Temperature".into()));
b.finish().unwrap()
}
/// Create a NetCDF-4 file with groups.
fn make_grouped_netcdf4() -> Vec<u8> {
let mut b = FileBuilder::new();
b.set_attr(
"_NCProperties",
AttrValue::String("version=2,clawhdf5=0.1.0".into()),
);
b.set_attr("title", AttrValue::String("Grouped NetCDF-4".into()));
// Root-level dimension
b.create_dataset("time")
.with_f64_data(&[0.0, 1.0, 2.0])
.with_shape(&[3])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(0))
.set_attr("units", AttrValue::String("hours since 2020-01-01".into()));
// Group: surface
let mut g_surface = b.create_group("surface");
g_surface
.create_dataset("pressure")
.with_f64_data(&[1013.25, 1012.0, 1011.5])
.set_attr("units", AttrValue::String("hPa".into()))
.set_attr("long_name", AttrValue::String("Surface Pressure".into()));
g_surface.set_attr("description", AttrValue::String("Surface variables".into()));
let finished_surface = g_surface.finish();
b.add_group(finished_surface);
// Group: upper_air
let mut g_upper = b.create_group("upper_air");
g_upper
.create_dataset("wind_speed")
.with_f64_data(&[5.0, 7.5, 6.2])
.set_attr("units", AttrValue::String("m/s".into()))
.set_attr(
"long_name",
AttrValue::String("Wind Speed at 500hPa".into()),
);
let finished_upper = g_upper.finish();
b.add_group(finished_upper);
b.finish().unwrap()
}
/// Create a file with various data types.
fn make_multitype_netcdf4() -> Vec<u8> {
let mut b = FileBuilder::new();
// Dimension
b.create_dataset("x")
.with_f64_data(&[1.0, 2.0, 3.0])
.with_shape(&[3])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(0));
// Float64 variable
b.create_dataset("var_f64")
.with_f64_data(&[1.1, 2.2, 3.3])
.with_shape(&[3]);
// Float32 variable
b.create_dataset("var_f32")
.with_f32_data(&[1.5f32, 2.5, 3.5])
.with_shape(&[3]);
// Int32 variable
b.create_dataset("var_i32")
.with_i32_data(&[10, 20, 30])
.with_shape(&[3]);
// Int64 variable
b.create_dataset("var_i64")
.with_i64_data(&[100, 200, 300])
.with_shape(&[3]);
// UInt8 variable
b.create_dataset("var_u8")
.with_u8_data(&[1, 2, 3])
.with_shape(&[3]);
b.finish().unwrap()
}
/// Create a file with valid_range and additional CF attributes.
fn make_cf_rich_netcdf4() -> Vec<u8> {
let mut b = FileBuilder::new();
b.create_dataset("level")
.with_f64_data(&[1000.0, 850.0, 500.0, 200.0])
.with_shape(&[4])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(0))
.set_attr("units", AttrValue::String("hPa".into()))
.set_attr("long_name", AttrValue::String("Pressure Level".into()))
.set_attr("axis", AttrValue::String("Z".into()))
.set_attr("valid_range", AttrValue::F64Array(vec![0.0, 1100.0]));
b.finish().unwrap()
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[test]
fn test_open_netcdf4_from_bytes() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
assert!(format!("{file:?}").contains("NetCDF4File"));
}
#[test]
fn test_nc_properties() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let props = file.nc_properties().unwrap();
assert!(props.is_some());
assert!(props.unwrap().contains("clawhdf5"));
}
#[test]
fn test_global_attrs() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let attrs = file.global_attrs().unwrap();
assert!(matches!(
attrs.get("Conventions"),
Some(AttrValue::String(s)) if s == "CF-1.8"
));
assert!(matches!(
attrs.get("history"),
Some(AttrValue::String(s)) if s.contains("clawhdf5-netcdf4")
));
}
#[test]
fn test_read_dimensions() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let dims = file.dimensions().unwrap();
assert_eq!(dims.len(), 2);
let lat_dim = dims.iter().find(|d| d.name == "lat").unwrap();
assert_eq!(lat_dim.size, 3);
assert!(!lat_dim.is_unlimited);
let lon_dim = dims.iter().find(|d| d.name == "lon").unwrap();
assert_eq!(lon_dim.size, 4);
assert!(!lon_dim.is_unlimited);
}
#[test]
fn test_dimension_order_by_dimid() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let dims = file.dimensions().unwrap();
// Dimensions should be sorted by _Netcdf4Dimid: lat=0, lon=1
assert_eq!(dims[0].name, "lat");
assert_eq!(dims[1].name, "lon");
}
#[test]
fn test_read_variables() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let vars = file.variables().unwrap();
// Should have lat, lon, temperature
assert_eq!(vars.len(), 3);
let var_names: Vec<&str> = vars.iter().map(|v| v.name()).collect();
assert!(var_names.contains(&"lat"));
assert!(var_names.contains(&"lon"));
assert!(var_names.contains(&"temperature"));
}
#[test]
fn test_variable_dimensions() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut temp = file.variable("temperature").unwrap();
let dims = temp.dimensions();
assert_eq!(dims.len(), 2);
assert_eq!(dims[0].size, 3); // lat
assert_eq!(dims[1].size, 4); // lon
let shape = temp.shape().unwrap();
assert_eq!(shape, vec![3, 4]);
let _ = temp.attrs().unwrap();
}
#[test]
fn test_variable_cf_attributes() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut temp = file.variable("temperature").unwrap();
let cf = temp.cf_attributes().unwrap();
assert_eq!(cf.units.as_deref(), Some("K"));
assert_eq!(cf.long_name.as_deref(), Some("Surface Temperature"));
assert_eq!(cf.standard_name.as_deref(), Some("surface_temperature"));
assert_eq!(cf.fill_value, Some(FillValue::Float(-9999.0)));
assert!(cf.scale_factor.is_none());
assert!(cf.add_offset.is_none());
}
#[test]
fn test_coordinate_variable() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let lat = file.variable("lat").unwrap();
// lat is a coordinate variable: 1-D with name matching its dimension
assert!(lat.is_coordinate());
let temp = file.variable("temperature").unwrap();
assert!(!temp.is_coordinate());
}
#[test]
fn test_coordinate_cf_attrs() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut lat = file.variable("lat").unwrap();
let cf = lat.cf_attributes().unwrap();
assert_eq!(cf.units.as_deref(), Some("degrees_north"));
assert_eq!(cf.axis.as_deref(), Some("Y"));
assert_eq!(cf.standard_name.as_deref(), Some("latitude"));
}
#[test]
fn test_read_raw_f64() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let lat = file.variable("lat").unwrap();
let data = lat.read_raw_f64().unwrap();
assert_eq!(data, vec![-30.0, 0.0, 30.0]);
}
#[test]
fn test_read_temperature_data() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut temp = file.variable("temperature").unwrap();
let data = temp.read_f64().unwrap();
// No scale_factor/add_offset, so data should be unchanged
assert_eq!(data.len(), 12);
assert_eq!(data[0], 20.0);
assert_eq!(data[4], 28.0);
}
#[test]
fn test_scale_factor_add_offset() {
let bytes = make_packed_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut var = file.variable("packed_temp").unwrap();
let cf = var.cf_attributes().unwrap();
assert_eq!(cf.scale_factor, Some(0.01));
assert_eq!(cf.add_offset, Some(273.15));
let data = var.read_f64().unwrap();
assert_eq!(data.len(), 5);
// packed=100 -> 100*0.01 + 273.15 = 274.15
assert!((data[0] - 274.15).abs() < 1e-10);
// packed=200 -> 200*0.01 + 273.15 = 275.15
assert!((data[1] - 275.15).abs() < 1e-10);
// packed=-9999 (fill) -> NaN
assert!(data[2].is_nan());
// packed=300 -> 300*0.01 + 273.15 = 276.15
assert!((data[3] - 276.15).abs() < 1e-10);
// packed=150 -> 150*0.01 + 273.15 = 274.65
assert!((data[4] - 274.65).abs() < 1e-10);
}
#[test]
fn test_raw_read_bypasses_scaling() {
let bytes = make_packed_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("packed_temp").unwrap();
let raw = var.read_raw_f64().unwrap();
assert_eq!(raw, vec![100.0, 200.0, -9999.0, 300.0, 150.0]);
}
#[test]
fn test_groups() {
let bytes = make_grouped_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut group_names = file.group_names().unwrap();
group_names.sort();
assert_eq!(group_names, vec!["surface", "upper_air"]);
}
#[test]
fn test_group_attrs() {
let bytes = make_grouped_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let surface = file.group("surface").unwrap();
let attrs = surface.attrs().unwrap();
assert!(matches!(
attrs.get("description"),
Some(AttrValue::String(s)) if s == "Surface variables"
));
}
#[test]
fn test_group_variables() {
let bytes = make_grouped_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let surface = file.group("surface").unwrap();
let var_names = surface.variable_names().unwrap();
assert!(var_names.contains(&"pressure".to_string()));
let mut pressure = surface.variable("pressure").unwrap();
let data = pressure.read_f64().unwrap();
assert_eq!(data, vec![1013.25, 1012.0, 1011.5]);
let cf = pressure.cf_attributes().unwrap();
assert_eq!(cf.units.as_deref(), Some("hPa"));
assert_eq!(cf.long_name.as_deref(), Some("Surface Pressure"));
}
#[test]
fn test_group_nested_access() {
let bytes = make_grouped_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let upper = file.group("upper_air").unwrap();
let mut wind = upper.variable("wind_speed").unwrap();
let data = wind.read_f64().unwrap();
assert_eq!(data, vec![5.0, 7.5, 6.2]);
let cf = wind.cf_attributes().unwrap();
assert_eq!(cf.units.as_deref(), Some("m/s"));
}
#[test]
fn test_nc_type_f64() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_f64").unwrap();
assert_eq!(var.nc_type().unwrap(), NcType::Double);
}
#[test]
fn test_nc_type_f32() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_f32").unwrap();
assert_eq!(var.nc_type().unwrap(), NcType::Float);
}
#[test]
fn test_nc_type_i32() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_i32").unwrap();
assert_eq!(var.nc_type().unwrap(), NcType::Int);
}
#[test]
fn test_nc_type_i64() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_i64").unwrap();
assert_eq!(var.nc_type().unwrap(), NcType::Int64);
}
#[test]
fn test_nc_type_u8() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_u8").unwrap();
assert_eq!(var.nc_type().unwrap(), NcType::UByte);
}
#[test]
fn test_read_i32_data() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_i32").unwrap();
let data = var.read_raw_i32().unwrap();
assert_eq!(data, vec![10, 20, 30]);
}
#[test]
fn test_read_f32_data() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_f32").unwrap();
let data = var.read_raw_f32().unwrap();
assert_eq!(data, vec![1.5f32, 2.5, 3.5]);
}
#[test]
fn test_read_i64_data() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_i64").unwrap();
let data = var.read_raw_i64().unwrap();
assert_eq!(data, vec![100, 200, 300]);
}
#[test]
fn test_valid_range() {
let bytes = make_cf_rich_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut var = file.variable("level").unwrap();
let cf = var.cf_attributes().unwrap();
assert_eq!(cf.valid_range, Some((0.0, 1100.0)));
assert_eq!(cf.axis.as_deref(), Some("Z"));
}
#[test]
fn test_nc_type_display() {
assert_eq!(NcType::Double.to_string(), "NC_DOUBLE");
assert_eq!(NcType::Float.to_string(), "NC_FLOAT");
assert_eq!(NcType::Int.to_string(), "NC_INT");
assert_eq!(NcType::Byte.to_string(), "NC_BYTE");
assert_eq!(NcType::String.to_string(), "NC_STRING");
}
#[test]
fn test_variable_not_found_error() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let err = file.variable("nonexistent").unwrap_err();
assert!(err.to_string().contains("variable not found"));
}
#[test]
fn test_group_not_found_error() {
let bytes = make_grouped_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let err = file.group("nonexistent").unwrap_err();
assert!(err.to_string().contains("group not found"));
}
#[test]
fn test_error_display() {
use clawhdf5_netcdf4::Error;
let err = Error::DimensionNotFound("time".into());
assert_eq!(err.to_string(), "dimension not found: time");
let err = Error::TypeError("cannot convert".into());
assert_eq!(err.to_string(), "type error: cannot convert");
let err = Error::NotNetCDF4("missing signature".into());
assert!(err.to_string().contains("not a NetCDF-4 file"));
}
#[test]
fn test_fill_value_as_f64() {
assert_eq!(FillValue::Float(1.0).as_f64(), Some(1.0));
assert_eq!(FillValue::Int(-999).as_f64(), Some(-999.0));
assert_eq!(FillValue::UInt(42).as_f64(), Some(42.0));
assert_eq!(FillValue::String("na".into()).as_f64(), None);
}
#[test]
fn test_time_dimension_with_calendar() {
let bytes = make_packed_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut time_var = file.variable("time").unwrap();
let cf = time_var.cf_attributes().unwrap();
assert_eq!(cf.units.as_deref(), Some("days since 2000-01-01"));
assert_eq!(cf.calendar.as_deref(), Some("standard"));
assert_eq!(cf.axis.as_deref(), Some("T"));
}
#[test]
fn test_open_from_disk() {
let bytes = make_simple_netcdf4();
let path = std::env::temp_dir().join("clawhdf5_netcdf4_test.nc");
std::fs::write(&path, &bytes).unwrap();
let file = NetCDF4File::open(&path).unwrap();
let dims = file.dimensions().unwrap();
assert_eq!(dims.len(), 2);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_global_attrs_title() {
let bytes = make_grouped_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let attrs = file.global_attrs().unwrap();
assert!(matches!(
attrs.get("title"),
Some(AttrValue::String(s)) if s == "Grouped NetCDF-4"
));
}
#[test]
fn test_real_world_temperature_grid() {
// Simulate a real-world temperature dataset with lat/lon/time
let mut b = FileBuilder::new();
b.set_attr("Conventions", AttrValue::String("CF-1.8".into()));
b.set_attr(
"_NCProperties",
AttrValue::String("version=2,clawhdf5=0.1.0".into()),
);
b.set_attr(
"title",
AttrValue::String("ERA5 Temperature Reanalysis".into()),
);
b.set_attr("institution", AttrValue::String("ECMWF".into()));
// Time dimension (2 steps)
b.create_dataset("time")
.with_f64_data(&[0.0, 6.0])
.with_shape(&[2])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(0))
.set_attr("units", AttrValue::String("hours since 2020-01-01".into()))
.set_attr("calendar", AttrValue::String("proleptic_gregorian".into()))
.set_attr("axis", AttrValue::String("T".into()));
// Latitude dimension (3 values)
b.create_dataset("latitude")
.with_f64_data(&[90.0, 0.0, -90.0])
.with_shape(&[3])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(1))
.set_attr("units", AttrValue::String("degrees_north".into()))
.set_attr("axis", AttrValue::String("Y".into()));
// Longitude dimension (4 values)
b.create_dataset("longitude")
.with_f64_data(&[0.0, 90.0, 180.0, 270.0])
.with_shape(&[4])
.set_attr("CLASS", AttrValue::String("DIMENSION_SCALE".into()))
.set_attr("_Netcdf4Dimid", AttrValue::I64(2))
.set_attr("units", AttrValue::String("degrees_east".into()))
.set_attr("axis", AttrValue::String("X".into()));
// Temperature variable (2x3x4) with scale/offset
let temp_data: Vec<f64> = (0..24).map(|i| i as f64 * 100.0).collect();
b.create_dataset("t2m")
.with_f64_data(&temp_data)
.with_shape(&[2, 3, 4])
.set_attr("units", AttrValue::String("K".into()))
.set_attr("long_name", AttrValue::String("2 metre temperature".into()))
.set_attr("standard_name", AttrValue::String("air_temperature".into()))
.set_attr("scale_factor", AttrValue::F64(0.001))
.set_attr("add_offset", AttrValue::F64(273.15))
.set_attr("_FillValue", AttrValue::F64(-32767.0));
let bytes = b.finish().unwrap();
let file = NetCDF4File::from_bytes(bytes).unwrap();
// Check dimensions
let dims = file.dimensions().unwrap();
assert_eq!(dims.len(), 3);
assert_eq!(dims[0].name, "time");
assert_eq!(dims[0].size, 2);
assert_eq!(dims[1].name, "latitude");
assert_eq!(dims[1].size, 3);
assert_eq!(dims[2].name, "longitude");
assert_eq!(dims[2].size, 4);
// Check variables
let vars = file.variables().unwrap();
assert_eq!(vars.len(), 4); // time, latitude, longitude, t2m
// Check temperature with scaling
let mut t2m = file.variable("t2m").unwrap();
let cf = t2m.cf_attributes().unwrap();
assert_eq!(cf.scale_factor, Some(0.001));
assert_eq!(cf.add_offset, Some(273.15));
assert_eq!(cf.standard_name.as_deref(), Some("air_temperature"));
let data = t2m.read_f64().unwrap();
assert_eq!(data.len(), 24);
// First value: 0.0 * 0.001 + 273.15 = 273.15
assert!((data[0] - 273.15).abs() < 1e-10);
// Second value: 100.0 * 0.001 + 273.15 = 273.25
assert!((data[1] - 273.25).abs() < 1e-10);
// Check global attrs
let attrs = file.global_attrs().unwrap();
assert!(matches!(
attrs.get("institution"),
Some(AttrValue::String(s)) if s == "ECMWF"
));
}
#[test]
fn test_variable_read_raw_bytes() {
let bytes = make_multitype_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let var = file.variable("var_i32").unwrap();
let raw = var.read_raw().unwrap();
// 3 i32 values = 12 bytes
assert_eq!(raw.len(), 12);
// First i32 = 10
let val = i32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
assert_eq!(val, 10);
}
#[test]
fn test_hdf5_file_access() {
let bytes = make_simple_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let hdf5 = file.hdf5_file();
// Can use the underlying HDF5 file for advanced operations
assert!(hdf5.superblock().version >= 2);
}
#[test]
fn test_missing_value_attribute() {
let bytes = make_packed_netcdf4();
let file = NetCDF4File::from_bytes(bytes).unwrap();
let mut var = file.variable("packed_temp").unwrap();
let cf = var.cf_attributes().unwrap();
assert_eq!(cf.missing_value, Some(FillValue::Float(-9999.0)));
}
#[test]
fn test_dimension_struct_equality() {
let d1 = Dimension {
name: "time".into(),
size: 10,
is_unlimited: false,
};
let d2 = Dimension {
name: "time".into(),
size: 10,
is_unlimited: false,
};
assert_eq!(d1, d2);
let d3 = Dimension {
name: "time".into(),
size: 10,
is_unlimited: true,
};
assert_ne!(d1, d3);
}