use std::path::Path;
fn fixture_path(subdir: &str, name: &str) -> Option<std::path::PathBuf> {
let base = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("testdata")
.join(subdir);
let path = base.join(name);
if path.exists() {
Some(path)
} else {
None
}
}
macro_rules! skip_if_missing {
($subdir:expr, $name:expr) => {
match fixture_path($subdir, $name) {
Some(p) => p,
None => {
eprintln!("SKIPPED: fixture {}/{} not found", $subdir, $name);
return;
}
}
};
}
#[cfg(feature = "netcdf4")]
fn assert_close(actual: f64, expected: f64, tolerance: f64) {
assert!(
(actual - expected).abs() <= tolerance,
"got {actual}, expected {expected}"
);
}
#[cfg(feature = "netcdf4")]
fn create_sparse_huge_nc4_fixture(path: &Path) {
const DIM: usize = 1 << 20;
let mut file = netcdf::create_with(path, netcdf::Options::NETCDF4).unwrap();
file.add_dimension("row", DIM).unwrap();
file.add_dimension("col", DIM).unwrap();
{
let mut variable = file.add_variable::<f32>("sparse", &["row", "col"]).unwrap();
variable.set_chunking(&[1024, 1024]).unwrap();
variable.set_fill_value(42.5_f32).unwrap();
}
file.enddef().unwrap();
}
#[cfg(feature = "netcdf4")]
fn create_nc4_coordinate_variable_fixture(path: &Path) {
let mut file = netcdf::create_with(path, netcdf::Options::NETCDF4).unwrap();
file.add_dimension("lat", 3).unwrap();
file.add_variable::<f64>("lat", &["lat"]).unwrap();
file.add_variable::<f32>("temp", &["lat"]).unwrap();
file.enddef().unwrap();
{
let mut lat = file.variable_mut("lat").unwrap();
lat.put_values(&[-90.0_f64, 0.0, 90.0], (..,)).unwrap();
}
{
let mut temp = file.variable_mut("temp").unwrap();
temp.put_values(&[271.0_f32, 272.0, 273.0], (..,)).unwrap();
}
}
#[cfg(all(feature = "netcdf4", feature = "cf"))]
fn create_nc4_cf_coordinate_variable_fixture(path: &Path) {
let mut file = netcdf::create_with(path, netcdf::Options::NETCDF4).unwrap();
file.add_dimension("time", 2).unwrap();
file.add_dimension("lat", 3).unwrap();
{
let mut time = file.add_variable::<f64>("time", &["time"]).unwrap();
time.put_attribute("units", "hours since 2000-01-01 00:00:00")
.unwrap();
time.put_attribute("calendar", "standard").unwrap();
}
{
let mut lat = file.add_variable::<f64>("lat", &["lat"]).unwrap();
lat.put_attribute("units", "degrees_north").unwrap();
}
{
let mut temp = file
.add_variable::<f32>("temperature", &["time", "lat"])
.unwrap();
temp.put_attribute("units", "K").unwrap();
}
file.enddef().unwrap();
{
let mut time = file.variable_mut("time").unwrap();
time.put_values(&[0.0_f64, 24.0], (..,)).unwrap();
}
{
let mut lat = file.variable_mut("lat").unwrap();
lat.put_values(&[-45.0_f64, 0.0, 45.0], (..,)).unwrap();
}
{
let mut temp = file.variable_mut("temperature").unwrap();
temp.put_values(&[270.0_f32, 271.0, 272.0, 273.0, 274.0, 275.0], (.., ..))
.unwrap();
}
}
#[cfg(feature = "netcdf4")]
fn create_nc4_dimension_only_fixture(path: &Path) {
let mut file = netcdf::create_with(path, netcdf::Options::NETCDF4).unwrap();
file.add_dimension("x", 3).unwrap();
file.add_variable::<f32>("temp", &["x"]).unwrap();
file.enddef().unwrap();
let mut temp = file.variable_mut("temp").unwrap();
temp.put_values(&[1.0_f32, 2.0, 3.0], (..,)).unwrap();
}
#[test]
fn test_cdf1_simple() {
let path = skip_if_missing!("netcdf3", "cdf1_simple.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
assert_eq!(file.format(), netcdf_reader::NcFormat::Classic);
assert_eq!(file.dimensions().unwrap().len(), 2);
assert_eq!(file.dimensions().unwrap()[0].name, "x");
assert_eq!(file.dimensions().unwrap()[0].size, 5);
let var = file.variable("temp").unwrap();
assert_eq!(var.shape(), vec![5, 10]);
let classic = file.as_classic().unwrap();
let data: ndarray::ArrayD<f32> = classic.read_variable("temp").unwrap();
assert_eq!(data.shape(), &[5, 10]);
assert!((data[[0, 0]] - 0.0).abs() < 1e-6);
}
#[test]
fn test_cdf5_new_types() {
let path = skip_if_missing!("netcdf3", "cdf5_new_types.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
assert_eq!(file.format(), netcdf_reader::NcFormat::Cdf5);
let classic = file.as_classic().unwrap();
let ubyte_data: ndarray::ArrayD<u8> = classic.read_variable("ubyte_var").unwrap();
assert_eq!(ubyte_data.as_slice().unwrap(), &[1, 2, 3, 4]);
let int64_data: ndarray::ArrayD<i64> = classic.read_variable("int64_var").unwrap();
assert_eq!(int64_data.as_slice().unwrap(), &[1, 2, 3, 4]);
}
#[test]
fn test_record_vars() {
let path = skip_if_missing!("netcdf3", "record_vars.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let var = file.variable("series").unwrap();
assert!(var.dimensions()[0].is_unlimited);
assert_eq!(var.shape(), vec![3, 5]);
let classic = file.as_classic().unwrap();
let data: ndarray::ArrayD<f64> = classic.read_variable("series").unwrap();
assert_eq!(data[[0, 0]], 1.0);
assert_eq!(data[[2, 4]], 15.0);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_basic() {
let path = skip_if_missing!("netcdf4", "nc4_basic.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
assert!(matches!(
file.format(),
netcdf_reader::NcFormat::Nc4 | netcdf_reader::NcFormat::Nc4Classic
));
let dims = file.dimensions().unwrap();
assert!(dims.len() >= 2);
let vars = file.variables().unwrap();
assert!(!vars.is_empty());
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_from_bytes_with_options() {
let path = skip_if_missing!("netcdf4", "nc4_basic.nc");
let bytes = std::fs::read(&path).unwrap();
let file = netcdf_reader::NcFile::from_bytes_with_options(
&bytes,
netcdf_reader::NcOpenOptions {
chunk_cache_bytes: 1024,
chunk_cache_slots: 17,
metadata_mode: netcdf_reader::NcMetadataMode::Strict,
filter_registry: None,
},
)
.unwrap();
assert!(matches!(
file.format(),
netcdf_reader::NcFormat::Nc4 | netcdf_reader::NcFormat::Nc4Classic
));
let data: ndarray::ArrayD<f64> = file.read_variable("data").unwrap();
assert_eq!(data.shape(), &[5, 10]);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_from_storage_with_options() {
let path = skip_if_missing!("netcdf4", "nc4_basic.nc");
let bytes = std::fs::read(&path).unwrap();
let file = netcdf_reader::NcFile::from_storage_with_options(
std::sync::Arc::new(netcdf_reader::BytesStorage::new(bytes)),
netcdf_reader::NcOpenOptions {
chunk_cache_bytes: 1024,
chunk_cache_slots: 17,
metadata_mode: netcdf_reader::NcMetadataMode::Strict,
filter_registry: None,
},
)
.unwrap();
assert!(matches!(
file.format(),
netcdf_reader::NcFormat::Nc4 | netcdf_reader::NcFormat::Nc4Classic
));
let data: ndarray::ArrayD<f64> = file.read_variable("data").unwrap();
assert_eq!(data.shape(), &[5, 10]);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_compressed() {
let path = skip_if_missing!("netcdf4", "nc4_compressed.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
assert!(matches!(
file.format(),
netcdf_reader::NcFormat::Nc4 | netcdf_reader::NcFormat::Nc4Classic
));
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_groups_nested_lookup_and_read() {
let path = skip_if_missing!("netcdf4", "nc4_groups.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let obs = file.group("obs").unwrap();
assert_eq!(obs.name, "obs");
let temp_data: ndarray::ArrayD<f32> = file.read_variable("/obs/temperature").unwrap();
assert_eq!(temp_data.shape(), &[3]);
assert_eq!(obs.dimension("time").unwrap().size, 3);
let temperature = file.variable("obs/temperature").unwrap();
let dim_names: Vec<&str> = temperature
.dimensions()
.iter()
.map(|d| d.name.as_str())
.collect();
assert_eq!(dim_names, vec!["time"]);
assert!((temp_data[[1]] - 21.0).abs() < 1e-6);
assert!(file.variable("temperature").is_err());
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_classic_model() {
let path = skip_if_missing!("netcdf4", "nc4_classic_model.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
assert_eq!(file.format(), netcdf_reader::NcFormat::Nc4Classic);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_same_size_dims() {
let path = skip_if_missing!("netcdf4", "same_size_dims.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let var = file.variable("temperature").unwrap();
assert_eq!(var.dimensions().len(), 2);
let dim_names: Vec<&str> = var.dimensions().iter().map(|d| d.name.as_str()).collect();
assert_eq!(dim_names, vec!["lat", "lon"]);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_coordinate_dimension_scale_is_variable_metadata() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("coordinate_variable.nc");
create_nc4_coordinate_variable_fixture(&path);
let file = netcdf_reader::NcFile::open(&path).unwrap();
let lat = file.variable("lat").unwrap();
assert_eq!(lat.shape(), vec![3]);
assert!(matches!(lat.dtype(), netcdf_reader::NcType::Double));
let dim_names: Vec<&str> = lat.dimensions().iter().map(|d| d.name.as_str()).collect();
assert_eq!(dim_names, vec!["lat"]);
let values: ndarray::ArrayD<f64> = file.read_variable("lat").unwrap();
assert_eq!(values.as_slice().unwrap(), &[-90.0, 0.0, 90.0]);
let names: Vec<&str> = file.variables().unwrap().iter().map(|v| v.name()).collect();
assert!(names.contains(&"lat"));
assert!(names.contains(&"temp"));
}
#[cfg(all(feature = "netcdf4", feature = "cf"))]
#[test]
fn test_nc4_coordinate_variables_drive_cf_axis_and_time_discovery() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("cf_coordinate_variable.nc");
create_nc4_cf_coordinate_variable_fixture(&path);
let file = netcdf_reader::NcFile::open(&path).unwrap();
assert!(file.variable("time").unwrap().is_coordinate_variable());
assert_eq!(file.coordinate_variable("lat").unwrap().name(), "lat");
let mut axes: Vec<(&str, netcdf_reader::cf::CfAxisType)> = file
.cf_coordinate_axes("")
.unwrap()
.iter()
.map(|axis| (axis.variable.name(), axis.axis_type))
.collect();
axes.sort_by(|left, right| left.0.cmp(right.0));
assert_eq!(
axes,
vec![
("lat", netcdf_reader::cf::CfAxisType::Y),
("time", netcdf_reader::cf::CfAxisType::T)
]
);
let variable_axes: Vec<netcdf_reader::cf::CfAxisType> = file
.cf_variable_axes("temperature")
.unwrap()
.iter()
.map(|axis| axis.axis_type)
.collect();
assert_eq!(
variable_axes,
vec![
netcdf_reader::cf::CfAxisType::T,
netcdf_reader::cf::CfAxisType::Y
]
);
let time = file
.cf_variable_time_coordinate("temperature")
.unwrap()
.unwrap();
assert_eq!(time.variable.name(), "time");
assert_eq!(time.time_ref.unit, netcdf_reader::cf::CfTimeUnit::Hours);
let raw = file.read_variable_as_f64("time").unwrap();
let values: Vec<f64> = raw.iter().copied().collect();
let decoded = netcdf_reader::cf::decode_time_coordinate_values(time.variable, &values)
.unwrap()
.unwrap();
assert_eq!(decoded[0].format("%Y-%m-%d").to_string(), "2000-01-01");
assert_eq!(decoded[1].format("%Y-%m-%d").to_string(), "2000-01-02");
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_climate_4d_coordinate_variables_and_slice() {
let path = skip_if_missing!("netcdf4", "nc4_climate_4d.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
assert_eq!(
file.global_attribute("Conventions")
.unwrap()
.value
.as_string()
.unwrap(),
"CF-1.8"
);
assert_eq!(file.dimension("time").unwrap().size, 4);
assert_eq!(file.dimension("level").unwrap().size, 3);
assert_eq!(file.dimension("lat").unwrap().size, 6);
assert_eq!(file.dimension("lon").unwrap().size, 12);
for name in ["time", "level", "lat", "lon"] {
let variable = file.variable(name).unwrap();
assert_eq!(variable.ndim(), 1, "{name} should be a coordinate variable");
assert_eq!(variable.dimensions()[0].name, name);
}
let lat: ndarray::ArrayD<f64> = file.read_variable("lat").unwrap();
let lon: ndarray::ArrayD<f64> = file.read_variable("lon").unwrap();
assert_close(lat[[0]], -75.0, 1e-12);
assert_close(lat[[5]], 75.0, 1e-12);
assert_close(lon[[0]], 0.0, 1e-12);
assert_close(lon[[11]], 330.0, 1e-12);
let temp = file.variable("temperature").unwrap();
let dim_names: Vec<&str> = temp.dimensions().iter().map(|d| d.name.as_str()).collect();
assert_eq!(dim_names, vec!["time", "level", "lat", "lon"]);
let selection = netcdf_reader::NcSliceInfo {
selections: vec![
netcdf_reader::NcSliceInfoElem::Index(1),
netcdf_reader::NcSliceInfoElem::Index(2),
netcdf_reader::NcSliceInfoElem::Slice {
start: 2,
end: 5,
step: 1,
},
netcdf_reader::NcSliceInfoElem::Slice {
start: 3,
end: 8,
step: 1,
},
],
};
let sliced: ndarray::ArrayD<f32> = file.read_variable_slice("temperature", &selection).unwrap();
assert_eq!(sliced.shape(), &[3, 5]);
assert!(sliced
.iter()
.all(|value| value.is_finite() && *value > 270.0 && *value < 310.0));
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_packed_cf_unpacks_and_masks_fill_values() {
let path = skip_if_missing!("netcdf4", "nc4_packed_cf.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let temp = file.variable("temperature").unwrap();
assert_eq!(temp.shape(), vec![10, 10]);
assert!(matches!(temp.dtype(), netcdf_reader::NcType::Short));
assert_close(
temp.attribute("scale_factor")
.unwrap()
.value
.as_f64()
.unwrap(),
0.01,
1e-12,
);
assert_close(
temp.attribute("add_offset")
.unwrap()
.value
.as_f64()
.unwrap(),
273.15,
1e-12,
);
let raw: ndarray::ArrayD<i16> = file.read_variable("temperature").unwrap();
assert_eq!(raw[[0, 1]], 1);
assert_eq!(raw[[5, 5]], -9999);
let unpacked_masked = file.read_variable_unpacked_masked("temperature").unwrap();
assert_eq!(unpacked_masked.shape(), &[10, 10]);
assert!(unpacked_masked[[0, 0]].is_nan());
assert!(unpacked_masked[[5, 5]].is_nan());
assert!(unpacked_masked[[9, 9]].is_nan());
assert_close(unpacked_masked[[0, 1]], 273.16, 1e-10);
assert_close(unpacked_masked[[9, 8]], 274.13, 1e-10);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_shared_dims_coordinate_variables_and_shared_shapes() {
let path = skip_if_missing!("netcdf4", "nc4_shared_dims.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let lat: ndarray::ArrayD<f64> = file.read_variable("lat").unwrap();
let lon: ndarray::ArrayD<f64> = file.read_variable("lon").unwrap();
assert_eq!(lat.shape(), &[8]);
assert_eq!(lon.shape(), &[16]);
assert_close(lat[[0]], -87.5, 1e-12);
assert_close(lat[[7]], 87.5, 1e-12);
assert_close(lon[[15]], 337.5, 1e-12);
for name in ["temperature", "humidity", "pressure"] {
let variable = file.variable(name).unwrap();
let dim_names: Vec<&str> = variable
.dimensions()
.iter()
.map(|d| d.name.as_str())
.collect();
assert_eq!(dim_names, vec!["lat", "lon"]);
let selection = netcdf_reader::NcSliceInfo {
selections: vec![
netcdf_reader::NcSliceInfoElem::Slice {
start: 1,
end: 5,
step: 2,
},
netcdf_reader::NcSliceInfoElem::Slice {
start: 2,
end: 10,
step: 3,
},
],
};
let data: ndarray::ArrayD<f32> = file.read_variable_slice(name, &selection).unwrap();
assert_eq!(data.shape(), &[2, 3]);
assert!(data.iter().all(|value| value.is_finite()));
}
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_zero_record_unlimited_dimension_reads_empty_array() {
let path = skip_if_missing!("netcdf4", "nc4_zero_record.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let time = file.dimension("time").unwrap();
assert!(time.is_unlimited);
assert_eq!(time.size, 0);
let series = file.variable("series").unwrap();
assert_eq!(series.shape(), vec![0, 5]);
let values: ndarray::ArrayD<f32> = file.read_variable("series").unwrap();
assert_eq!(values.shape(), &[0, 5]);
assert_eq!(values.len(), 0);
let selection = netcdf_reader::NcSliceInfo {
selections: vec![
netcdf_reader::NcSliceInfoElem::Slice {
start: 0,
end: 0,
step: 1,
},
netcdf_reader::NcSliceInfoElem::Slice {
start: 0,
end: 5,
step: 1,
},
],
};
let sliced: ndarray::ArrayD<f32> = file.read_variable_slice("series", &selection).unwrap();
assert_eq!(sliced.shape(), &[0, 5]);
assert_eq!(sliced.len(), 0);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_dimension_only_scale_is_not_variable_metadata() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("dimension_only.nc");
create_nc4_dimension_only_fixture(&path);
let file = netcdf_reader::NcFile::open(&path).unwrap();
assert_eq!(file.dimension("x").unwrap().size, 3);
assert!(file.variable("x").is_err());
let temp = file.variable("temp").unwrap();
let dim_names: Vec<&str> = temp.dimensions().iter().map(|d| d.name.as_str()).collect();
assert_eq!(dim_names, vec!["x"]);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_string_variable_reads() {
let path = skip_if_missing!("netcdf4", "nc4_string_var.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let strings = file.read_variable_as_strings("names").unwrap();
assert_eq!(strings, vec!["alpha", "beta", "gamma", "delta"]);
let err = file.read_variable_as_string("names").unwrap_err();
assert!(matches!(err, netcdf_reader::Error::InvalidData(_)));
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_read_variable_unified() {
let path = skip_if_missing!("netcdf4", "nc4_basic.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let data: ndarray::ArrayD<f64> = file.read_variable("data").unwrap();
assert_eq!(data.shape(), &[5, 10]);
assert!((data[[0, 0]] - 0.0).abs() < 1e-10);
assert!((data[[4, 9]] - 49.0).abs() < 1e-10);
}
#[cfg(feature = "netcdf4")]
#[test]
fn test_nc4_sparse_huge_logical_slice_reads_fill_value() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("sparse_huge.nc");
create_sparse_huge_nc4_fixture(&path);
let expected_fill = netcdf::open(&path)
.unwrap()
.variable("sparse")
.unwrap()
.fill_value::<f32>()
.unwrap()
.unwrap_or(0.0);
let file = netcdf_reader::NcFile::open(&path).unwrap();
let selection = netcdf_reader::NcSliceInfo {
selections: vec![
netcdf_reader::NcSliceInfoElem::Index((1 << 20) - 1),
netcdf_reader::NcSliceInfoElem::Index((1 << 20) - 1),
],
};
let sliced: ndarray::ArrayD<f32> = file.read_variable_slice("sparse", &selection).unwrap();
assert_eq!(sliced.shape(), &[]);
assert_eq!(sliced[[]], expected_fill);
let hdf5 = hdf5_reader::Hdf5File::open(&path).unwrap();
let dataset = hdf5.dataset("/sparse").unwrap();
let hdf5_selection = hdf5_reader::SliceInfo {
selections: vec![
hdf5_reader::SliceInfoElem::Slice {
start: ((1 << 20) - 1) as u64,
end: 1 << 20,
step: 1,
},
hdf5_reader::SliceInfoElem::Slice {
start: ((1 << 20) - 1) as u64,
end: 1 << 20,
step: 1,
},
],
};
let hdf5_sliced: ndarray::ArrayD<f32> = dataset.read_slice(&hdf5_selection).unwrap();
assert_eq!(hdf5_sliced.shape(), &[1, 1]);
assert_eq!(hdf5_sliced[[0, 0]], expected_fill);
}
#[test]
fn test_classic_read_variable_unified() {
let path = skip_if_missing!("netcdf3", "cdf1_simple.nc");
let file = netcdf_reader::NcFile::open(&path).unwrap();
let data: ndarray::ArrayD<f32> = file.read_variable("temp").unwrap();
assert_eq!(data.shape(), &[5, 10]);
assert!((data[[0, 0]] - 0.0).abs() < 1e-6);
}