From e2f0aed1bf5ef003997f6258a7385c4c7335dc78 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 12 Oct 2026 10:42:13 +0700 Subject: [PATCH] #1244: test that a native save failing partway leaves the file as it was --- crates/engine/src/cmd/fileio/tests.rs | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/engine/src/cmd/fileio/tests.rs b/crates/engine/src/cmd/fileio/tests.rs index 5af1e823..67cd74a9 100644 --- a/crates/engine/src/cmd/fileio/tests.rs +++ b/crates/engine/src/cmd/fileio/tests.rs @@ -442,3 +442,46 @@ fn a_native_save_streams_the_bytes_the_encoder_makes() { assert_eq!(e.to_string(), "disk full"); let _ = std::fs::remove_dir_all(&d); } + +/// A native save whose file write fails partway (a full disk) leaves the file as it was, and no +/// temporary file behind. +#[test] +fn a_save_that_fails_while_streaming_leaves_the_file_as_it_was() { + /// Takes `left` bytes, then fails as a full disk does. + struct FailAfter<'a> { + inner: &'a mut dyn std::io::Write, + left: usize, + } + impl std::io::Write for FailAfter<'_> { + fn write(&mut self, b: &[u8]) -> std::io::Result { + if self.left == 0 { + return Err(std::io::Error::other("disk full")); + } + let n = b.len().min(self.left); + self.left -= n; + self.inner.write(&b[..n]) + } + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } + } + let d = tmp_dir("stream-fail"); + let mut s = session(2000.0, 2000.0, 1); + // Enough objects that the file is written in several pieces, the failure in the middle. + for i in 0..1500 { + s.execute("shape.rectangle", &json!({"x": i % 100 * 20, "y": i / 100 * 20, "width": 10, "height": 10})).unwrap(); + } + for compress in [false, true] { + let path = d.join("a.vectorcraft").to_string_lossy().to_string(); + s.execute("file.saveCopy", &json!({"path": path, "compress": compress})).unwrap(); + let before = std::fs::read(&path).unwrap(); + s.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap(); + let plan = save_plan(&s, SaveMode::Copy, &json!({"path": path, "compress": compress})).unwrap(); + let job = save_job(&mut s, plan).unwrap(); + let e = job.write_streamed(&mut |p, fill| super::write_file_with(p, &mut |w| fill(&mut FailAfter { inner: w, left: 10_000 }))).unwrap_err(); + assert!(e.to_string().contains("disk full") || e.to_string().contains("couldn't be encoded"), "{e}"); + assert_eq!(std::fs::read(&path).unwrap(), before, "compress: {compress}"); + assert_eq!(std::fs::read_dir(&d).unwrap().count(), 1, "no temporary file is left"); + } + let _ = std::fs::remove_dir_all(&d); +}