1use std::path::{Path, PathBuf};
2
3use ad_core_rs::attributes::{NDAttrSource, NDAttrValue};
4use ad_core_rs::error::{ADError, ADResult};
5use ad_core_rs::finalize::Finalize;
6use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
7use ad_core_rs::ndarray_pool::NDArrayPool;
8use ad_core_rs::plugin::file_base::{FileAttributes, NDFileMode, NDFileWriter};
9use ad_core_rs::plugin::file_controller::FilePluginController;
10use ad_core_rs::plugin::runtime::{
11 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
12};
13
14use netcdf3::{DataSet, FileReader, FileWriter, Version};
15use parking_lot::Mutex;
16
17const VAR_NAME: &str = "array_data";
18const DIM_UNLIMITED: &str = "numArrays";
19const ND_NETCDF_FILE_VERSION: f64 = 3.1;
23
24struct DimMeta {
26 size: usize,
27 offset: usize,
28 binning: usize,
29 reverse: bool,
30}
31
32struct AttrData {
34 name: String,
35 description: String,
36 source: String,
38 source_type: String,
40 data_type_string: String,
42 value: NDAttrValue,
43}
44
45struct FrameData {
47 dims: Vec<usize>,
48 dim_meta: Vec<DimMeta>,
49 data: NDDataBuffer,
50 data_type: NDDataType,
51 attrs: Vec<AttrData>,
52 unique_id: i32,
53 time_stamp: f64,
54 epics_ts_sec: i32,
55 epics_ts_nsec: i32,
56}
57
58fn attr_source_type_string(src: &NDAttrSource) -> &'static str {
61 match src {
62 NDAttrSource::Driver => "NDAttrSourceDriver",
63 NDAttrSource::EpicsPV(_) => "NDAttrSourceEPICSPV",
64 NDAttrSource::Param { .. } => "NDAttrSourceParam",
65 NDAttrSource::Function(_) => "NDAttrSourceFunct",
66 NDAttrSource::Constant(_) => "NDAttrSourceConst",
67 NDAttrSource::Undefined => "Undefined",
68 }
69}
70
71fn attr_data_type_string(value: &NDAttrValue) -> &'static str {
73 match value {
74 NDAttrValue::Int8(_) => "Int8",
75 NDAttrValue::UInt8(_) => "UInt8",
76 NDAttrValue::Int16(_) => "Int16",
77 NDAttrValue::UInt16(_) => "UInt16",
78 NDAttrValue::Int32(_) => "Int32",
79 NDAttrValue::UInt32(_) => "UInt32",
80 NDAttrValue::Int64(_) => "Int64",
81 NDAttrValue::UInt64(_) => "UInt64",
82 NDAttrValue::Float32(_) => "Float32",
83 NDAttrValue::Float64(_) => "Float64",
84 NDAttrValue::String(_) => "String",
85 NDAttrValue::Undefined => "Undefined",
86 }
87}
88
89pub struct NetcdfWriter {
97 current_path: Option<PathBuf>,
98 frames: Vec<FrameData>,
99 open_multiple: bool,
106 file_attributes: FileAttributes,
109}
110
111impl NetcdfWriter {
112 pub fn new() -> Self {
113 Self {
114 current_path: None,
115 frames: Vec::new(),
116 open_multiple: false,
117 file_attributes: FileAttributes::default(),
118 }
119 }
120}
121
122fn nc_data_type(dt: NDDataType) -> ADResult<netcdf3::DataType> {
134 match dt {
135 NDDataType::Int8 | NDDataType::UInt8 => Ok(netcdf3::DataType::I8),
136 NDDataType::Int16 | NDDataType::UInt16 => Ok(netcdf3::DataType::I16),
137 NDDataType::Int32 | NDDataType::UInt32 => Ok(netcdf3::DataType::I32),
138 NDDataType::Float32 => Ok(netcdf3::DataType::F32),
139 NDDataType::Float64 => Ok(netcdf3::DataType::F64),
140 NDDataType::Int64 | NDDataType::UInt64 => Ok(netcdf3::DataType::F64),
141 }
142}
143
144fn write_var_data(writer: &mut FileWriter, data: &NDDataBuffer) -> ADResult<()> {
146 let err = |e: netcdf3::error::WriteError| {
147 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
148 };
149 match data {
150 NDDataBuffer::I8(v) => writer.write_var_i8(VAR_NAME, v).map_err(err),
151 NDDataBuffer::U8(v) => {
155 let reinterp: Vec<i8> = v.iter().map(|&x| x as i8).collect();
156 writer.write_var_i8(VAR_NAME, &reinterp).map_err(err)
157 }
158 NDDataBuffer::I16(v) => writer.write_var_i16(VAR_NAME, v).map_err(err),
159 NDDataBuffer::U16(v) => {
160 let reinterp: Vec<i16> = v.iter().map(|&x| x as i16).collect();
161 writer.write_var_i16(VAR_NAME, &reinterp).map_err(err)
162 }
163 NDDataBuffer::I32(v) => writer.write_var_i32(VAR_NAME, v).map_err(err),
164 NDDataBuffer::U32(v) => {
165 let reinterp: Vec<i32> = v.iter().map(|&x| x as i32).collect();
166 writer.write_var_i32(VAR_NAME, &reinterp).map_err(err)
167 }
168 NDDataBuffer::F32(v) => writer.write_var_f32(VAR_NAME, v).map_err(err),
169 NDDataBuffer::F64(v) => writer.write_var_f64(VAR_NAME, v).map_err(err),
170 NDDataBuffer::I64(v) => {
171 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
172 writer.write_var_f64(VAR_NAME, &reinterp).map_err(err)
173 }
174 NDDataBuffer::U64(v) => {
175 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
176 writer.write_var_f64(VAR_NAME, &reinterp).map_err(err)
177 }
178 }
179}
180
181fn write_record_data(
183 writer: &mut FileWriter,
184 record_index: usize,
185 data: &NDDataBuffer,
186) -> ADResult<()> {
187 let err = |e: netcdf3::error::WriteError| {
188 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
189 };
190 match data {
191 NDDataBuffer::I8(v) => writer
192 .write_record_i8(VAR_NAME, record_index, v)
193 .map_err(err),
194 NDDataBuffer::U8(v) => {
196 let reinterp: Vec<i8> = v.iter().map(|&x| x as i8).collect();
197 writer
198 .write_record_i8(VAR_NAME, record_index, &reinterp)
199 .map_err(err)
200 }
201 NDDataBuffer::I16(v) => writer
202 .write_record_i16(VAR_NAME, record_index, v)
203 .map_err(err),
204 NDDataBuffer::U16(v) => {
205 let reinterp: Vec<i16> = v.iter().map(|&x| x as i16).collect();
206 writer
207 .write_record_i16(VAR_NAME, record_index, &reinterp)
208 .map_err(err)
209 }
210 NDDataBuffer::I32(v) => writer
211 .write_record_i32(VAR_NAME, record_index, v)
212 .map_err(err),
213 NDDataBuffer::U32(v) => {
214 let reinterp: Vec<i32> = v.iter().map(|&x| x as i32).collect();
215 writer
216 .write_record_i32(VAR_NAME, record_index, &reinterp)
217 .map_err(err)
218 }
219 NDDataBuffer::F32(v) => writer
220 .write_record_f32(VAR_NAME, record_index, v)
221 .map_err(err),
222 NDDataBuffer::F64(v) => writer
223 .write_record_f64(VAR_NAME, record_index, v)
224 .map_err(err),
225 NDDataBuffer::I64(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 NDDataBuffer::U64(v) => {
232 let reinterp: Vec<f64> = v.iter().map(|&x| x as f64).collect();
233 writer
234 .write_record_f64(VAR_NAME, record_index, &reinterp)
235 .map_err(err)
236 }
237 }
238}
239
240const ATTR_STRING_DIM: &str = "attrStringSize";
241const ATTR_STRING_SIZE: usize = 256;
242
243fn attr_nc_type(value: &NDAttrValue) -> netcdf3::DataType {
251 match value {
252 NDAttrValue::Int8(_) | NDAttrValue::UInt8(_) | NDAttrValue::Undefined => {
253 netcdf3::DataType::I8
254 }
255 NDAttrValue::Int16(_) | NDAttrValue::UInt16(_) => netcdf3::DataType::I16,
256 NDAttrValue::Int32(_) | NDAttrValue::UInt32(_) => netcdf3::DataType::I32,
257 NDAttrValue::Float32(_) => netcdf3::DataType::F32,
258 NDAttrValue::Float64(_) | NDAttrValue::Int64(_) | NDAttrValue::UInt64(_) => {
259 netcdf3::DataType::F64
260 }
261 NDAttrValue::String(_) => netcdf3::DataType::U8,
262 }
263}
264
265fn write_attr_value(
268 writer: &mut FileWriter,
269 var_name: &str,
270 record_index: usize,
271 multi: bool,
272 value: &NDAttrValue,
273) -> ADResult<()> {
274 let werr = |e: netcdf3::error::WriteError| {
275 ADError::UnsupportedConversion(format!("NetCDF attr write error: {:?}", e))
276 };
277 if let NDAttrValue::String(s) = value {
282 let mut bytes: Vec<u8> = s.bytes().take(ATTR_STRING_SIZE).collect();
283 bytes.resize(ATTR_STRING_SIZE, 0);
284 return if multi {
285 writer
286 .write_record_u8(var_name, record_index, &bytes)
287 .map_err(werr)
288 } else {
289 writer.write_var_u8(var_name, &bytes).map_err(werr)
290 };
291 }
292 match attr_nc_type(value) {
293 netcdf3::DataType::I8 => {
294 let v = value.as_i64().unwrap_or(0) as i8;
295 if multi {
296 writer
297 .write_record_i8(var_name, record_index, &[v])
298 .map_err(werr)
299 } else {
300 writer.write_var_i8(var_name, &[v]).map_err(werr)
301 }
302 }
303 netcdf3::DataType::I16 => {
304 let v = value.as_i64().unwrap_or(0) as i16;
305 if multi {
306 writer
307 .write_record_i16(var_name, record_index, &[v])
308 .map_err(werr)
309 } else {
310 writer.write_var_i16(var_name, &[v]).map_err(werr)
311 }
312 }
313 netcdf3::DataType::I32 => {
314 let v = value.as_i64().unwrap_or(0) as i32;
315 if multi {
316 writer
317 .write_record_i32(var_name, record_index, &[v])
318 .map_err(werr)
319 } else {
320 writer.write_var_i32(var_name, &[v]).map_err(werr)
321 }
322 }
323 netcdf3::DataType::F32 => {
324 let v = value.as_f64().unwrap_or(0.0) as f32;
325 if multi {
326 writer
327 .write_record_f32(var_name, record_index, &[v])
328 .map_err(werr)
329 } else {
330 writer.write_var_f32(var_name, &[v]).map_err(werr)
331 }
332 }
333 netcdf3::DataType::F64 => {
334 let v = value.as_f64().unwrap_or(0.0);
335 if multi {
336 writer
337 .write_record_f64(var_name, record_index, &[v])
338 .map_err(werr)
339 } else {
340 writer.write_var_f64(var_name, &[v]).map_err(werr)
341 }
342 }
343 netcdf3::DataType::U8 => unreachable!("attr_nc_type returns U8 only for strings"),
345 }
346}
347
348fn define_data_set(
362 first: &FrameData,
363 num_frames: usize,
364 multi: bool,
365) -> ADResult<(DataSet, Vec<String>)> {
366 let map_def = |e: netcdf3::error::InvalidDataSet| {
367 ADError::UnsupportedConversion(format!("NetCDF definition error: {:?}", e))
368 };
369
370 let mut ds = DataSet::new();
371 let ndims = first.dims.len();
372
373 ds.add_global_attr_i32("dataType", vec![first.data_type as i32])
378 .map_err(map_def)?;
379 ds.add_global_attr_f64("NDNetCDFFileVersion", vec![ND_NETCDF_FILE_VERSION])
380 .map_err(map_def)?;
381 ds.add_global_attr_i32("numArrayDims", vec![ndims as i32])
382 .map_err(map_def)?;
383 let dim_size: Vec<i32> = first.dim_meta.iter().map(|d| d.size as i32).collect();
386 ds.add_global_attr_i32("dimSize", dim_size)
387 .map_err(map_def)?;
388 let dim_offset: Vec<i32> = first.dim_meta.iter().map(|d| d.offset as i32).collect();
389 ds.add_global_attr_i32("dimOffset", dim_offset)
390 .map_err(map_def)?;
391 let dim_binning: Vec<i32> = first.dim_meta.iter().map(|d| d.binning as i32).collect();
392 ds.add_global_attr_i32("dimBinning", dim_binning)
393 .map_err(map_def)?;
394 let dim_reverse: Vec<i32> = first
395 .dim_meta
396 .iter()
397 .map(|d| if d.reverse { 1 } else { 0 })
398 .collect();
399 ds.add_global_attr_i32("dimReverse", dim_reverse)
400 .map_err(map_def)?;
401
402 if multi {
406 ds.set_unlimited_dim(DIM_UNLIMITED, num_frames)
407 .map_err(map_def)?;
408 } else {
409 ds.add_fixed_dim(DIM_UNLIMITED, 1).map_err(map_def)?;
410 }
411 let mut dim_names: Vec<String> = Vec::new();
414 for i in 0..ndims {
415 let name = format!("dim{}", i);
416 ds.add_fixed_dim(&name, first.dims[ndims - 1 - i])
417 .map_err(map_def)?;
418 dim_names.push(name);
419 }
420 ds.add_fixed_dim(ATTR_STRING_DIM, ATTR_STRING_SIZE)
423 .map_err(map_def)?;
424
425 ds.add_var("uniqueId", &[DIM_UNLIMITED], netcdf3::DataType::I32)
428 .map_err(map_def)?;
429 ds.add_var("timeStamp", &[DIM_UNLIMITED], netcdf3::DataType::F64)
430 .map_err(map_def)?;
431 ds.add_var("epicsTSSec", &[DIM_UNLIMITED], netcdf3::DataType::I32)
432 .map_err(map_def)?;
433 ds.add_var("epicsTSNsec", &[DIM_UNLIMITED], netcdf3::DataType::I32)
434 .map_err(map_def)?;
435
436 let mut var_dims: Vec<&str> = vec![DIM_UNLIMITED];
439 var_dims.extend(dim_names.iter().map(|s| s.as_str()));
440 ds.add_var(VAR_NAME, &var_dims, nc_data_type(first.data_type)?)
441 .map_err(map_def)?;
442
443 let mut attr_var_names: Vec<String> = Vec::new();
449 for attr in &first.attrs {
450 ds.add_global_attr_string(
451 &format!("Attr_{}_DataType", attr.name),
452 &attr.data_type_string,
453 )
454 .map_err(map_def)?;
455 ds.add_global_attr_string(
456 &format!("Attr_{}_Description", attr.name),
457 &attr.description,
458 )
459 .map_err(map_def)?;
460 ds.add_global_attr_string(&format!("Attr_{}_Source", attr.name), &attr.source)
461 .map_err(map_def)?;
462 ds.add_global_attr_string(&format!("Attr_{}_SourceType", attr.name), &attr.source_type)
463 .map_err(map_def)?;
464
465 let var_name = format!("Attr_{}", attr.name);
466 if matches!(attr.value, NDAttrValue::String(_)) {
469 ds.add_var(
470 &var_name,
471 &[DIM_UNLIMITED, ATTR_STRING_DIM],
472 attr_nc_type(&attr.value),
473 )
474 .map_err(map_def)?;
475 } else {
476 ds.add_var(&var_name, &[DIM_UNLIMITED], attr_nc_type(&attr.value))
477 .map_err(map_def)?;
478 }
479 attr_var_names.push(var_name);
480 }
481
482 Ok((ds, attr_var_names))
483}
484
485impl NDFileWriter for NetcdfWriter {
486 fn open_file(&mut self, path: &Path, mode: NDFileMode, array: &NDArray) -> ADResult<()> {
487 self.current_path = Some(path.to_path_buf());
488 self.frames.clear();
489 self.file_attributes.open(array);
492 self.open_multiple = mode != NDFileMode::Single;
497 Ok(())
498 }
499
500 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
501 nc_data_type(array.data.data_type())?;
503
504 let dims: Vec<usize> = array.dims.iter().map(|d| d.size).collect();
505 let dim_meta: Vec<DimMeta> = array
506 .dims
507 .iter()
508 .map(|d| DimMeta {
509 size: d.size,
510 offset: d.offset,
511 binning: d.binning,
512 reverse: d.reverse,
513 })
514 .collect();
515 self.file_attributes.frame(array);
521 let attrs: Vec<AttrData> = self
522 .file_attributes
523 .iter()
524 .map(|a| AttrData {
525 name: a.name.clone(),
526 description: a.description.clone(),
527 source: a.source.source_string().to_string(),
530 source_type: attr_source_type_string(&a.source).to_string(),
531 data_type_string: attr_data_type_string(&a.value).to_string(),
532 value: a.value.clone(),
533 })
534 .collect();
535
536 self.frames.push(FrameData {
537 dims,
538 dim_meta,
539 data: array.data.clone(),
540 data_type: array.data.data_type(),
541 attrs,
542 unique_id: array.unique_id,
543 time_stamp: array.time_stamp,
544 epics_ts_sec: array.timestamp.sec as i32,
545 epics_ts_nsec: array.timestamp.nsec as i32,
546 });
547 Ok(())
548 }
549
550 fn close_file(&mut self) -> ADResult<()> {
558 let mut closing = Finalize::new(self, |w: &mut Self| w.frames.clear());
559 closing.run(|w| {
560 let path = match w.current_path.take() {
561 Some(p) => p,
562 None => return Ok(()),
563 };
564
565 if w.frames.is_empty() {
566 return Ok(());
567 }
568
569 let map_write = |e: netcdf3::error::WriteError| {
570 ADError::UnsupportedConversion(format!("NetCDF write error: {:?}", e))
571 };
572
573 let first = &w.frames[0];
574 let multi = w.open_multiple;
577 let (ds, attr_var_names) = define_data_set(first, w.frames.len(), multi)?;
578
579 let mut writer = FileWriter::open(&path).map_err(map_write)?;
581 writer
582 .set_def(&ds, Version::Classic, 0)
583 .map_err(map_write)?;
584
585 if multi {
586 for (i, frame) in w.frames.iter().enumerate() {
587 write_record_data(&mut writer, i, &frame.data)?;
588 writer
589 .write_record_i32("uniqueId", i, &[frame.unique_id])
590 .map_err(map_write)?;
591 writer
592 .write_record_f64("timeStamp", i, &[frame.time_stamp])
593 .map_err(map_write)?;
594 writer
595 .write_record_i32("epicsTSSec", i, &[frame.epics_ts_sec])
596 .map_err(map_write)?;
597 writer
598 .write_record_i32("epicsTSNsec", i, &[frame.epics_ts_nsec])
599 .map_err(map_write)?;
600 for (attr, var_name) in frame.attrs.iter().zip(&attr_var_names) {
606 write_attr_value(&mut writer, var_name, i, true, &attr.value)?;
607 }
608 }
609 } else {
610 write_var_data(&mut writer, &w.frames[0].data)?;
611 writer
612 .write_var_i32("uniqueId", &[first.unique_id])
613 .map_err(map_write)?;
614 writer
615 .write_var_f64("timeStamp", &[first.time_stamp])
616 .map_err(map_write)?;
617 writer
618 .write_var_i32("epicsTSSec", &[first.epics_ts_sec])
619 .map_err(map_write)?;
620 writer
621 .write_var_i32("epicsTSNsec", &[first.epics_ts_nsec])
622 .map_err(map_write)?;
623 for (attr, var_name) in first.attrs.iter().zip(&attr_var_names) {
624 write_attr_value(&mut writer, var_name, 0, false, &attr.value)?;
625 }
626 }
627
628 writer.close().map_err(map_write)?;
629 Ok(())
630 })
631 }
632
633 fn read_file(&mut self) -> ADResult<NDArray> {
634 let path = self
635 .current_path
636 .as_ref()
637 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
638
639 let map_read = |e: netcdf3::error::ReadError| {
640 ADError::UnsupportedConversion(format!("NetCDF read error: {:?}", e))
641 };
642
643 let mut reader = FileReader::open(path).map_err(map_read)?;
644
645 let (is_record, dims, original_type_ordinal) = {
647 let ds = reader.data_set();
648 let var = ds.get_var(VAR_NAME).ok_or_else(|| {
649 ADError::UnsupportedConversion(format!(
650 "variable '{}' not found in NetCDF file",
651 VAR_NAME
652 ))
653 })?;
654
655 let is_record = ds.is_record_var(VAR_NAME).unwrap_or(false);
656
657 let var_dims_rc = var.get_dims();
658 let mut dims: Vec<NDDimension> = Vec::new();
659 for d in &var_dims_rc {
660 if d.is_unlimited() || d.name() == DIM_UNLIMITED {
664 continue;
665 }
666 dims.push(NDDimension::new(d.size()));
667 }
668
669 let original_type_ordinal = ds
670 .get_global_attr_i32("dataType")
671 .and_then(|slice| slice.first().copied());
672
673 (is_record, dims, original_type_ordinal)
674 };
675
676 let data_vec = if is_record {
678 reader.read_record(VAR_NAME, 0).map_err(map_read)?
679 } else {
680 reader.read_var(VAR_NAME).map_err(map_read)?
681 };
682
683 let (nd_type, buf) = match data_vec {
684 netcdf3::DataVector::I8(v) => (NDDataType::Int8, NDDataBuffer::I8(v)),
685 netcdf3::DataVector::U8(v) => (NDDataType::UInt8, NDDataBuffer::U8(v)),
686 netcdf3::DataVector::I16(v) => (NDDataType::Int16, NDDataBuffer::I16(v)),
687 netcdf3::DataVector::I32(v) => (NDDataType::Int32, NDDataBuffer::I32(v)),
688 netcdf3::DataVector::F32(v) => (NDDataType::Float32, NDDataBuffer::F32(v)),
689 netcdf3::DataVector::F64(v) => (NDDataType::Float64, NDDataBuffer::F64(v)),
690 };
691
692 let actual_type = original_type_ordinal
694 .and_then(|v| NDDataType::from_ordinal(v as u8))
695 .unwrap_or(nd_type);
696
697 let buf = match (actual_type, buf) {
701 (NDDataType::UInt8, NDDataBuffer::I8(v)) => {
702 NDDataBuffer::U8(v.into_iter().map(|x| x as u8).collect())
703 }
704 (NDDataType::UInt16, NDDataBuffer::I16(v)) => {
705 NDDataBuffer::U16(v.into_iter().map(|x| x as u16).collect())
706 }
707 (NDDataType::UInt32, NDDataBuffer::I32(v)) => {
708 NDDataBuffer::U32(v.into_iter().map(|x| x as u32).collect())
709 }
710 (_, buf) => buf,
711 };
712
713 let mut arr = NDArray::new(dims, actual_type);
714 arr.data = buf;
715 Ok(arr)
716 }
717
718 fn supports_multiple_arrays(&self) -> bool {
719 true
720 }
721
722 fn writes_incrementally(&self) -> bool {
729 false
730 }
731}
732
733pub struct NetcdfFileProcessor {
735 ctrl: Mutex<FilePluginController<NetcdfWriter>>,
736}
737
738impl NetcdfFileProcessor {
739 pub fn new() -> Self {
740 Self {
741 ctrl: Mutex::new(FilePluginController::new(NetcdfWriter::new())),
742 }
743 }
744}
745
746impl Default for NetcdfFileProcessor {
747 fn default() -> Self {
748 Self::new()
749 }
750}
751
752impl NDPluginProcess for NetcdfFileProcessor {
753 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
754 self.ctrl.lock().process_array(array)
755 }
756
757 fn plugin_type(&self) -> &str {
758 "NDFileNetCDF"
759 }
760
761 fn does_array_callbacks(&self) -> bool {
764 false
765 }
766
767 fn register_params(
768 &mut self,
769 base: &mut asyn_rs::port::PortDriverBase,
770 ) -> asyn_rs::error::AsynResult<()> {
771 self.ctrl.lock().register_params(base)
772 }
773
774 fn on_param_change(&self, reason: usize, params: &PluginParamSnapshot) -> ParamChangeResult {
775 self.ctrl.lock().on_param_change(reason, params)
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
783 use ad_core_rs::plugin::file_base::NDPluginFileBase;
784 use std::sync::atomic::{AtomicU32, Ordering};
785
786 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
787
788 fn temp_path(prefix: &str) -> PathBuf {
789 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
790 std::env::temp_dir().join(format!(
791 "adcore_test_{}_{}_{}.nc",
792 std::process::id(),
793 prefix,
794 n
795 ))
796 }
797
798 #[test]
803 fn close_file_clears_the_frame_buffer_when_the_write_fails() {
804 let root = tempfile::tempdir().expect("fixture root");
809 let path = root.path().join("no-such-dir").join("frames.nc");
810 let mut writer = NetcdfWriter::new();
811
812 let arr = NDArray::new(
813 vec![NDDimension::new(4), NDDimension::new(4)],
814 NDDataType::UInt8,
815 );
816 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
817 writer.write_file(&arr).unwrap();
818 assert_eq!(writer.frames.len(), 1);
819
820 assert!(
821 writer.close_file().is_err(),
822 "the write into a missing directory must fail the close"
823 );
824 assert!(
825 writer.frames.is_empty(),
826 "a failed close must still drop the frames of the file that was never written"
827 );
828 }
829
830 #[test]
831 fn test_write_u8_mono() {
832 let path = temp_path("nc_u8");
833 let mut writer = NetcdfWriter::new();
834
835 let mut arr = NDArray::new(
836 vec![NDDimension::new(4), NDDimension::new(4)],
837 NDDataType::UInt8,
838 );
839 if let NDDataBuffer::U8(v) = &mut arr.data {
840 for i in 0..16 {
841 v[i] = i as u8;
842 }
843 }
844
845 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
846 writer.write_file(&arr).unwrap();
847 writer.close_file().unwrap();
848
849 let data = std::fs::read(&path).unwrap();
851 assert!(data.len() > 16);
852 assert_eq!(&data[0..3], b"CDF", "Expected NetCDF magic bytes");
853
854 std::fs::remove_file(&path).ok();
855 }
856
857 #[test]
858 fn test_write_u16() {
859 let path = temp_path("nc_u16");
860 let mut writer = NetcdfWriter::new();
861
862 let mut arr = NDArray::new(
863 vec![NDDimension::new(4), NDDimension::new(4)],
864 NDDataType::UInt16,
865 );
866 if let NDDataBuffer::U16(v) = &mut arr.data {
867 for i in 0..16 {
868 v[i] = (i * 1000) as u16;
869 }
870 }
871
872 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
873 writer.write_file(&arr).unwrap();
874 writer.close_file().unwrap();
875
876 let data = std::fs::read(&path).unwrap();
877 assert!(data.len() > 32);
878 assert_eq!(&data[0..3], b"CDF");
879
880 std::fs::remove_file(&path).ok();
881 }
882
883 #[test]
884 fn test_roundtrip_u8() {
885 let path = temp_path("nc_rt_u8");
886 let mut writer = NetcdfWriter::new();
887
888 let mut arr = NDArray::new(
889 vec![NDDimension::new(4), NDDimension::new(4)],
890 NDDataType::UInt8,
891 );
892 if let NDDataBuffer::U8(v) = &mut arr.data {
893 for i in 0..16 {
894 v[i] = (i * 10) as u8;
895 }
896 }
897
898 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
899 writer.write_file(&arr).unwrap();
900 writer.close_file().unwrap();
901
902 writer.current_path = Some(path.clone());
903 let read_back = writer.read_file().unwrap();
904 if let (NDDataBuffer::U8(orig), NDDataBuffer::U8(read)) = (&arr.data, &read_back.data) {
905 assert_eq!(orig, read);
906 } else {
907 panic!("data type mismatch on roundtrip");
908 }
909
910 std::fs::remove_file(&path).ok();
911 }
912
913 #[test]
914 fn test_roundtrip_i16() {
915 let path = temp_path("nc_rt_i16");
916 let mut writer = NetcdfWriter::new();
917
918 let mut arr = NDArray::new(
919 vec![NDDimension::new(4), NDDimension::new(4)],
920 NDDataType::Int16,
921 );
922 if let NDDataBuffer::I16(v) = &mut arr.data {
923 for i in 0..16 {
924 v[i] = (i as i16) * 100 - 500;
925 }
926 }
927
928 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
929 writer.write_file(&arr).unwrap();
930 writer.close_file().unwrap();
931
932 writer.current_path = Some(path.clone());
933 let read_back = writer.read_file().unwrap();
934 if let (NDDataBuffer::I16(orig), NDDataBuffer::I16(read)) = (&arr.data, &read_back.data) {
935 assert_eq!(orig, read);
936 } else {
937 panic!("data type mismatch on roundtrip");
938 }
939
940 std::fs::remove_file(&path).ok();
941 }
942
943 #[test]
944 fn test_roundtrip_f32() {
945 let path = temp_path("nc_rt_f32");
946 let mut writer = NetcdfWriter::new();
947
948 let mut arr = NDArray::new(
949 vec![NDDimension::new(4), NDDimension::new(4)],
950 NDDataType::Float32,
951 );
952 if let NDDataBuffer::F32(v) = &mut arr.data {
953 for i in 0..16 {
954 v[i] = i as f32 * 0.5;
955 }
956 }
957
958 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
959 writer.write_file(&arr).unwrap();
960 writer.close_file().unwrap();
961
962 writer.current_path = Some(path.clone());
963 let read_back = writer.read_file().unwrap();
964 if let (NDDataBuffer::F32(orig), NDDataBuffer::F32(read)) = (&arr.data, &read_back.data) {
965 assert_eq!(orig, read);
966 } else {
967 panic!("data type mismatch on roundtrip");
968 }
969
970 std::fs::remove_file(&path).ok();
971 }
972
973 #[test]
974 fn test_multiple_frames() {
975 let path = temp_path("nc_multi");
976 let mut writer = NetcdfWriter::new();
977
978 let mut arr1 = NDArray::new(
979 vec![NDDimension::new(4), NDDimension::new(4)],
980 NDDataType::UInt8,
981 );
982 if let NDDataBuffer::U8(v) = &mut arr1.data {
983 for i in 0..16 {
984 v[i] = i as u8;
985 }
986 }
987
988 let mut arr2 = NDArray::new(
989 vec![NDDimension::new(4), NDDimension::new(4)],
990 NDDataType::UInt8,
991 );
992 if let NDDataBuffer::U8(v) = &mut arr2.data {
993 for i in 0..16 {
994 v[i] = (i as u8).wrapping_add(100);
995 }
996 }
997
998 let mut arr3 = NDArray::new(
999 vec![NDDimension::new(4), NDDimension::new(4)],
1000 NDDataType::UInt8,
1001 );
1002 if let NDDataBuffer::U8(v) = &mut arr3.data {
1003 for i in 0..16 {
1004 v[i] = (i as u8).wrapping_add(200);
1005 }
1006 }
1007
1008 writer.open_file(&path, NDFileMode::Stream, &arr1).unwrap();
1009 writer.write_file(&arr1).unwrap();
1010 writer.write_file(&arr2).unwrap();
1011 writer.write_file(&arr3).unwrap();
1012 writer.close_file().unwrap();
1013
1014 writer.current_path = Some(path.clone());
1016 let read_back = writer.read_file().unwrap();
1017 if let NDDataBuffer::U8(v) = &read_back.data {
1018 assert_eq!(v.len(), 16);
1019 for i in 0..16 {
1020 assert_eq!(v[i], i as u8, "mismatch at index {}", i);
1021 }
1022 } else {
1023 panic!("expected U8 data");
1024 }
1025
1026 std::fs::remove_file(&path).ok();
1027 }
1028
1029 #[test]
1030 fn test_num_arrays_dim_keyed_on_open_mode_not_frame_count() {
1031 let frame = || NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1039 let num_arrays_is_unlimited = |path: &PathBuf| -> bool {
1040 let reader = FileReader::open(path).unwrap();
1041 reader
1042 .data_set()
1043 .get_dim(DIM_UNLIMITED)
1044 .expect("numArrays dimension")
1045 .is_unlimited()
1046 };
1047
1048 let path = temp_path("nc_mode_capture_one");
1050 let mut writer = NetcdfWriter::new();
1051 writer
1052 .open_file(&path, NDFileMode::Capture, &frame())
1053 .unwrap();
1054 writer.write_file(&frame()).unwrap();
1055 writer.close_file().unwrap();
1056 assert!(
1057 num_arrays_is_unlimited(&path),
1058 "Capture with 1 frame must still be NC_UNLIMITED"
1059 );
1060 std::fs::remove_file(&path).ok();
1061
1062 let path = temp_path("nc_mode_stream_one");
1064 let mut writer = NetcdfWriter::new();
1065 writer
1066 .open_file(&path, NDFileMode::Stream, &frame())
1067 .unwrap();
1068 writer.write_file(&frame()).unwrap();
1069 writer.close_file().unwrap();
1070 assert!(
1071 num_arrays_is_unlimited(&path),
1072 "Stream with 1 frame must still be NC_UNLIMITED"
1073 );
1074 std::fs::remove_file(&path).ok();
1075
1076 let path = temp_path("nc_mode_single_one");
1078 let mut writer = NetcdfWriter::new();
1079 writer
1080 .open_file(&path, NDFileMode::Single, &frame())
1081 .unwrap();
1082 writer.write_file(&frame()).unwrap();
1083 writer.close_file().unwrap();
1084 assert!(
1085 !num_arrays_is_unlimited(&path),
1086 "Single must be a fixed numArrays dimension"
1087 );
1088 std::fs::remove_file(&path).ok();
1089 }
1090
1091 #[test]
1092 fn test_attributes_stored_as_per_frame_variables() {
1093 let path = temp_path("nc_attrs");
1094 let mut writer = NetcdfWriter::new();
1095
1096 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1097 arr.attributes.add(NDAttribute::new_static(
1098 "exposure",
1099 "Exposure time",
1100 NDAttrSource::Driver,
1101 NDAttrValue::Float64(0.5),
1102 ));
1103 arr.attributes.add(NDAttribute::new_static(
1104 "gain",
1105 "Detector gain",
1106 NDAttrSource::Driver,
1107 NDAttrValue::Int32(42),
1108 ));
1109
1110 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1111 writer.write_file(&arr).unwrap();
1112 writer.close_file().unwrap();
1113
1114 let mut reader = FileReader::open(&path).unwrap();
1115 {
1116 let ds = reader.data_set();
1117 assert!(ds.get_var("Attr_exposure").is_some());
1119 assert!(ds.get_var("Attr_gain").is_some());
1120 assert_eq!(
1122 ds.get_global_attr_as_string("Attr_exposure_DataType"),
1123 Some("Float64".to_string())
1124 );
1125 assert_eq!(
1126 ds.get_global_attr_as_string("Attr_gain_DataType"),
1127 Some("Int32".to_string())
1128 );
1129 assert_eq!(
1130 ds.get_global_attr_as_string("Attr_exposure_Description"),
1131 Some("Exposure time".to_string())
1132 );
1133 assert_eq!(
1134 ds.get_global_attr_as_string("Attr_gain_SourceType"),
1135 Some("NDAttrSourceDriver".to_string())
1136 );
1137 }
1138 if let netcdf3::DataVector::F64(v) = reader.read_var("Attr_exposure").unwrap() {
1140 assert_eq!(v, vec![0.5]);
1141 } else {
1142 panic!("Attr_exposure should be F64");
1143 }
1144 if let netcdf3::DataVector::I32(v) = reader.read_var("Attr_gain").unwrap() {
1145 assert_eq!(v, vec![42]);
1146 } else {
1147 panic!("Attr_gain should be I32");
1148 }
1149
1150 drop(reader);
1151 std::fs::remove_file(&path).ok();
1152 }
1153
1154 #[test]
1160 fn attribute_that_drops_out_records_its_last_value() {
1161 let path = temp_path("nc_attr_sticky");
1162 let mut writer = NetcdfWriter::new();
1163
1164 let mk = |exposure: Option<f64>| {
1165 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1166 if let Some(v) = exposure {
1167 arr.attributes.add(NDAttribute::new_static(
1168 "exposure",
1169 "",
1170 NDAttrSource::Driver,
1171 NDAttrValue::Float64(v),
1172 ));
1173 }
1174 arr
1175 };
1176
1177 let a0 = mk(Some(0.5));
1178 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
1179 writer.write_file(&a0).unwrap();
1180 writer.write_file(&mk(Some(0.75))).unwrap();
1181 writer.write_file(&mk(None)).unwrap();
1182 writer.close_file().unwrap();
1183
1184 let mut reader = FileReader::open(&path).unwrap();
1185 match reader.read_var("Attr_exposure").unwrap() {
1186 netcdf3::DataVector::F64(v) => assert_eq!(v, vec![0.5, 0.75, 0.75]),
1187 other => panic!("Attr_exposure should be F64, got {other:?}"),
1188 }
1189
1190 drop(reader);
1191 std::fs::remove_file(&path).ok();
1192 }
1193
1194 #[test]
1195 fn test_single_frame_array_data_has_leading_numarrays_dim() {
1196 let path = temp_path("nc_rank");
1197 let mut writer = NetcdfWriter::new();
1198
1199 let arr = NDArray::new(
1200 vec![NDDimension::new(4), NDDimension::new(3)],
1201 NDDataType::UInt8,
1202 );
1203 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1204 writer.write_file(&arr).unwrap();
1205 writer.close_file().unwrap();
1206
1207 let reader = FileReader::open(&path).unwrap();
1208 let ds = reader.data_set();
1209 let var = ds.get_var("array_data").unwrap();
1210 assert_eq!(var.get_dims().len(), 3);
1213 assert_eq!(var.get_dims()[0].name(), "numArrays");
1214 assert_eq!(var.get_dims()[0].size(), 1);
1215
1216 drop(reader);
1217 std::fs::remove_file(&path).ok();
1218 }
1219
1220 #[test]
1221 fn test_global_attrs_match_c_set() {
1222 let path = temp_path("nc_globals");
1227 let mut writer = NetcdfWriter::new();
1228
1229 let arr = NDArray::new(
1230 vec![NDDimension::new(4), NDDimension::new(3)],
1231 NDDataType::UInt8,
1232 );
1233 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1234 writer.write_file(&arr).unwrap();
1235 writer.close_file().unwrap();
1236
1237 let reader = FileReader::open(&path).unwrap();
1238 let ds = reader.data_set();
1239
1240 assert_eq!(
1241 ds.get_global_attr_f64("NDNetCDFFileVersion"),
1242 Some([3.1f64].as_slice()),
1243 "NDNetCDFFileVersion global must be the 3.1 double"
1244 );
1245 assert!(ds.has_global_attr("dataType"));
1246 assert!(
1247 !ds.has_global_attr("uniqueId"),
1248 "uniqueId is a variable in C, not a global attribute"
1249 );
1250 assert!(
1251 !ds.has_global_attr("numArrays"),
1252 "numArrays is a dimension in C, not a global attribute"
1253 );
1254 assert!(ds.get_var("uniqueId").is_some());
1256
1257 drop(reader);
1258 std::fs::remove_file(&path).ok();
1259 }
1260
1261 #[test]
1262 fn test_all_four_metadata_variables_written_single_frame() {
1263 let path = temp_path("nc_meta");
1264 let mut writer = NetcdfWriter::new();
1265
1266 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
1267 arr.unique_id = 99;
1268 arr.time_stamp = 12.5;
1269 arr.timestamp.sec = 555;
1270 arr.timestamp.nsec = 777;
1271
1272 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1273 writer.write_file(&arr).unwrap();
1274 writer.close_file().unwrap();
1275
1276 let mut reader = FileReader::open(&path).unwrap();
1277 for name in ["uniqueId", "timeStamp", "epicsTSSec", "epicsTSNsec"] {
1278 assert!(
1279 reader.data_set().get_var(name).is_some(),
1280 "{name} variable missing"
1281 );
1282 }
1283 match reader.read_var("uniqueId").unwrap() {
1284 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![99]),
1285 other => panic!("uniqueId wrong type: {other:?}"),
1286 }
1287 match reader.read_var("epicsTSSec").unwrap() {
1288 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![555]),
1289 other => panic!("epicsTSSec wrong type: {other:?}"),
1290 }
1291 match reader.read_var("epicsTSNsec").unwrap() {
1292 netcdf3::DataVector::I32(v) => assert_eq!(v, vec![777]),
1293 other => panic!("epicsTSNsec wrong type: {other:?}"),
1294 }
1295
1296 drop(reader);
1297 std::fs::remove_file(&path).ok();
1298 }
1299
1300 #[test]
1301 fn test_nddatatype_ordinals_match_c() {
1302 assert_eq!(NDDataType::Int8 as i32, 0);
1306 assert_eq!(NDDataType::UInt8 as i32, 1);
1307 assert_eq!(NDDataType::Int16 as i32, 2);
1308 assert_eq!(NDDataType::UInt16 as i32, 3);
1309 assert_eq!(NDDataType::Int32 as i32, 4);
1310 assert_eq!(NDDataType::UInt32 as i32, 5);
1311 assert_eq!(NDDataType::Int64 as i32, 6);
1312 assert_eq!(NDDataType::UInt64 as i32, 7);
1313 assert_eq!(NDDataType::Float32 as i32, 8);
1314 assert_eq!(NDDataType::Float64 as i32, 9);
1315 }
1316
1317 const NC_BYTE: u32 = 1;
1326 const NC_CHAR: u32 = 2;
1327 const NC_SHORT: u32 = 3;
1328 const NC_INT: u32 = 4;
1329 const NC_DOUBLE: u32 = 6;
1330
1331 struct RawVar {
1332 name: String,
1333 nc_type: u32,
1334 dim_ids: Vec<u32>,
1335 begin: usize,
1336 }
1337
1338 struct RawHeader {
1339 dims: Vec<(String, u32)>,
1340 gatt_names: Vec<String>,
1341 vars: Vec<RawVar>,
1342 }
1343
1344 impl RawHeader {
1345 fn var(&self, name: &str) -> &RawVar {
1346 self.vars
1347 .iter()
1348 .find(|v| v.name == name)
1349 .unwrap_or_else(|| panic!("no variable {} in file", name))
1350 }
1351 fn var_names(&self) -> Vec<&str> {
1352 self.vars.iter().map(|v| v.name.as_str()).collect()
1353 }
1354 fn dim_names(&self) -> Vec<&str> {
1355 self.dims.iter().map(|(n, _)| n.as_str()).collect()
1356 }
1357 }
1358
1359 struct Cursor<'a> {
1362 b: &'a [u8],
1363 p: usize,
1364 }
1365
1366 impl<'a> Cursor<'a> {
1367 fn u32(&mut self) -> u32 {
1368 let v = u32::from_be_bytes(self.b[self.p..self.p + 4].try_into().unwrap());
1369 self.p += 4;
1370 v
1371 }
1372 fn name(&mut self) -> String {
1374 let n = self.u32() as usize;
1375 let s = String::from_utf8(self.b[self.p..self.p + n].to_vec()).unwrap();
1376 self.p += n.div_ceil(4) * 4;
1377 s
1378 }
1379 fn skip_att_list(&mut self) {
1380 let tag = self.u32();
1381 let n = self.u32() as usize;
1382 assert!(
1383 tag == 0x0C || (tag == 0 && n == 0),
1384 "bad att_list tag {tag}"
1385 );
1386 for _ in 0..n {
1387 let _name = self.name();
1388 let nc_type = self.u32();
1389 let nelems = self.u32() as usize;
1390 let size = match nc_type {
1391 1 | 2 => 1,
1392 3 => 2,
1393 4 | 5 => 4,
1394 6 => 8,
1395 t => panic!("bad nc_type {t}"),
1396 };
1397 self.p += (nelems * size).div_ceil(4) * 4;
1398 }
1399 }
1400 }
1401
1402 fn parse_header(bytes: &[u8]) -> RawHeader {
1403 assert_eq!(&bytes[0..4], b"CDF\x01", "not a CDF-1 file");
1404 let mut c = Cursor { b: bytes, p: 4 };
1405 let _numrecs = c.u32();
1406
1407 let tag = c.u32();
1409 let ndims = c.u32() as usize;
1410 assert!(tag == 0x0A || (tag == 0 && ndims == 0));
1411 let mut dims = Vec::new();
1412 for _ in 0..ndims {
1413 let name = c.name();
1414 let len = c.u32();
1415 dims.push((name, len));
1416 }
1417
1418 let tag = c.u32();
1420 let natts = c.u32() as usize;
1421 assert!(tag == 0x0C || (tag == 0 && natts == 0));
1422 let mut gatt_names = Vec::new();
1423 for _ in 0..natts {
1424 let name = c.name();
1425 let nc_type = c.u32();
1426 let nelems = c.u32() as usize;
1427 let size = match nc_type {
1428 1 | 2 => 1,
1429 3 => 2,
1430 4 | 5 => 4,
1431 6 => 8,
1432 t => panic!("bad nc_type {t}"),
1433 };
1434 c.p += (nelems * size).div_ceil(4) * 4;
1435 gatt_names.push(name);
1436 }
1437
1438 let tag = c.u32();
1440 let nvars = c.u32() as usize;
1441 assert!(tag == 0x0B || (tag == 0 && nvars == 0));
1442 let mut vars = Vec::new();
1443 for _ in 0..nvars {
1444 let name = c.name();
1445 let rank = c.u32() as usize;
1446 let dim_ids: Vec<u32> = (0..rank).map(|_| c.u32()).collect();
1447 c.skip_att_list();
1448 let nc_type = c.u32();
1449 let _vsize = c.u32();
1450 let begin = c.u32() as usize;
1451 vars.push(RawVar {
1452 name,
1453 nc_type,
1454 dim_ids,
1455 begin,
1456 });
1457 }
1458 RawHeader {
1459 dims,
1460 gatt_names,
1461 vars,
1462 }
1463 }
1464
1465 fn write_one(path: &PathBuf, arr: &NDArray) -> Vec<u8> {
1466 let mut writer = NetcdfWriter::new();
1467 writer.open_file(path, NDFileMode::Single, arr).unwrap();
1468 writer.write_file(arr).unwrap();
1469 writer.close_file().unwrap();
1470 let bytes = std::fs::read(path).unwrap();
1471 std::fs::remove_file(path).ok();
1472 bytes
1473 }
1474
1475 #[test]
1478 fn test_r8_68_uint8_array_data_is_nc_byte() {
1479 let path = temp_path("nc_r8_68_byte");
1480 let mut arr = NDArray::new(
1481 vec![NDDimension::new(2), NDDimension::new(2)],
1482 NDDataType::UInt8,
1483 );
1484 if let NDDataBuffer::U8(v) = &mut arr.data {
1485 v.copy_from_slice(&[0, 1, 200, 255]);
1486 }
1487 let bytes = write_one(&path, &arr);
1488 let hdr = parse_header(&bytes);
1489
1490 let var = hdr.var("array_data");
1491 assert_eq!(
1492 var.nc_type, NC_BYTE,
1493 "array_data must be NC_BYTE, not NC_CHAR"
1494 );
1495
1496 assert_eq!(&bytes[var.begin..var.begin + 4], &[0x00, 0x01, 0xC8, 0xFF]);
1499 }
1500
1501 #[test]
1504 fn test_r8_68_int8_array_data_is_nc_byte() {
1505 let path = temp_path("nc_r8_68_i8");
1506 let arr = NDArray::new(
1507 vec![NDDimension::new(2), NDDimension::new(2)],
1508 NDDataType::Int8,
1509 );
1510 let hdr = parse_header(&write_one(&path, &arr));
1511 assert_eq!(hdr.var("array_data").nc_type, NC_BYTE);
1512 }
1513
1514 #[test]
1518 fn test_r8_68_attr_variable_nc_types_match_c() {
1519 let path = temp_path("nc_r8_68_attrs");
1520 let mut arr = NDArray::new(
1521 vec![NDDimension::new(2), NDDimension::new(2)],
1522 NDDataType::UInt16,
1523 );
1524 arr.attributes.add(NDAttribute::new_static(
1525 "Str",
1526 "a string",
1527 NDAttrSource::Driver,
1528 NDAttrValue::String("hello".into()),
1529 ));
1530 arr.attributes.add(NDAttribute::new_static(
1531 "I8",
1532 "a byte",
1533 NDAttrSource::Driver,
1534 NDAttrValue::Int8(-3),
1535 ));
1536 arr.attributes.add(NDAttribute::new_static(
1537 "I16",
1538 "a short",
1539 NDAttrSource::Driver,
1540 NDAttrValue::Int16(-3),
1541 ));
1542 arr.attributes.add(NDAttribute::new_static(
1543 "I32",
1544 "an int",
1545 NDAttrSource::Driver,
1546 NDAttrValue::Int32(-3),
1547 ));
1548 arr.attributes.add(NDAttribute::new_static(
1549 "I64",
1550 "a long",
1551 NDAttrSource::Driver,
1552 NDAttrValue::Int64(-3),
1553 ));
1554 let bytes = write_one(&path, &arr);
1555 let hdr = parse_header(&bytes);
1556
1557 assert_eq!(
1558 hdr.var("Attr_Str").nc_type,
1559 NC_CHAR,
1560 "string attr must be NC_CHAR"
1561 );
1562 assert_eq!(hdr.var("Attr_I8").nc_type, NC_BYTE);
1563 assert_eq!(hdr.var("Attr_I16").nc_type, NC_SHORT);
1564 assert_eq!(hdr.var("Attr_I32").nc_type, NC_INT);
1565 assert_eq!(hdr.var("Attr_I64").nc_type, NC_DOUBLE);
1567
1568 let str_var = hdr.var("Attr_Str");
1570 assert_eq!(str_var.dim_ids.len(), 2);
1571 let attr_dim = str_var.dim_ids[1] as usize;
1572 assert_eq!(hdr.dims[attr_dim], ("attrStringSize".to_string(), 256));
1573 assert_eq!(&bytes[str_var.begin..str_var.begin + 6], b"hello\0");
1575 }
1576
1577 #[test]
1581 fn test_r8_68_attr_string_dim_defined_without_string_attrs() {
1582 let path = temp_path("nc_r8_68_nodim");
1583 let arr = NDArray::new(
1584 vec![NDDimension::new(2), NDDimension::new(2)],
1585 NDDataType::UInt16,
1586 );
1587 let hdr = parse_header(&write_one(&path, &arr));
1588 assert_eq!(
1589 hdr.dim_names(),
1590 vec!["numArrays", "dim0", "dim1", "attrStringSize"]
1591 );
1592 assert_eq!(hdr.dims[3].1, 256);
1593 }
1594
1595 #[test]
1601 fn test_r8_68_definition_order_matches_c() {
1602 let path = temp_path("nc_r8_68_order");
1603 let mut arr = NDArray::new(
1604 vec![NDDimension::new(4), NDDimension::new(2)],
1605 NDDataType::UInt16,
1606 );
1607 arr.attributes.add(NDAttribute::new_static(
1608 "Gain",
1609 "detector gain",
1610 NDAttrSource::Driver,
1611 NDAttrValue::Float64(2.5),
1612 ));
1613 let hdr = parse_header(&write_one(&path, &arr));
1614
1615 assert_eq!(
1616 hdr.dim_names(),
1617 vec!["numArrays", "dim0", "dim1", "attrStringSize"]
1618 );
1619 assert_eq!(hdr.dims[1].1, 2);
1621 assert_eq!(hdr.dims[2].1, 4);
1622
1623 assert_eq!(
1624 hdr.var_names(),
1625 vec![
1626 "uniqueId",
1627 "timeStamp",
1628 "epicsTSSec",
1629 "epicsTSNsec",
1630 "array_data",
1631 "Attr_Gain",
1632 ]
1633 );
1634
1635 assert_eq!(
1636 hdr.gatt_names,
1637 vec![
1638 "dataType",
1639 "NDNetCDFFileVersion",
1640 "numArrayDims",
1641 "dimSize",
1642 "dimOffset",
1643 "dimBinning",
1644 "dimReverse",
1645 "Attr_Gain_DataType",
1646 "Attr_Gain_Description",
1647 "Attr_Gain_Source",
1648 "Attr_Gain_SourceType",
1649 ]
1650 );
1651 }
1652
1653 #[test]
1658 fn stream_mode_is_refused_rather_than_buffered_in_ram() {
1659 let mut fb = NDPluginFileBase::new();
1660 fb.file_path = "/tmp/".into();
1661 fb.file_name = "nc_stream_".into();
1662 fb.set_mode(NDFileMode::Stream);
1663 fb.set_num_capture(0);
1664
1665 let mut writer = NetcdfWriter::new();
1666 let array = std::sync::Arc::new(NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8));
1667 assert!(fb.process_array(array, &mut writer).is_err());
1668 assert!(writer.frames.is_empty(), "no frame may be buffered");
1669 assert!(!fb.is_open());
1670 assert_eq!(fb.num_captured(), 0);
1671 }
1672}