1use std::path::{Path, PathBuf};
50use std::sync::Arc;
51
52use crate::attributes::{NDAttribute, NDAttributeList};
53use crate::error::{ADError, ADResult};
54use crate::finalize::Finalize;
55use crate::ndarray::NDArray;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum NDFileMode {
60 Single = 0,
61 Capture = 1,
62 Stream = 2,
63}
64
65impl NDFileMode {
66 pub fn from_i32(v: i32) -> Self {
67 match v {
68 0 => Self::Single,
69 1 => Self::Capture,
70 _ => Self::Stream,
71 }
72 }
73}
74
75pub trait NDFileWriter: Send + Sync {
77 fn open_file(&mut self, path: &Path, mode: NDFileMode, array: &NDArray) -> ADResult<()>;
78 fn write_file(&mut self, array: &NDArray) -> ADResult<()>;
79 fn read_file(&mut self) -> ADResult<NDArray>;
80 fn close_file(&mut self) -> ADResult<()>;
81 fn supports_multiple_arrays(&self) -> bool {
82 true
83 }
84 fn set_num_capture(&mut self, _n: usize) {}
89 fn writes_incrementally(&self) -> bool {
96 true
97 }
98}
99
100#[derive(Debug, Clone, Default)]
121pub struct FileAttributes {
122 list: NDAttributeList,
123}
124
125impl FileAttributes {
126 pub fn open(&mut self, array: &NDArray) {
129 self.list.clear();
130 self.list.copy_from(&array.attributes);
131 }
132
133 pub fn frame(&mut self, array: &NDArray) {
135 self.list.copy_from(&array.attributes);
136 }
137
138 pub fn get(&self, name: &str) -> Option<&NDAttribute> {
141 self.list.get(name)
142 }
143
144 pub fn iter(&self) -> impl Iterator<Item = &NDAttribute> {
147 self.list.iter()
148 }
149
150 pub fn len(&self) -> usize {
151 self.list.len()
152 }
153
154 pub fn is_empty(&self) -> bool {
155 self.list.is_empty()
156 }
157}
158
159pub(crate) fn with_open_file<W: NDFileWriter + ?Sized, R>(
171 writer: &mut W,
172 path: &Path,
173 mode: NDFileMode,
174 layout: &NDArray,
175 body: impl FnOnce(&mut W) -> ADResult<R>,
176) -> ADResult<R> {
177 let mut open = Finalize::new(writer, |w: &mut W| {
178 let _ = w.close_file();
179 });
180 let value = open.run(|w| {
181 w.open_file(path, mode, layout)?;
182 body(w)
183 })?;
184 let closed = open.run(|w| w.close_file());
185 open.disarm();
186 closed.map(|()| value)
187}
188
189pub struct NDPluginFileBase {
191 pub file_path: String,
192 pub file_name: String,
193 pub file_number: i32,
194 pub file_template: String,
195 pub auto_increment: bool,
196 pub temp_suffix: String,
197 pub create_dir: i32,
198 pub lazy_open: bool,
199 pub delete_driver_file: bool,
200 capture_buffer: Vec<Arc<NDArray>>,
201 num_capture: usize,
202 num_captured: usize,
203 is_open: bool,
204 mode: NDFileMode,
205 last_written_name: String,
206}
207
208impl NDPluginFileBase {
209 pub fn new() -> Self {
210 Self {
211 file_path: String::new(),
212 file_name: String::new(),
213 file_number: 0,
214 file_template: String::new(),
215 auto_increment: false,
216 temp_suffix: String::new(),
217 create_dir: 0,
218 lazy_open: false,
219 delete_driver_file: false,
220 capture_buffer: Vec::new(),
221 num_capture: 1,
222 num_captured: 0,
223 is_open: false,
224 mode: NDFileMode::Single,
225 last_written_name: String::new(),
226 }
227 }
228
229 pub fn create_file_name(&self) -> String {
235 if self.file_template.is_empty() {
236 format!(
237 "{}{}{:04}",
238 self.file_path, self.file_name, self.file_number
239 )
240 } else {
241 let mut result = String::new();
242 let mut chars = self.file_template.chars().peekable();
243 let mut s_count = 0;
244 while let Some(c) = chars.next() {
245 if c == '%' {
246 let mut left_justify = false;
251 let mut zero_pad = false;
252 loop {
253 match chars.peek() {
254 Some('-') => {
255 left_justify = true;
256 chars.next();
257 }
258 Some('0') => {
259 zero_pad = true;
260 chars.next();
261 }
262 _ => break,
263 }
264 }
265 let mut spec = String::new();
266 while let Some(&nc) = chars.peek() {
267 if nc.is_ascii_digit() || nc == '.' {
268 spec.push(nc);
269 chars.next();
270 } else {
271 break;
272 }
273 }
274 match chars.next() {
275 Some('%') if spec.is_empty() && !left_justify => result.push('%'),
277 Some('s') => {
278 s_count += 1;
279 match s_count {
280 1 => result.push_str(&self.file_path),
281 2 => result.push_str(&self.file_name),
282 _ => {}
283 }
284 }
285 Some('d') => {
286 let (width, precision) = match spec.split_once('.') {
290 Some((w, p)) => (
291 w.parse::<usize>().unwrap_or(0),
292 p.parse::<usize>().unwrap_or(0),
293 ),
294 None => (spec.parse::<usize>().unwrap_or(0), 0),
295 };
296 let digits = format!("{:0>prec$}", self.file_number, prec = precision);
298 if digits.len() >= width {
302 result.push_str(&digits);
303 } else if zero_pad && !left_justify && precision == 0 {
304 result.push_str(&format!(
305 "{:0>width$}",
306 self.file_number,
307 width = width
308 ));
309 } else {
310 let pad = " ".repeat(width - digits.len());
311 if left_justify {
312 result.push_str(&digits);
313 result.push_str(&pad);
314 } else {
315 result.push_str(&pad);
316 result.push_str(&digits);
317 }
318 }
319 }
320 Some(other) => {
321 result.push('%');
322 if left_justify {
323 result.push('-');
324 }
325 if zero_pad {
326 result.push('0');
327 }
328 result.push_str(&spec);
329 result.push(other);
330 }
331 None => result.push('%'),
332 }
333 } else {
334 result.push(c);
335 }
336 }
337 result
338 }
339 }
340
341 pub fn temp_file_path(&self) -> Option<PathBuf> {
343 if self.temp_suffix.is_empty() {
344 None
345 } else {
346 let name = self.create_file_name();
347 Some(PathBuf::from(format!("{}{}", name, self.temp_suffix)))
348 }
349 }
350
351 pub fn last_written_name(&self) -> &str {
353 &self.last_written_name
354 }
355
356 pub fn ensure_directory(&self) -> ADResult<()> {
361 if self.create_dir != 0 && !self.file_path.is_empty() {
362 std::fs::create_dir_all(&self.file_path)?;
363 }
364 Ok(())
365 }
366
367 fn write_path(&self) -> (PathBuf, Option<PathBuf>) {
369 let final_path = PathBuf::from(self.create_file_name());
370 if self.temp_suffix.is_empty() {
371 (final_path, None)
372 } else {
373 let temp = PathBuf::from(format!("{}{}", final_path.display(), self.temp_suffix));
374 (temp, Some(final_path))
375 }
376 }
377
378 fn rename_temp(temp_path: &Path, final_path: &Path) -> ADResult<()> {
380 std::fs::rename(temp_path, final_path)?;
381 Ok(())
382 }
383
384 fn maybe_delete_driver_file(&self, array: &NDArray) {
388 if !self.delete_driver_file {
389 return;
390 }
391 if let Some(driver_file) = array
395 .attributes
396 .get("DriverFileName")
397 .and_then(|attr| attr.value.as_string_typed())
398 {
399 if !driver_file.is_empty() {
400 let _ = std::fs::remove_file(driver_file);
401 }
402 }
403 }
404
405 pub fn process_array(
407 &mut self,
408 array: Arc<NDArray>,
409 writer: &mut dyn NDFileWriter,
410 ) -> ADResult<()> {
411 writer.set_num_capture(self.num_capture);
414 match self.mode {
415 NDFileMode::Single => {
416 self.last_written_name = self.create_file_name();
417 let (write_path, final_path) = self.write_path();
418 with_open_file(writer, &write_path, NDFileMode::Single, &array, |w| {
419 w.write_file(&array)
420 })?;
421 if let Some(final_path) = final_path {
422 Self::rename_temp(&write_path, &final_path)?;
423 }
424 self.maybe_delete_driver_file(&array);
425 if self.auto_increment {
426 self.file_number += 1;
427 }
428 }
429 NDFileMode::Capture => {
430 self.capture_buffer.push(array);
431 self.num_captured = self.capture_buffer.len();
432 if self.num_capture > 0 && self.num_captured >= self.num_capture {
434 self.flush_capture(writer)?;
435 }
436 }
437 NDFileMode::Stream => {
438 self.open_stream(writer, &array)?;
439 writer.write_file(&array)?;
440 self.maybe_delete_driver_file(&array);
441 self.num_captured += 1;
442 }
443 }
444 Ok(())
445 }
446
447 pub fn flush_capture(&mut self, writer: &mut dyn NDFileWriter) -> ADResult<()> {
461 if self.capture_buffer.is_empty() {
462 return Ok(());
463 }
464 writer.set_num_capture(self.num_capture);
465
466 if writer.supports_multiple_arrays() {
467 self.last_written_name = self.create_file_name();
470 let (write_path, final_path) = self.write_path();
471 let buffer = &self.capture_buffer;
472 with_open_file(writer, &write_path, NDFileMode::Capture, &buffer[0], |w| {
473 for arr in buffer {
474 w.write_file(arr)?;
475 }
476 Ok(())
477 })?;
478 if let Some(final_path) = final_path {
479 Self::rename_temp(&write_path, &final_path)?;
480 }
481 let landed = self.take_landed(self.capture_buffer.len());
482 for arr in &landed {
484 self.maybe_delete_driver_file(arr);
485 }
486 if self.auto_increment {
487 self.file_number += 1;
488 }
489 } else {
490 while !self.capture_buffer.is_empty() {
495 let arr = Arc::clone(&self.capture_buffer[0]);
496 self.last_written_name = self.create_file_name();
497 let (write_path, final_path) = self.write_path();
498 with_open_file(writer, &write_path, NDFileMode::Single, &arr, |w| {
499 w.write_file(&arr)
500 })?;
501 if let Some(final_path) = final_path {
502 Self::rename_temp(&write_path, &final_path)?;
503 }
504 self.take_landed(1);
505 self.maybe_delete_driver_file(&arr);
506 if self.auto_increment {
507 self.file_number += 1;
508 }
509 }
510 }
511
512 Ok(())
513 }
514
515 fn take_landed(&mut self, n: usize) -> Vec<Arc<NDArray>> {
520 let landed: Vec<Arc<NDArray>> = self.capture_buffer.drain(..n).collect();
521 self.num_captured = self.capture_buffer.len();
522 landed
523 }
524
525 pub fn open_stream(&mut self, writer: &mut dyn NDFileWriter, array: &NDArray) -> ADResult<()> {
541 if self.is_open {
542 return Ok(());
543 }
544 if !writer.writes_incrementally() {
545 return Err(ADError::UnsupportedConversion(
546 "this file format writes the whole file when it is closed and \
547 cannot stream; use FileWriteMode=Capture"
548 .into(),
549 ));
550 }
551 writer.set_num_capture(self.num_capture);
552 self.last_written_name = self.create_file_name();
553 let (write_path, _) = self.write_path();
554 writer.open_file(&write_path, NDFileMode::Stream, array)?;
555 self.is_open = true;
556 Ok(())
557 }
558
559 pub fn force_close(&mut self, writer: &mut dyn NDFileWriter) -> ADResult<()> {
562 if self.is_open {
563 self.close_stream(writer)?;
564 }
565 Ok(())
566 }
567
568 pub fn close_stream(&mut self, writer: &mut dyn NDFileWriter) -> ADResult<()> {
575 if !self.is_open {
576 return Ok(());
577 }
578 let mut closing = Finalize::new(self, |base: &mut Self| base.is_open = false);
579 closing.run(|base| {
580 writer.close_file()?;
581 if !base.temp_suffix.is_empty() {
583 let final_name = base.create_file_name();
584 let temp_name = format!("{}{}", final_name, base.temp_suffix);
585 Self::rename_temp(Path::new(&temp_name), Path::new(&final_name))?;
586 }
587 if base.auto_increment {
588 base.file_number += 1;
589 }
590 Ok(())
591 })
592 }
593
594 pub fn is_open(&self) -> bool {
595 self.is_open
596 }
597
598 pub fn set_mode(&mut self, mode: NDFileMode) {
599 self.mode = mode;
600 }
601
602 pub fn set_num_capture(&mut self, n: usize) {
603 self.num_capture = n;
604 }
605
606 pub fn num_captured(&self) -> usize {
607 self.num_captured
608 }
609
610 pub fn mode(&self) -> NDFileMode {
611 self.mode
612 }
613
614 pub fn num_capture_target(&self) -> usize {
615 self.num_capture
616 }
617
618 pub fn capture_array(&mut self, array: Arc<NDArray>) {
619 self.capture_buffer.push(array);
620 self.num_captured = self.capture_buffer.len();
621 }
622
623 pub fn clear_capture(&mut self) {
624 self.capture_buffer.clear();
625 self.num_captured = 0;
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632 use crate::ndarray::{NDDataType, NDDimension};
633
634 struct MockWriter {
636 opens: Vec<PathBuf>,
637 writes: usize,
638 closes: usize,
639 multi: bool,
640 fail_close: bool,
643 fail_write: bool,
645 incremental: bool,
647 }
648
649 impl MockWriter {
650 fn new(multi: bool) -> Self {
651 Self {
652 opens: Vec::new(),
653 writes: 0,
654 closes: 0,
655 multi,
656 fail_close: false,
657 fail_write: false,
658 incremental: true,
659 }
660 }
661 }
662
663 impl NDFileWriter for MockWriter {
664 fn open_file(&mut self, path: &Path, _mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
665 self.opens.push(path.to_path_buf());
666 Ok(())
667 }
668 fn write_file(&mut self, _array: &NDArray) -> ADResult<()> {
669 if self.fail_write {
670 return Err(crate::error::ADError::UnsupportedConversion(
671 "disk full".into(),
672 ));
673 }
674 self.writes += 1;
675 Ok(())
676 }
677 fn read_file(&mut self) -> ADResult<NDArray> {
678 Err(crate::error::ADError::UnsupportedConversion(
679 "not implemented".into(),
680 ))
681 }
682 fn close_file(&mut self) -> ADResult<()> {
683 self.closes += 1;
684 if self.fail_close {
685 return Err(crate::error::ADError::UnsupportedConversion(
686 "disk full".into(),
687 ));
688 }
689 Ok(())
690 }
691 fn supports_multiple_arrays(&self) -> bool {
692 self.multi
693 }
694 fn writes_incrementally(&self) -> bool {
695 self.incremental
696 }
697 }
698
699 fn make_array(id: i32) -> Arc<NDArray> {
700 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
701 arr.unique_id = id;
702 Arc::new(arr)
703 }
704
705 #[test]
706 fn test_single_mode() {
707 let mut fb = NDPluginFileBase::new();
708 fb.file_path = "/tmp/".into();
709 fb.file_name = "test_".into();
710 fb.file_number = 1;
711 fb.auto_increment = true;
712 fb.set_mode(NDFileMode::Single);
713
714 let mut writer = MockWriter::new(false);
715 fb.process_array(make_array(1), &mut writer).unwrap();
716
717 assert_eq!(writer.opens.len(), 1);
718 assert_eq!(writer.writes, 1);
719 assert_eq!(writer.closes, 1);
720 assert_eq!(fb.file_number, 2); }
722
723 #[test]
724 fn test_capture_mode() {
725 let mut fb = NDPluginFileBase::new();
726 fb.file_path = "/tmp/".into();
727 fb.file_name = "cap_".into();
728 fb.set_mode(NDFileMode::Capture);
729 fb.set_num_capture(3);
730
731 let mut writer = MockWriter::new(true);
732
733 fb.process_array(make_array(1), &mut writer).unwrap();
735 assert_eq!(writer.writes, 0); fb.process_array(make_array(2), &mut writer).unwrap();
737 assert_eq!(writer.writes, 0);
738 fb.process_array(make_array(3), &mut writer).unwrap();
739 assert_eq!(writer.opens.len(), 1);
741 assert_eq!(writer.writes, 3);
742 assert_eq!(writer.closes, 1);
743 }
744
745 #[test]
746 fn test_capture_mode_single_image_format() {
747 let mut fb = NDPluginFileBase::new();
748 fb.file_path = "/tmp/".into();
749 fb.file_name = "jpeg_".into();
750 fb.file_number = 0;
751 fb.auto_increment = true;
752 fb.set_mode(NDFileMode::Capture);
753 fb.set_num_capture(3);
754
755 let mut writer = MockWriter::new(false); fb.process_array(make_array(1), &mut writer).unwrap();
758 fb.process_array(make_array(2), &mut writer).unwrap();
759 fb.process_array(make_array(3), &mut writer).unwrap();
760 assert_eq!(writer.opens.len(), 3);
762 assert_eq!(writer.writes, 3);
763 assert_eq!(writer.closes, 3);
764 assert_eq!(fb.file_number, 3); }
766
767 #[test]
768 fn test_stream_mode() {
769 let mut fb = NDPluginFileBase::new();
770 fb.file_path = "/tmp/".into();
771 fb.file_name = "stream_".into();
772 fb.set_mode(NDFileMode::Stream);
773
774 let mut writer = MockWriter::new(true);
775
776 fb.process_array(make_array(1), &mut writer).unwrap();
777 fb.process_array(make_array(2), &mut writer).unwrap();
778 fb.process_array(make_array(3), &mut writer).unwrap();
779
780 assert_eq!(writer.opens.len(), 1); assert_eq!(writer.writes, 3);
782 assert_eq!(writer.closes, 0); fb.close_stream(&mut writer).unwrap();
785 assert_eq!(writer.closes, 1);
786 }
787
788 #[test]
789 fn test_create_file_name_default() {
790 let mut fb = NDPluginFileBase::new();
791 fb.file_path = "/data/".into();
792 fb.file_name = "img_".into();
793 fb.file_number = 42;
794 assert_eq!(fb.create_file_name(), "/data/img_0042");
795 }
796
797 #[test]
798 fn test_create_file_name_template() {
799 let mut fb = NDPluginFileBase::new();
800 fb.file_path = "/data/".into();
801 fb.file_name = "img_".into();
802 fb.file_number = 5;
803 fb.file_template = "%s%s%d.tif".into();
804 assert_eq!(fb.create_file_name(), "/data/img_5.tif");
805 }
806
807 #[test]
808 fn test_create_file_name_printf_specs() {
809 let mut fb = NDPluginFileBase::new();
811 fb.file_path = "/d/".into();
812 fb.file_name = "f".into();
813 fb.file_number = 7;
814
815 fb.file_template = "%s%s_%3.3d.dat".into();
816 assert_eq!(fb.create_file_name(), "/d/f_007.dat");
817
818 fb.file_template = "%s%s_%5d".into();
820 assert_eq!(fb.create_file_name(), "/d/f_ 7");
821
822 fb.file_template = "%s%s_%-5d".into();
824 assert_eq!(fb.create_file_name(), "/d/f_7 ");
825
826 fb.file_template = "%s%s_%05d".into();
828 assert_eq!(fb.create_file_name(), "/d/f_00007");
829
830 fb.file_template = "%s%s_%d%%".into();
832 assert_eq!(fb.create_file_name(), "/d/f_7%");
833 }
834
835 #[test]
836 fn test_auto_increment() {
837 let mut fb = NDPluginFileBase::new();
838 fb.file_path = "/tmp/".into();
839 fb.file_name = "t_".into();
840 fb.file_number = 0;
841 fb.auto_increment = true;
842 fb.set_mode(NDFileMode::Single);
843
844 let mut writer = MockWriter::new(false);
845 fb.process_array(make_array(1), &mut writer).unwrap();
846 assert_eq!(fb.file_number, 1);
847 fb.process_array(make_array(2), &mut writer).unwrap();
848 assert_eq!(fb.file_number, 2);
849 }
850
851 #[test]
852 fn test_temp_suffix() {
853 let mut fb = NDPluginFileBase::new();
854 fb.file_path = "/data/".into();
855 fb.file_name = "img_".into();
856 fb.file_number = 1;
857 fb.temp_suffix = ".tmp".into();
858
859 let temp = fb.temp_file_path().unwrap();
860 assert_eq!(temp.to_str().unwrap(), "/data/img_0001.tmp");
861 }
862
863 fn make_array_with_driver_file(id: i32, driver_file: &str) -> Arc<NDArray> {
864 use crate::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
865 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
866 arr.unique_id = id;
867 arr.attributes.add(NDAttribute::new_static(
868 "DriverFileName",
869 "",
870 NDAttrSource::Driver,
871 NDAttrValue::String(driver_file.to_string()),
872 ));
873 Arc::new(arr)
874 }
875
876 #[test]
877 fn test_delete_driver_file_in_capture_mode() {
878 let dir = std::env::temp_dir();
881 let f1 = dir.join(format!(
882 "adcore_capture_driver_1_{}.raw",
883 std::process::id()
884 ));
885 let f2 = dir.join(format!(
886 "adcore_capture_driver_2_{}.raw",
887 std::process::id()
888 ));
889 std::fs::write(&f1, b"x").unwrap();
890 std::fs::write(&f2, b"y").unwrap();
891
892 let mut fb = NDPluginFileBase::new();
893 fb.file_path = format!("{}/", dir.display());
894 fb.file_name = "capdel_".into();
895 fb.delete_driver_file = true;
896 fb.set_mode(NDFileMode::Capture);
897 fb.set_num_capture(2);
898
899 let mut writer = MockWriter::new(true);
900 fb.process_array(
901 make_array_with_driver_file(1, f1.to_str().unwrap()),
902 &mut writer,
903 )
904 .unwrap();
905 fb.process_array(
906 make_array_with_driver_file(2, f2.to_str().unwrap()),
907 &mut writer,
908 )
909 .unwrap();
910
911 assert!(
913 !f1.exists(),
914 "driver file 1 should be deleted in capture mode"
915 );
916 assert!(
917 !f2.exists(),
918 "driver file 2 should be deleted in capture mode"
919 );
920 }
921
922 #[test]
923 fn test_delete_driver_file_capture_single_image_format() {
924 let dir = std::env::temp_dir();
925 let f1 = dir.join(format!(
926 "adcore_capture_si_driver_1_{}.raw",
927 std::process::id()
928 ));
929 std::fs::write(&f1, b"x").unwrap();
930
931 let mut fb = NDPluginFileBase::new();
932 fb.file_path = format!("{}/", dir.display());
933 fb.file_name = "capdelsi_".into();
934 fb.delete_driver_file = true;
935 fb.set_mode(NDFileMode::Capture);
936 fb.set_num_capture(1);
937
938 let mut writer = MockWriter::new(false); fb.process_array(
940 make_array_with_driver_file(1, f1.to_str().unwrap()),
941 &mut writer,
942 )
943 .unwrap();
944
945 assert!(
946 !f1.exists(),
947 "driver file should be deleted in capture mode"
948 );
949 }
950
951 #[test]
952 fn test_ensure_directory() {
953 let fb = NDPluginFileBase::new();
954 fb.ensure_directory().unwrap();
956 }
957
958 #[test]
962 fn single_write_closes_the_file_when_the_write_fails() {
963 let mut fb = NDPluginFileBase::new();
964 fb.file_path = "/tmp/".into();
965 fb.file_name = "adcore_single_fail_".into();
966 fb.set_mode(NDFileMode::Single);
967
968 let mut writer = MockWriter::new(false);
969 writer.fail_write = true;
970 assert!(fb.process_array(make_array(1), &mut writer).is_err());
971 assert_eq!(writer.opens.len(), 1);
972 assert_eq!(
973 writer.closes, 1,
974 "a failed write must still close the file it opened"
975 );
976 }
977
978 #[test]
980 fn flush_capture_closes_the_file_when_a_frame_write_fails() {
981 let (mut fb, mut writer) = partial_flush_fixture();
982 writer.fail_write = true;
983 assert!(fb.flush_capture(&mut writer).is_err());
984 assert_eq!(
985 writer.closes, 1,
986 "the file opened for the failing frame is still closed"
987 );
988 }
989
990 #[test]
995 fn flush_capture_keeps_the_buffer_when_a_frame_write_fails() {
996 let (mut fb, mut writer) = partial_flush_fixture();
997 writer.fail_write = true;
998 assert!(fb.flush_capture(&mut writer).is_err());
999
1000 writer.fail_write = false;
1001 fb.flush_capture(&mut writer).unwrap();
1002 assert_eq!(
1003 writer.writes, 2,
1004 "both buffered frames survive the failed flush and are written on retry"
1005 );
1006 }
1007
1008 fn partial_flush_fixture() -> (NDPluginFileBase, MockWriter) {
1011 let mut fb = NDPluginFileBase::new();
1012 fb.file_path = "/tmp/".into();
1013 fb.file_name = "adcore_partial_".into();
1014 fb.set_mode(NDFileMode::Capture);
1015 fb.set_num_capture(2);
1016 fb.capture_array(make_array(1));
1017 fb.capture_array(make_array(2));
1018 (fb, MockWriter::new(false))
1019 }
1020
1021 #[test]
1024 fn close_stream_clears_is_open_when_close_file_fails() {
1025 let mut fb = NDPluginFileBase::new();
1026 fb.file_path = "/tmp/".into();
1027 fb.file_name = "adcore_wedge_".into();
1028 fb.set_mode(NDFileMode::Stream);
1029
1030 let mut writer = MockWriter::new(true);
1031 fb.process_array(make_array(1), &mut writer).unwrap();
1032 assert!(fb.is_open());
1033
1034 writer.fail_close = true;
1035 assert!(
1036 fb.close_stream(&mut writer).is_err(),
1037 "the close failure is still reported"
1038 );
1039 assert!(
1040 !fb.is_open(),
1041 "a failed close must still end the open state"
1042 );
1043
1044 writer.fail_close = false;
1045 fb.process_array(make_array(2), &mut writer).unwrap();
1046 assert_eq!(
1047 writer.opens.len(),
1048 2,
1049 "the next frame opens a new stream file instead of appending to the stale handle"
1050 );
1051 }
1052
1053 #[test]
1056 fn close_stream_clears_is_open_when_temp_rename_fails() {
1057 let mut fb = NDPluginFileBase::new();
1058 fb.file_path = "/tmp/adcore-no-such-dir-for-close-stream/".into();
1060 fb.file_name = "norename_".into();
1061 fb.temp_suffix = ".tmp".into();
1062 fb.set_mode(NDFileMode::Stream);
1063
1064 let mut writer = MockWriter::new(true);
1065 fb.process_array(make_array(1), &mut writer).unwrap();
1066 assert!(fb.is_open());
1067
1068 assert!(fb.close_stream(&mut writer).is_err());
1069 assert!(
1070 !fb.is_open(),
1071 "a failed temp rename must still end the open state"
1072 );
1073 }
1074
1075 #[test]
1081 fn stream_open_is_refused_for_a_writer_that_writes_only_on_close() {
1082 let mut fb = NDPluginFileBase::new();
1083 fb.file_path = "/tmp/".into();
1084 fb.file_name = "stream_".into();
1085 fb.set_mode(NDFileMode::Stream);
1086 fb.set_num_capture(0);
1087
1088 let mut writer = MockWriter::new(true);
1089 writer.incremental = false;
1090
1091 assert!(fb.process_array(make_array(1), &mut writer).is_err());
1092 assert!(writer.opens.is_empty(), "no file may be opened");
1093 assert_eq!(writer.writes, 0, "no frame may be handed to the writer");
1094 assert!(!fb.is_open());
1095 assert_eq!(
1096 fb.num_captured(),
1097 0,
1098 "NumCaptured must not count a frame that is not on disk"
1099 );
1100 }
1101}