netcdf-reader 0.2.0

Pure-Rust NetCDF-3 classic and NetCDF-4 (HDF5-backed) file reader
Documentation
//! Integration tests for netcdf-reader against generated fixtures.
//!
//! These tests require the fixture files to be generated by running:
//!   cd testdata && python3 generate_fixtures.py
//!
//! Tests are skipped at runtime if fixture files are missing.

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 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();
}

// ---- NetCDF-3 tests ----

#[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().len(), 2);
    assert_eq!(file.dimensions()[0].name, "x");
    assert_eq!(file.dimensions()[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);
}

// ---- NetCDF-4 tests ----

#[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();
    assert!(dims.len() >= 2);

    let vars = file.variables();
    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,
            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();

    // NETCDF4_CLASSIC should be detected as Nc4Classic
    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();

    // Both dimensions are size 10 — DIMENSION_LIST should resolve them correctly
    let var = file.variable("temperature").unwrap();
    assert_eq!(var.dimensions().len(), 2);

    // With DIMENSION_LIST parsing, dimensions should be named "lat" and "lon"
    // (not both matched to the first size-10 dim)
    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_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();

    // Use the unified read_variable method
    let data: ndarray::ArrayD<f64> = file.read_variable("data").unwrap();
    assert_eq!(data.shape(), &[5, 10]);
    // First element should be 0.0
    assert!((data[[0, 0]] - 0.0).abs() < 1e-10);
    // Last element should be 49.0
    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();

    // Use the unified read_variable method on classic format
    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);
}