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