1use std::path::{Path, PathBuf};
2
3use ad_core_rs::attributes::{NDAttrSource, NDAttrValue};
4use ad_core_rs::error::{ADError, ADResult};
5use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
6use ad_core_rs::ndarray_pool::NDArrayPool;
7use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
8use ad_core_rs::plugin::file_controller::FilePluginController;
9use ad_core_rs::plugin::runtime::{
10 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
11};
12
13use netcdf3::{DataSet, FileReader, FileWriter, Version};
14
15const VAR_NAME: &str = "array_data";
16const DIM_UNLIMITED: &str = "numArrays";
17const ND_NETCDF_FILE_VERSION: f64 = 3.1;
21
22struct DimMeta {
24 size: usize,
25 offset: usize,
26 binning: usize,
27 reverse: bool,
28}
29
30struct AttrData {
32 name: String,
33 description: String,
34 source: String,
36 source_type: String,
38 data_type_string: String,
40 value: NDAttrValue,
41}
42
43struct FrameData {
45 dims: Vec<usize>,
46 dim_meta: Vec<DimMeta>,
47 data: NDDataBuffer,
48 data_type: NDDataType,
49 attrs: Vec<AttrData>,
50 unique_id: i32,
51 time_stamp: f64,
52 epics_ts_sec: i32,
53 epics_ts_nsec: i32,
54}
55
56fn attr_source_type_string(src: &NDAttrSource) -> &'static str {
59 match src {
60 NDAttrSource::Driver => "NDAttrSourceDriver",
61 NDAttrSource::EpicsPV(_) => "NDAttrSourceEPICSPV",
62 NDAttrSource::Param { .. } => "NDAttrSourceParam",
63 NDAttrSource::Function(_) => "NDAttrSourceFunct",
64 NDAttrSource::Constant(_) => "NDAttrSourceConst",
65 NDAttrSource::Undefined => "Undefined",
66 }
67}
68
69fn attr_data_type_string(value: &NDAttrValue) -> &'static str {
71 match value {
72 NDAttrValue::Int8(_) => "Int8",
73 NDAttrValue::UInt8(_) => "UInt8",
74 NDAttrValue::Int16(_) => "Int16",
75 NDAttrValue::UInt16(_) => "UInt16",
76 NDAttrValue::Int32(_) => "Int32",
77 NDAttrValue::UInt32(_) => "UInt32",
78 NDAttrValue::Int64(_) => "Int64",
79 NDAttrValue::UInt64(_) => "UInt64",
80 NDAttrValue::Float32(_) => "Float32",
81 NDAttrValue::Float64(_) => "Float64",
82 NDAttrValue::String(_) => "String",
83 NDAttrValue::Undefined => "Undefined",
84 }
85}
86
87pub struct NetcdfWriter {
95 current_path: Option<PathBuf>,
96 frames: Vec<FrameData>,
97 open_multiple: bool,
104}
105
106impl NetcdfWriter {
107 pub fn new() -> Self {
108 Self {
109 current_path: None,
110 frames: Vec::new(),
111 open_multiple: false,
112 }
113 }
114}
115
116fn nc_data_type(dt: NDDataType) -> ADResult<netcdf3::DataType> {
128 match dt {
129 NDDataType::Int8 | NDDataType::UInt8 => Ok(netcdf3::DataType::I8),
130 NDDataType::Int16 | NDDataType::UInt16 => Ok(netcdf3::DataType::I16),
131 NDDataType::Int32 | NDDataType::UInt32 => Ok(netcdf3::DataType::I32),
132 NDDataType::Float32 => Ok(netcdf3::DataType::F32),
133 NDDataType::Float64 => Ok(netcdf3::DataType::F64),
134 NDDataType::Int64 | NDDataType::UInt64 => Ok(netcdf3::DataType::F64),
135 }
136}
137
138fn write_var_data(writer: &mut FileWriter, data: &NDDataBuffer) -> ADResult<()> {
140 let err = |e: netcdf3::error::WriteError| {
141 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
142 };
143 match data {
144 NDDataBuffer::I8(v) => writer.write_var_i8(VAR_NAME, v).map_err(err),
145 NDDataBuffer::U8(v) => {
149 let reinterp: Vec<i8> = v.iter().map(|&x| x as i8).collect();
150 writer.write_var_i8(VAR_NAME, &reinterp).map_err(err)
151 }
152 NDDataBuffer::I16(v) => writer.write_var_i16(VAR_NAME, v).map_err(err),
153 NDDataBuffer::U16(v) => {
154 let reinterp: Vec<i16> = v.iter().map(|&x| x as i16).collect();
155 writer.write_var_i16(VAR_NAME, &reinterp).map_err(err)
156 }
157 NDDataBuffer::I32(v) => writer.write_var_i32(VAR_NAME, v).map_err(err),
158 NDDataBuffer::U32(v) => {
159 let reinterp: Vec<i32> = v.iter().map(|&x| x as i32).collect();
160 writer.write_var_i32(VAR_NAME, &reinterp).map_err(err)
161 }
162 NDDataBuffer::F32(v) => writer.write_var_f32(VAR_NAME, v).map_err(err),
163 NDDataBuffer::F64(v) => writer.write_var_f64(VAR_NAME, v).map_err(err),
164 NDDataBuffer::I64(v) => {
165 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
166 writer.write_var_f64(VAR_NAME, &reinterp).map_err(err)
167 }
168 NDDataBuffer::U64(v) => {
169 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
170 writer.write_var_f64(VAR_NAME, &reinterp).map_err(err)
171 }
172 }
173}
174
175fn write_record_data(
177 writer: &mut FileWriter,
178 record_index: usize,
179 data: &NDDataBuffer,
180) -> ADResult<()> {
181 let err = |e: netcdf3::error::WriteError| {
182 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
183 };
184 match data {
185 NDDataBuffer::I8(v) => writer
186 .write_record_i8(VAR_NAME, record_index, v)
187 .map_err(err),
188 NDDataBuffer::U8(v) => {
190 let reinterp: Vec<i8> = v.iter().map(|&x| x as i8).collect();
191 writer
192 .write_record_i8(VAR_NAME, record_index, &reinterp)
193 .map_err(err)
194 }
195 NDDataBuffer::I16(v) => writer
196 .write_record_i16(VAR_NAME, record_index, v)
197 .map_err(err),
198 NDDataBuffer::U16(v) => {
199 let reinterp: Vec<i16> = v.iter().map(|&x| x as i16).collect();
200 writer
201 .write_record_i16(VAR_NAME, record_index, &reinterp)
202 .map_err(err)
203 }
204 NDDataBuffer::I32(v) => writer
205 .write_record_i32(VAR_NAME, record_index, v)
206 .map_err(err),
207 NDDataBuffer::U32(v) => {
208 let reinterp: Vec<i32> = v.iter().map(|&x| x as i32).collect();
209 writer
210 .write_record_i32(VAR_NAME, record_index, &reinterp)
211 .map_err(err)
212 }
213 NDDataBuffer::F32(v) => writer
214 .write_record_f32(VAR_NAME, record_index, v)
215 .map_err(err),
216 NDDataBuffer::F64(v) => writer
217 .write_record_f64(VAR_NAME, record_index, v)
218 .map_err(err),
219 NDDataBuffer::I64(v) => {
220 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
221 writer
222 .write_record_f64(VAR_NAME, record_index, &reinterp)
223 .map_err(err)
224 }
225 NDDataBuffer::U64(v) => {
226 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
227 writer
228 .write_record_f64(VAR_NAME, record_index, &reinterp)
229 .map_err(err)
230 }
231 }
232}
233
234const ATTR_STRING_DIM: &str = "attrStringSize";
235const ATTR_STRING_SIZE: usize = 256;
236
237fn attr_nc_type(value: &NDAttrValue) -> netcdf3::DataType {
245 match value {
246 NDAttrValue::Int8(_) | NDAttrValue::UInt8(_) | NDAttrValue::Undefined => {
247 netcdf3::DataType::I8
248 }
249 NDAttrValue::Int16(_) | NDAttrValue::UInt16(_) => netcdf3::DataType::I16,
250 NDAttrValue::Int32(_) | NDAttrValue::UInt32(_) => netcdf3::DataType::I32,
251 NDAttrValue::Float32(_) => netcdf3::DataType::F32,
252 NDAttrValue::Float64(_) | NDAttrValue::Int64(_) | NDAttrValue::UInt64(_) => {
253 netcdf3::DataType::F64
254 }
255 NDAttrValue::String(_) => netcdf3::DataType::U8,
256 }
257}
258
259fn write_attr_value(
262 writer: &mut FileWriter,
263 var_name: &str,
264 record_index: usize,
265 multi: bool,
266 value: &NDAttrValue,
267) -> ADResult<()> {
268 let werr = |e: netcdf3::error::WriteError| {
269 ADError::UnsupportedConversion(format!("NetCDF attr write error: {:?}", e))
270 };
271 if let NDAttrValue::String(s) = value {
276 let mut bytes: Vec<u8> = s.bytes().take(ATTR_STRING_SIZE).collect();
277 bytes.resize(ATTR_STRING_SIZE, 0);
278 return if multi {
279 writer
280 .write_record_u8(var_name, record_index, &bytes)
281 .map_err(werr)
282 } else {
283 writer.write_var_u8(var_name, &bytes).map_err(werr)
284 };
285 }
286 match attr_nc_type(value) {
287 netcdf3::DataType::I8 => {
288 let v = value.as_i64().unwrap_or(0) as i8;
289 if multi {
290 writer
291 .write_record_i8(var_name, record_index, &[v])
292 .map_err(werr)
293 } else {
294 writer.write_var_i8(var_name, &[v]).map_err(werr)
295 }
296 }
297 netcdf3::DataType::I16 => {
298 let v = value.as_i64().unwrap_or(0) as i16;
299 if multi {
300 writer
301 .write_record_i16(var_name, record_index, &[v])
302 .map_err(werr)
303 } else {
304 writer.write_var_i16(var_name, &[v]).map_err(werr)
305 }
306 }
307 netcdf3::DataType::I32 => {
308 let v = value.as_i64().unwrap_or(0) as i32;
309 if multi {
310 writer
311 .write_record_i32(var_name, record_index, &[v])
312 .map_err(werr)
313 } else {
314 writer.write_var_i32(var_name, &[v]).map_err(werr)
315 }
316 }
317 netcdf3::DataType::F32 => {
318 let v = value.as_f64().unwrap_or(0.0) as f32;
319 if multi {
320 writer
321 .write_record_f32(var_name, record_index, &[v])
322 .map_err(werr)
323 } else {
324 writer.write_var_f32(var_name, &[v]).map_err(werr)
325 }
326 }
327 netcdf3::DataType::F64 => {
328 let v = value.as_f64().unwrap_or(0.0);
329 if multi {
330 writer
331 .write_record_f64(var_name, record_index, &[v])
332 .map_err(werr)
333 } else {
334 writer.write_var_f64(var_name, &[v]).map_err(werr)
335 }
336 }
337 netcdf3::DataType::U8 => unreachable!("attr_nc_type returns U8 only for strings"),
339 }
340}
341
342fn define_data_set(
356 first: &FrameData,
357 num_frames: usize,
358 multi: bool,
359) -> ADResult<(DataSet, Vec<String>)> {
360 let map_def = |e: netcdf3::error::InvalidDataSet| {
361 ADError::UnsupportedConversion(format!("NetCDF definition error: {:?}", e))
362 };
363
364 let mut ds = DataSet::new();
365 let ndims = first.dims.len();
366
367 ds.add_global_attr_i32("dataType", vec![first.data_type as i32])
372 .map_err(map_def)?;
373 ds.add_global_attr_f64("NDNetCDFFileVersion", vec![ND_NETCDF_FILE_VERSION])
374 .map_err(map_def)?;
375 ds.add_global_attr_i32("numArrayDims", vec![ndims as i32])
376 .map_err(map_def)?;
377 let dim_size: Vec<i32> = first.dim_meta.iter().map(|d| d.size as i32).collect();
380 ds.add_global_attr_i32("dimSize", dim_size)
381 .map_err(map_def)?;
382 let dim_offset: Vec<i32> = first.dim_meta.iter().map(|d| d.offset as i32).collect();
383 ds.add_global_attr_i32("dimOffset", dim_offset)
384 .map_err(map_def)?;
385 let dim_binning: Vec<i32> = first.dim_meta.iter().map(|d| d.binning as i32).collect();
386 ds.add_global_attr_i32("dimBinning", dim_binning)
387 .map_err(map_def)?;
388 let dim_reverse: Vec<i32> = first
389 .dim_meta
390 .iter()
391 .map(|d| if d.reverse { 1 } else { 0 })
392 .collect();
393 ds.add_global_attr_i32("dimReverse", dim_reverse)
394 .map_err(map_def)?;
395
396 if multi {
400 ds.set_unlimited_dim(DIM_UNLIMITED, num_frames)
401 .map_err(map_def)?;
402 } else {
403 ds.add_fixed_dim(DIM_UNLIMITED, 1).map_err(map_def)?;
404 }
405 let mut dim_names: Vec<String> = Vec::new();
408 for i in 0..ndims {
409 let name = format!("dim{}", i);
410 ds.add_fixed_dim(&name, first.dims[ndims - 1 - i])
411 .map_err(map_def)?;
412 dim_names.push(name);
413 }
414 ds.add_fixed_dim(ATTR_STRING_DIM, ATTR_STRING_SIZE)
417 .map_err(map_def)?;
418
419 ds.add_var("uniqueId", &[DIM_UNLIMITED], netcdf3::DataType::I32)
422 .map_err(map_def)?;
423 ds.add_var("timeStamp", &[DIM_UNLIMITED], netcdf3::DataType::F64)
424 .map_err(map_def)?;
425 ds.add_var("epicsTSSec", &[DIM_UNLIMITED], netcdf3::DataType::I32)
426 .map_err(map_def)?;
427 ds.add_var("epicsTSNsec", &[DIM_UNLIMITED], netcdf3::DataType::I32)
428 .map_err(map_def)?;
429
430 let mut var_dims: Vec<&str> = vec![DIM_UNLIMITED];
433 var_dims.extend(dim_names.iter().map(|s| s.as_str()));
434 ds.add_var(VAR_NAME, &var_dims, nc_data_type(first.data_type)?)
435 .map_err(map_def)?;
436
437 let mut attr_var_names: Vec<String> = Vec::new();
443 for attr in &first.attrs {
444 ds.add_global_attr_string(
445 &format!("Attr_{}_DataType", attr.name),
446 &attr.data_type_string,
447 )
448 .map_err(map_def)?;
449 ds.add_global_attr_string(
450 &format!("Attr_{}_Description", attr.name),
451 &attr.description,
452 )
453 .map_err(map_def)?;
454 ds.add_global_attr_string(&format!("Attr_{}_Source", attr.name), &attr.source)
455 .map_err(map_def)?;
456 ds.add_global_attr_string(&format!("Attr_{}_SourceType", attr.name), &attr.source_type)
457 .map_err(map_def)?;
458
459 let var_name = format!("Attr_{}", attr.name);
460 if matches!(attr.value, NDAttrValue::String(_)) {
463 ds.add_var(
464 &var_name,
465 &[DIM_UNLIMITED, ATTR_STRING_DIM],
466 attr_nc_type(&attr.value),
467 )
468 .map_err(map_def)?;
469 } else {
470 ds.add_var(&var_name, &[DIM_UNLIMITED], attr_nc_type(&attr.value))
471 .map_err(map_def)?;
472 }
473 attr_var_names.push(var_name);
474 }
475
476 Ok((ds, attr_var_names))
477}
478
479impl NDFileWriter for NetcdfWriter {
480 fn open_file(&mut self, path: &Path, mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
481 self.current_path = Some(path.to_path_buf());
482 self.frames.clear();
483 self.open_multiple = mode != NDFileMode::Single;
488 Ok(())
489 }
490
491 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
492 nc_data_type(array.data.data_type())?;
494
495 let dims: Vec<usize> = array.dims.iter().map(|d| d.size).collect();
496 let dim_meta: Vec<DimMeta> = array
497 .dims
498 .iter()
499 .map(|d| DimMeta {
500 size: d.size,
501 offset: d.offset,
502 binning: d.binning,
503 reverse: d.reverse,
504 })
505 .collect();
506 let attrs: Vec<AttrData> = array
507 .attributes
508 .iter()
509 .map(|a| AttrData {
510 name: a.name.clone(),
511 description: a.description.clone(),
512 source: a.source.source_string().to_string(),
515 source_type: attr_source_type_string(&a.source).to_string(),
516 data_type_string: attr_data_type_string(&a.value).to_string(),
517 value: a.value.clone(),
518 })
519 .collect();
520
521 self.frames.push(FrameData {
522 dims,
523 dim_meta,
524 data: array.data.clone(),
525 data_type: array.data.data_type(),
526 attrs,
527 unique_id: array.unique_id,
528 time_stamp: array.time_stamp,
529 epics_ts_sec: array.timestamp.sec as i32,
530 epics_ts_nsec: array.timestamp.nsec as i32,
531 });
532 Ok(())
533 }
534
535 fn close_file(&mut self) -> ADResult<()> {
536 let path = match self.current_path.take() {
537 Some(p) => p,
538 None => return Ok(()),
539 };
540
541 if self.frames.is_empty() {
542 return Ok(());
543 }
544
545 let map_write = |e: netcdf3::error::WriteError| {
546 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
547 };
548
549 let first = &self.frames[0];
550 let multi = self.open_multiple;
553 let (ds, attr_var_names) = define_data_set(first, self.frames.len(), multi)?;
554
555 let mut writer = FileWriter::open(&path).map_err(map_write)?;
557 writer
558 .set_def(&ds, Version::Classic, 0)
559 .map_err(map_write)?;
560
561 if multi {
562 for (i, frame) in self.frames.iter().enumerate() {
563 write_record_data(&mut writer, i, &frame.data)?;
564 writer
565 .write_record_i32("uniqueId", i, &[frame.unique_id])
566 .map_err(map_write)?;
567 writer
568 .write_record_f64("timeStamp", i, &[frame.time_stamp])
569 .map_err(map_write)?;
570 writer
571 .write_record_i32("epicsTSSec", i, &[frame.epics_ts_sec])
572 .map_err(map_write)?;
573 writer
574 .write_record_i32("epicsTSNsec", i, &[frame.epics_ts_nsec])
575 .map_err(map_write)?;
576 for (attr, var_name) in first.attrs.iter().zip(&attr_var_names) {
579 let value = frame
580 .attrs
581 .iter()
582 .find(|a| a.name == attr.name)
583 .map(|a| &a.value)
584 .unwrap_or(&attr.value);
585 write_attr_value(&mut writer, var_name, i, true, value)?;
586 }
587 }
588 } else {
589 write_var_data(&mut writer, &self.frames[0].data)?;
590 writer
591 .write_var_i32("uniqueId", &[first.unique_id])
592 .map_err(map_write)?;
593 writer
594 .write_var_f64("timeStamp", &[first.time_stamp])
595 .map_err(map_write)?;
596 writer
597 .write_var_i32("epicsTSSec", &[first.epics_ts_sec])
598 .map_err(map_write)?;
599 writer
600 .write_var_i32("epicsTSNsec", &[first.epics_ts_nsec])
601 .map_err(map_write)?;
602 for (attr, var_name) in first.attrs.iter().zip(&attr_var_names) {
603 write_attr_value(&mut writer, var_name, 0, false, &attr.value)?;
604 }
605 }
606
607 writer.close().map_err(map_write)?;
608 self.frames.clear();
609 Ok(())
610 }
611
612 fn read_file(&mut self) -> ADResult<NDArray> {
613 let path = self
614 .current_path
615 .as_ref()
616 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
617
618 let map_read = |e: netcdf3::error::ReadError| {
619 ADError::UnsupportedConversion(format!("NetCDF read error: {:?}", e))
620 };
621
622 let mut reader = FileReader::open(path).map_err(map_read)?;
623
624 let (is_record, dims, original_type_ordinal) = {
626 let ds = reader.data_set();
627 let var = ds.get_var(VAR_NAME).ok_or_else(|| {
628 ADError::UnsupportedConversion(format!(
629 "variable '{}' not found in NetCDF file",
630 VAR_NAME
631 ))
632 })?;
633
634 let is_record = ds.is_record_var(VAR_NAME).unwrap_or(false);
635
636 let var_dims_rc = var.get_dims();
637 let mut dims: Vec<NDDimension> = Vec::new();
638 for d in &var_dims_rc {
639 if d.is_unlimited() || d.name() == DIM_UNLIMITED {
643 continue;
644 }
645 dims.push(NDDimension::new(d.size()));
646 }
647
648 let original_type_ordinal = ds
649 .get_global_attr_i32("dataType")
650 .and_then(|slice| slice.first().copied());
651
652 (is_record, dims, original_type_ordinal)
653 };
654
655 let data_vec = if is_record {
657 reader.read_record(VAR_NAME, 0).map_err(map_read)?
658 } else {
659 reader.read_var(VAR_NAME).map_err(map_read)?
660 };
661
662 let (nd_type, buf) = match data_vec {
663 netcdf3::DataVector::I8(v) => (NDDataType::Int8, NDDataBuffer::I8(v)),
664 netcdf3::DataVector::U8(v) => (NDDataType::UInt8, NDDataBuffer::U8(v)),
665 netcdf3::DataVector::I16(v) => (NDDataType::Int16, NDDataBuffer::I16(v)),
666 netcdf3::DataVector::I32(v) => (NDDataType::Int32, NDDataBuffer::I32(v)),
667 netcdf3::DataVector::F32(v) => (NDDataType::Float32, NDDataBuffer::F32(v)),
668 netcdf3::DataVector::F64(v) => (NDDataType::Float64, NDDataBuffer::F64(v)),
669 };
670
671 let actual_type = original_type_ordinal
673 .and_then(|v| NDDataType::from_ordinal(v as u8))
674 .unwrap_or(nd_type);
675
676 let buf = match (actual_type, buf) {
680 (NDDataType::UInt8, NDDataBuffer::I8(v)) => {
681 NDDataBuffer::U8(v.into_iter().map(|x| x as u8).collect())
682 }
683 (NDDataType::UInt16, NDDataBuffer::I16(v)) => {
684 NDDataBuffer::U16(v.into_iter().map(|x| x as u16).collect())
685 }
686 (NDDataType::UInt32, NDDataBuffer::I32(v)) => {
687 NDDataBuffer::U32(v.into_iter().map(|x| x as u32).collect())
688 }
689 (_, buf) => buf,
690 };
691
692 let mut arr = NDArray::new(dims, actual_type);
693 arr.data = buf;
694 Ok(arr)
695 }
696
697 fn supports_multiple_arrays(&self) -> bool {
698 true
699 }
700}
701
702pub struct NetcdfFileProcessor {
704 ctrl: FilePluginController<NetcdfWriter>,
705}
706
707impl NetcdfFileProcessor {
708 pub fn new() -> Self {
709 Self {
710 ctrl: FilePluginController::new(NetcdfWriter::new()),
711 }
712 }
713}
714
715impl Default for NetcdfFileProcessor {
716 fn default() -> Self {
717 Self::new()
718 }
719}
720
721impl NDPluginProcess for NetcdfFileProcessor {
722 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
723 self.ctrl.process_array(array)
724 }
725
726 fn plugin_type(&self) -> &str {
727 "NDFileNetCDF"
728 }
729
730 fn does_array_callbacks(&self) -> bool {
733 false
734 }
735
736 fn register_params(
737 &mut self,
738 base: &mut asyn_rs::port::PortDriverBase,
739 ) -> asyn_rs::error::AsynResult<()> {
740 self.ctrl.register_params(base)
741 }
742
743 fn on_param_change(
744 &mut self,
745 reason: usize,
746 params: &PluginParamSnapshot,
747 ) -> ParamChangeResult {
748 self.ctrl.on_param_change(reason, params)
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
756 use std::sync::atomic::{AtomicU32, Ordering};
757
758 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
759
760 fn temp_path(prefix: &str) -> PathBuf {
761 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
762 std::env::temp_dir().join(format!("adcore_test_{}_{}.nc", prefix, n))
763 }
764
765 #[test]
766 fn test_write_u8_mono() {
767 let path = temp_path("nc_u8");
768 let mut writer = NetcdfWriter::new();
769
770 let mut arr = NDArray::new(
771 vec![NDDimension::new(4), NDDimension::new(4)],
772 NDDataType::UInt8,
773 );
774 if let NDDataBuffer::U8(v) = &mut arr.data {
775 for i in 0..16 {
776 v[i] = i as u8;
777 }
778 }
779
780 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
781 writer.write_file(&arr).unwrap();
782 writer.close_file().unwrap();
783
784 let data = std::fs::read(&path).unwrap();
786 assert!(data.len() > 16);
787 assert_eq!(&data[0..3], b"CDF", "Expected NetCDF magic bytes");
788
789 std::fs::remove_file(&path).ok();
790 }
791
792 #[test]
793 fn test_write_u16() {
794 let path = temp_path("nc_u16");
795 let mut writer = NetcdfWriter::new();
796
797 let mut arr = NDArray::new(
798 vec![NDDimension::new(4), NDDimension::new(4)],
799 NDDataType::UInt16,
800 );
801 if let NDDataBuffer::U16(v) = &mut arr.data {
802 for i in 0..16 {
803 v[i] = (i * 1000) as u16;
804 }
805 }
806
807 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
808 writer.write_file(&arr).unwrap();
809 writer.close_file().unwrap();
810
811 let data = std::fs::read(&path).unwrap();
812 assert!(data.len() > 32);
813 assert_eq!(&data[0..3], b"CDF");
814
815 std::fs::remove_file(&path).ok();
816 }
817
818 #[test]
819 fn test_roundtrip_u8() {
820 let path = temp_path("nc_rt_u8");
821 let mut writer = NetcdfWriter::new();
822
823 let mut arr = NDArray::new(
824 vec![NDDimension::new(4), NDDimension::new(4)],
825 NDDataType::UInt8,
826 );
827 if let NDDataBuffer::U8(v) = &mut arr.data {
828 for i in 0..16 {
829 v[i] = (i * 10) as u8;
830 }
831 }
832
833 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
834 writer.write_file(&arr).unwrap();
835 writer.close_file().unwrap();
836
837 writer.current_path = Some(path.clone());
838 let read_back = writer.read_file().unwrap();
839 if let (NDDataBuffer::U8(orig), NDDataBuffer::U8(read)) = (&arr.data, &read_back.data) {
840 assert_eq!(orig, read);
841 } else {
842 panic!("data type mismatch on roundtrip");
843 }
844
845 std::fs::remove_file(&path).ok();
846 }
847
848 #[test]
849 fn test_roundtrip_i16() {
850 let path = temp_path("nc_rt_i16");
851 let mut writer = NetcdfWriter::new();
852
853 let mut arr = NDArray::new(
854 vec![NDDimension::new(4), NDDimension::new(4)],
855 NDDataType::Int16,
856 );
857 if let NDDataBuffer::I16(v) = &mut arr.data {
858 for i in 0..16 {
859 v[i] = (i as i16) * 100 - 500;
860 }
861 }
862
863 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
864 writer.write_file(&arr).unwrap();
865 writer.close_file().unwrap();
866
867 writer.current_path = Some(path.clone());
868 let read_back = writer.read_file().unwrap();
869 if let (NDDataBuffer::I16(orig), NDDataBuffer::I16(read)) = (&arr.data, &read_back.data) {
870 assert_eq!(orig, read);
871 } else {
872 panic!("data type mismatch on roundtrip");
873 }
874
875 std::fs::remove_file(&path).ok();
876 }
877
878 #[test]
879 fn test_roundtrip_f32() {
880 let path = temp_path("nc_rt_f32");
881 let mut writer = NetcdfWriter::new();
882
883 let mut arr = NDArray::new(
884 vec![NDDimension::new(4), NDDimension::new(4)],
885 NDDataType::Float32,
886 );
887 if let NDDataBuffer::F32(v) = &mut arr.data {
888 for i in 0..16 {
889 v[i] = i as f32 * 0.5;
890 }
891 }
892
893 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
894 writer.write_file(&arr).unwrap();
895 writer.close_file().unwrap();
896
897 writer.current_path = Some(path.clone());
898 let read_back = writer.read_file().unwrap();
899 if let (NDDataBuffer::F32(orig), NDDataBuffer::F32(read)) = (&arr.data, &read_back.data) {
900 assert_eq!(orig, read);
901 } else {
902 panic!("data type mismatch on roundtrip");
903 }
904
905 std::fs::remove_file(&path).ok();
906 }
907
908 #[test]
909 fn test_multiple_frames() {
910 let path = temp_path("nc_multi");
911 let mut writer = NetcdfWriter::new();
912
913 let mut arr1 = NDArray::new(
914 vec![NDDimension::new(4), NDDimension::new(4)],
915 NDDataType::UInt8,
916 );
917 if let NDDataBuffer::U8(v) = &mut arr1.data {
918 for i in 0..16 {
919 v[i] = i as u8;
920 }
921 }
922
923 let mut arr2 = NDArray::new(
924 vec![NDDimension::new(4), NDDimension::new(4)],
925 NDDataType::UInt8,
926 );
927 if let NDDataBuffer::U8(v) = &mut arr2.data {
928 for i in 0..16 {
929 v[i] = (i as u8).wrapping_add(100);
930 }
931 }
932
933 let mut arr3 = NDArray::new(
934 vec![NDDimension::new(4), NDDimension::new(4)],
935 NDDataType::UInt8,
936 );
937 if let NDDataBuffer::U8(v) = &mut arr3.data {
938 for i in 0..16 {
939 v[i] = (i as u8).wrapping_add(200);
940 }
941 }
942
943 writer.open_file(&path, NDFileMode::Stream, &arr1).unwrap();
944 writer.write_file(&arr1).unwrap();
945 writer.write_file(&arr2).unwrap();
946 writer.write_file(&arr3).unwrap();
947 writer.close_file().unwrap();
948
949 writer.current_path = Some(path.clone());
951 let read_back = writer.read_file().unwrap();
952 if let NDDataBuffer::U8(v) = &read_back.data {
953 assert_eq!(v.len(), 16);
954 for i in 0..16 {
955 assert_eq!(v[i], i as u8, "mismatch at index {}", i);
956 }
957 } else {
958 panic!("expected U8 data");
959 }
960
961 std::fs::remove_file(&path).ok();
962 }
963
964 #[test]
965 fn test_num_arrays_dim_keyed_on_open_mode_not_frame_count() {
966 let frame = || NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
974 let num_arrays_is_unlimited = |path: &PathBuf| -> bool {
975 let reader = FileReader::open(path).unwrap();
976 reader
977 .data_set()
978 .get_dim(DIM_UNLIMITED)
979 .expect("numArrays dimension")
980 .is_unlimited()
981 };
982
983 let path = temp_path("nc_mode_capture_one");
985 let mut writer = NetcdfWriter::new();
986 writer
987 .open_file(&path, NDFileMode::Capture, &frame())
988 .unwrap();
989 writer.write_file(&frame()).unwrap();
990 writer.close_file().unwrap();
991 assert!(
992 num_arrays_is_unlimited(&path),
993 "Capture with 1 frame must still be NC_UNLIMITED"
994 );
995 std::fs::remove_file(&path).ok();
996
997 let path = temp_path("nc_mode_stream_one");
999 let mut writer = NetcdfWriter::new();
1000 writer
1001 .open_file(&path, NDFileMode::Stream, &frame())
1002 .unwrap();
1003 writer.write_file(&frame()).unwrap();
1004 writer.close_file().unwrap();
1005 assert!(
1006 num_arrays_is_unlimited(&path),
1007 "Stream with 1 frame must still be NC_UNLIMITED"
1008 );
1009 std::fs::remove_file(&path).ok();
1010
1011 let path = temp_path("nc_mode_single_one");
1013 let mut writer = NetcdfWriter::new();
1014 writer
1015 .open_file(&path, NDFileMode::Single, &frame())
1016 .unwrap();
1017 writer.write_file(&frame()).unwrap();
1018 writer.close_file().unwrap();
1019 assert!(
1020 !num_arrays_is_unlimited(&path),
1021 "Single must be a fixed numArrays dimension"
1022 );
1023 std::fs::remove_file(&path).ok();
1024 }
1025
1026 #[test]
1027 fn test_attributes_stored_as_per_frame_variables() {
1028 let path = temp_path("nc_attrs");
1029 let mut writer = NetcdfWriter::new();
1030
1031 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1032 arr.attributes.add(NDAttribute::new_static(
1033 "exposure",
1034 "Exposure time",
1035 NDAttrSource::Driver,
1036 NDAttrValue::Float64(0.5),
1037 ));
1038 arr.attributes.add(NDAttribute::new_static(
1039 "gain",
1040 "Detector gain",
1041 NDAttrSource::Driver,
1042 NDAttrValue::Int32(42),
1043 ));
1044
1045 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1046 writer.write_file(&arr).unwrap();
1047 writer.close_file().unwrap();
1048
1049 let mut reader = FileReader::open(&path).unwrap();
1050 {
1051 let ds = reader.data_set();
1052 assert!(ds.get_var("Attr_exposure").is_some());
1054 assert!(ds.get_var("Attr_gain").is_some());
1055 assert_eq!(
1057 ds.get_global_attr_as_string("Attr_exposure_DataType"),
1058 Some("Float64".to_string())
1059 );
1060 assert_eq!(
1061 ds.get_global_attr_as_string("Attr_gain_DataType"),
1062 Some("Int32".to_string())
1063 );
1064 assert_eq!(
1065 ds.get_global_attr_as_string("Attr_exposure_Description"),
1066 Some("Exposure time".to_string())
1067 );
1068 assert_eq!(
1069 ds.get_global_attr_as_string("Attr_gain_SourceType"),
1070 Some("NDAttrSourceDriver".to_string())
1071 );
1072 }
1073 if let netcdf3::DataVector::F64(v) = reader.read_var("Attr_exposure").unwrap() {
1075 assert_eq!(v, vec![0.5]);
1076 } else {
1077 panic!("Attr_exposure should be F64");
1078 }
1079 if let netcdf3::DataVector::I32(v) = reader.read_var("Attr_gain").unwrap() {
1080 assert_eq!(v, vec![42]);
1081 } else {
1082 panic!("Attr_gain should be I32");
1083 }
1084
1085 drop(reader);
1086 std::fs::remove_file(&path).ok();
1087 }
1088
1089 #[test]
1090 fn test_single_frame_array_data_has_leading_numarrays_dim() {
1091 let path = temp_path("nc_rank");
1092 let mut writer = NetcdfWriter::new();
1093
1094 let arr = NDArray::new(
1095 vec![NDDimension::new(4), NDDimension::new(3)],
1096 NDDataType::UInt8,
1097 );
1098 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1099 writer.write_file(&arr).unwrap();
1100 writer.close_file().unwrap();
1101
1102 let reader = FileReader::open(&path).unwrap();
1103 let ds = reader.data_set();
1104 let var = ds.get_var("array_data").unwrap();
1105 assert_eq!(var.get_dims().len(), 3);
1108 assert_eq!(var.get_dims()[0].name(), "numArrays");
1109 assert_eq!(var.get_dims()[0].size(), 1);
1110
1111 drop(reader);
1112 std::fs::remove_file(&path).ok();
1113 }
1114
1115 #[test]
1116 fn test_global_attrs_match_c_set() {
1117 let path = temp_path("nc_globals");
1122 let mut writer = NetcdfWriter::new();
1123
1124 let arr = NDArray::new(
1125 vec![NDDimension::new(4), NDDimension::new(3)],
1126 NDDataType::UInt8,
1127 );
1128 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1129 writer.write_file(&arr).unwrap();
1130 writer.close_file().unwrap();
1131
1132 let reader = FileReader::open(&path).unwrap();
1133 let ds = reader.data_set();
1134
1135 assert_eq!(
1136 ds.get_global_attr_f64("NDNetCDFFileVersion"),
1137 Some([3.1f64].as_slice()),
1138 "NDNetCDFFileVersion global must be the 3.1 double"
1139 );
1140 assert!(ds.has_global_attr("dataType"));
1141 assert!(
1142 !ds.has_global_attr("uniqueId"),
1143 "uniqueId is a variable in C, not a global attribute"
1144 );
1145 assert!(
1146 !ds.has_global_attr("numArrays"),
1147 "numArrays is a dimension in C, not a global attribute"
1148 );
1149 assert!(ds.get_var("uniqueId").is_some());
1151
1152 drop(reader);
1153 std::fs::remove_file(&path).ok();
1154 }
1155
1156 #[test]
1157 fn test_all_four_metadata_variables_written_single_frame() {
1158 let path = temp_path("nc_meta");
1159 let mut writer = NetcdfWriter::new();
1160
1161 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1162 arr.unique_id = 99;
1163 arr.time_stamp = 12.5;
1164 arr.timestamp.sec = 555;
1165 arr.timestamp.nsec = 777;
1166
1167 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1168 writer.write_file(&arr).unwrap();
1169 writer.close_file().unwrap();
1170
1171 let mut reader = FileReader::open(&path).unwrap();
1172 for name in ["uniqueId", "timeStamp", "epicsTSSec", "epicsTSNsec"] {
1173 assert!(
1174 reader.data_set().get_var(name).is_some(),
1175 "{name} variable missing"
1176 );
1177 }
1178 match reader.read_var("uniqueId").unwrap() {
1179 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![99]),
1180 other => panic!("uniqueId wrong type: {other:?}"),
1181 }
1182 match reader.read_var("epicsTSSec").unwrap() {
1183 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![555]),
1184 other => panic!("epicsTSSec wrong type: {other:?}"),
1185 }
1186 match reader.read_var("epicsTSNsec").unwrap() {
1187 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![777]),
1188 other => panic!("epicsTSNsec wrong type: {other:?}"),
1189 }
1190
1191 drop(reader);
1192 std::fs::remove_file(&path).ok();
1193 }
1194
1195 #[test]
1196 fn test_nddatatype_ordinals_match_c() {
1197 assert_eq!(NDDataType::Int8 as i32, 0);
1201 assert_eq!(NDDataType::UInt8 as i32, 1);
1202 assert_eq!(NDDataType::Int16 as i32, 2);
1203 assert_eq!(NDDataType::UInt16 as i32, 3);
1204 assert_eq!(NDDataType::Int32 as i32, 4);
1205 assert_eq!(NDDataType::UInt32 as i32, 5);
1206 assert_eq!(NDDataType::Int64 as i32, 6);
1207 assert_eq!(NDDataType::UInt64 as i32, 7);
1208 assert_eq!(NDDataType::Float32 as i32, 8);
1209 assert_eq!(NDDataType::Float64 as i32, 9);
1210 }
1211
1212 const NC_BYTE: u32 = 1;
1221 const NC_CHAR: u32 = 2;
1222 const NC_SHORT: u32 = 3;
1223 const NC_INT: u32 = 4;
1224 const NC_DOUBLE: u32 = 6;
1225
1226 struct RawVar {
1227 name: String,
1228 nc_type: u32,
1229 dim_ids: Vec<u32>,
1230 begin: usize,
1231 }
1232
1233 struct RawHeader {
1234 dims: Vec<(String, u32)>,
1235 gatt_names: Vec<String>,
1236 vars: Vec<RawVar>,
1237 }
1238
1239 impl RawHeader {
1240 fn var(&self, name: &str) -> &RawVar {
1241 self.vars
1242 .iter()
1243 .find(|v| v.name == name)
1244 .unwrap_or_else(|| panic!("no variable {} in file", name))
1245 }
1246 fn var_names(&self) -> Vec<&str> {
1247 self.vars.iter().map(|v| v.name.as_str()).collect()
1248 }
1249 fn dim_names(&self) -> Vec<&str> {
1250 self.dims.iter().map(|(n, _)| n.as_str()).collect()
1251 }
1252 }
1253
1254 struct Cursor<'a> {
1257 b: &'a [u8],
1258 p: usize,
1259 }
1260
1261 impl<'a> Cursor<'a> {
1262 fn u32(&mut self) -> u32 {
1263 let v = u32::from_be_bytes(self.b[self.p..self.p + 4].try_into().unwrap());
1264 self.p += 4;
1265 v
1266 }
1267 fn name(&mut self) -> String {
1269 let n = self.u32() as usize;
1270 let s = String::from_utf8(self.b[self.p..self.p + n].to_vec()).unwrap();
1271 self.p += n.div_ceil(4) * 4;
1272 s
1273 }
1274 fn skip_att_list(&mut self) {
1275 let tag = self.u32();
1276 let n = self.u32() as usize;
1277 assert!(
1278 tag == 0x0C || (tag == 0 && n == 0),
1279 "bad att_list tag {tag}"
1280 );
1281 for _ in 0..n {
1282 let _name = self.name();
1283 let nc_type = self.u32();
1284 let nelems = self.u32() as usize;
1285 let size = match nc_type {
1286 1 | 2 => 1,
1287 3 => 2,
1288 4 | 5 => 4,
1289 6 => 8,
1290 t => panic!("bad nc_type {t}"),
1291 };
1292 self.p += (nelems * size).div_ceil(4) * 4;
1293 }
1294 }
1295 }
1296
1297 fn parse_header(bytes: &[u8]) -> RawHeader {
1298 assert_eq!(&bytes[0..4], b"CDF\x01", "not a CDF-1 file");
1299 let mut c = Cursor { b: bytes, p: 4 };
1300 let _numrecs = c.u32();
1301
1302 let tag = c.u32();
1304 let ndims = c.u32() as usize;
1305 assert!(tag == 0x0A || (tag == 0 && ndims == 0));
1306 let mut dims = Vec::new();
1307 for _ in 0..ndims {
1308 let name = c.name();
1309 let len = c.u32();
1310 dims.push((name, len));
1311 }
1312
1313 let tag = c.u32();
1315 let natts = c.u32() as usize;
1316 assert!(tag == 0x0C || (tag == 0 && natts == 0));
1317 let mut gatt_names = Vec::new();
1318 for _ in 0..natts {
1319 let name = c.name();
1320 let nc_type = c.u32();
1321 let nelems = c.u32() as usize;
1322 let size = match nc_type {
1323 1 | 2 => 1,
1324 3 => 2,
1325 4 | 5 => 4,
1326 6 => 8,
1327 t => panic!("bad nc_type {t}"),
1328 };
1329 c.p += (nelems * size).div_ceil(4) * 4;
1330 gatt_names.push(name);
1331 }
1332
1333 let tag = c.u32();
1335 let nvars = c.u32() as usize;
1336 assert!(tag == 0x0B || (tag == 0 && nvars == 0));
1337 let mut vars = Vec::new();
1338 for _ in 0..nvars {
1339 let name = c.name();
1340 let rank = c.u32() as usize;
1341 let dim_ids: Vec<u32> = (0..rank).map(|_| c.u32()).collect();
1342 c.skip_att_list();
1343 let nc_type = c.u32();
1344 let _vsize = c.u32();
1345 let begin = c.u32() as usize;
1346 vars.push(RawVar {
1347 name,
1348 nc_type,
1349 dim_ids,
1350 begin,
1351 });
1352 }
1353 RawHeader {
1354 dims,
1355 gatt_names,
1356 vars,
1357 }
1358 }
1359
1360 fn write_one(path: &PathBuf, arr: &NDArray) -> Vec<u8> {
1361 let mut writer = NetcdfWriter::new();
1362 writer.open_file(path, NDFileMode::Single, arr).unwrap();
1363 writer.write_file(arr).unwrap();
1364 writer.close_file().unwrap();
1365 let bytes = std::fs::read(path).unwrap();
1366 std::fs::remove_file(path).ok();
1367 bytes
1368 }
1369
1370 #[test]
1373 fn test_r8_68_uint8_array_data_is_nc_byte() {
1374 let path = temp_path("nc_r8_68_byte");
1375 let mut arr = NDArray::new(
1376 vec![NDDimension::new(2), NDDimension::new(2)],
1377 NDDataType::UInt8,
1378 );
1379 if let NDDataBuffer::U8(v) = &mut arr.data {
1380 v.copy_from_slice(&[0, 1, 200, 255]);
1381 }
1382 let bytes = write_one(&path, &arr);
1383 let hdr = parse_header(&bytes);
1384
1385 let var = hdr.var("array_data");
1386 assert_eq!(
1387 var.nc_type, NC_BYTE,
1388 "array_data must be NC_BYTE, not NC_CHAR"
1389 );
1390
1391 assert_eq!(&bytes[var.begin..var.begin + 4], &[0x00, 0x01, 0xC8, 0xFF]);
1394 }
1395
1396 #[test]
1399 fn test_r8_68_int8_array_data_is_nc_byte() {
1400 let path = temp_path("nc_r8_68_i8");
1401 let arr = NDArray::new(
1402 vec![NDDimension::new(2), NDDimension::new(2)],
1403 NDDataType::Int8,
1404 );
1405 let hdr = parse_header(&write_one(&path, &arr));
1406 assert_eq!(hdr.var("array_data").nc_type, NC_BYTE);
1407 }
1408
1409 #[test]
1413 fn test_r8_68_attr_variable_nc_types_match_c() {
1414 let path = temp_path("nc_r8_68_attrs");
1415 let mut arr = NDArray::new(
1416 vec![NDDimension::new(2), NDDimension::new(2)],
1417 NDDataType::UInt16,
1418 );
1419 arr.attributes.add(NDAttribute::new_static(
1420 "Str",
1421 "a string",
1422 NDAttrSource::Driver,
1423 NDAttrValue::String("hello".into()),
1424 ));
1425 arr.attributes.add(NDAttribute::new_static(
1426 "I8",
1427 "a byte",
1428 NDAttrSource::Driver,
1429 NDAttrValue::Int8(-3),
1430 ));
1431 arr.attributes.add(NDAttribute::new_static(
1432 "I16",
1433 "a short",
1434 NDAttrSource::Driver,
1435 NDAttrValue::Int16(-3),
1436 ));
1437 arr.attributes.add(NDAttribute::new_static(
1438 "I32",
1439 "an int",
1440 NDAttrSource::Driver,
1441 NDAttrValue::Int32(-3),
1442 ));
1443 arr.attributes.add(NDAttribute::new_static(
1444 "I64",
1445 "a long",
1446 NDAttrSource::Driver,
1447 NDAttrValue::Int64(-3),
1448 ));
1449 let bytes = write_one(&path, &arr);
1450 let hdr = parse_header(&bytes);
1451
1452 assert_eq!(
1453 hdr.var("Attr_Str").nc_type,
1454 NC_CHAR,
1455 "string attr must be NC_CHAR"
1456 );
1457 assert_eq!(hdr.var("Attr_I8").nc_type, NC_BYTE);
1458 assert_eq!(hdr.var("Attr_I16").nc_type, NC_SHORT);
1459 assert_eq!(hdr.var("Attr_I32").nc_type, NC_INT);
1460 assert_eq!(hdr.var("Attr_I64").nc_type, NC_DOUBLE);
1462
1463 let str_var = hdr.var("Attr_Str");
1465 assert_eq!(str_var.dim_ids.len(), 2);
1466 let attr_dim = str_var.dim_ids[1] as usize;
1467 assert_eq!(hdr.dims[attr_dim], ("attrStringSize".to_string(), 256));
1468 assert_eq!(&bytes[str_var.begin..str_var.begin + 6], b"hello\0");
1470 }
1471
1472 #[test]
1476 fn test_r8_68_attr_string_dim_defined_without_string_attrs() {
1477 let path = temp_path("nc_r8_68_nodim");
1478 let arr = NDArray::new(
1479 vec![NDDimension::new(2), NDDimension::new(2)],
1480 NDDataType::UInt16,
1481 );
1482 let hdr = parse_header(&write_one(&path, &arr));
1483 assert_eq!(
1484 hdr.dim_names(),
1485 vec!["numArrays", "dim0", "dim1", "attrStringSize"]
1486 );
1487 assert_eq!(hdr.dims[3].1, 256);
1488 }
1489
1490 #[test]
1496 fn test_r8_68_definition_order_matches_c() {
1497 let path = temp_path("nc_r8_68_order");
1498 let mut arr = NDArray::new(
1499 vec![NDDimension::new(4), NDDimension::new(2)],
1500 NDDataType::UInt16,
1501 );
1502 arr.attributes.add(NDAttribute::new_static(
1503 "Gain",
1504 "detector gain",
1505 NDAttrSource::Driver,
1506 NDAttrValue::Float64(2.5),
1507 ));
1508 let hdr = parse_header(&write_one(&path, &arr));
1509
1510 assert_eq!(
1511 hdr.dim_names(),
1512 vec!["numArrays", "dim0", "dim1", "attrStringSize"]
1513 );
1514 assert_eq!(hdr.dims[1].1, 2);
1516 assert_eq!(hdr.dims[2].1, 4);
1517
1518 assert_eq!(
1519 hdr.var_names(),
1520 vec![
1521 "uniqueId",
1522 "timeStamp",
1523 "epicsTSSec",
1524 "epicsTSNsec",
1525 "array_data",
1526 "Attr_Gain",
1527 ]
1528 );
1529
1530 assert_eq!(
1531 hdr.gatt_names,
1532 vec![
1533 "dataType",
1534 "NDNetCDFFileVersion",
1535 "numArrayDims",
1536 "dimSize",
1537 "dimOffset",
1538 "dimBinning",
1539 "dimReverse",
1540 "Attr_Gain_DataType",
1541 "Attr_Gain_Description",
1542 "Attr_Gain_Source",
1543 "Attr_Gain_SourceType",
1544 ]
1545 );
1546 }
1547}