Skip to main content

common/
filegen.rs

1use anyhow::{Context, anyhow};
2use async_recursion::async_recursion;
3use tracing::instrument;
4
5use crate::progress;
6
7/// Error type for filegen operations. See [`crate::error::OperationError`] for
8/// logging conventions and rationale.
9pub type Error = crate::error::OperationError<Summary>;
10
11#[derive(Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
12pub struct Summary {
13    pub files_created: usize,
14    pub directories_created: usize,
15    pub bytes_written: u64,
16}
17
18impl std::ops::Add for Summary {
19    type Output = Self;
20    fn add(self, other: Self) -> Self {
21        Self {
22            files_created: self.files_created + other.files_created,
23            directories_created: self.directories_created + other.directories_created,
24            bytes_written: self.bytes_written + other.bytes_written,
25        }
26    }
27}
28
29impl std::fmt::Display for Summary {
30    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31        write!(
32            f,
33            "files created: {}\n\
34            directories created: {}\n\
35            bytes written: {}",
36            self.files_created,
37            self.directories_created,
38            bytesize::ByteSize(self.bytes_written)
39        )
40    }
41}
42
43/// Configuration for file generation
44#[derive(Debug, Clone)]
45pub struct FileGenConfig {
46    /// Root directory for file generation
47    pub root: std::path::PathBuf,
48    /// Directory width at each level
49    pub dirwidth: Vec<usize>,
50    /// Number of files to generate at each leaf
51    pub numfiles: usize,
52    /// Size of each file in bytes
53    pub filesize: usize,
54    /// Write buffer size in bytes
55    pub writebuf: usize,
56    /// Chunk size for I/O throttling
57    pub chunk_size: u64,
58    /// Whether to generate files at leaf directories only
59    pub leaf_files: bool,
60}
61
62impl FileGenConfig {
63    /// Create a new file generation configuration
64    pub fn new(
65        root: impl Into<std::path::PathBuf>,
66        dirwidth: Vec<usize>,
67        numfiles: usize,
68        filesize: usize,
69    ) -> Self {
70        Self {
71            root: root.into(),
72            dirwidth,
73            numfiles,
74            filesize,
75            writebuf: 1024 * 1024, // 1MB default
76            chunk_size: 0,
77            leaf_files: false,
78        }
79    }
80}
81
82fn allocate_write_buffer(path: &std::path::Path, bufsize: usize) -> Vec<u8> {
83    #[cfg(test)]
84    crate::testutils::record_blocking_path_allocation(path);
85    #[cfg(not(test))]
86    let _ = path;
87    vec![0u8; bufsize]
88}
89
90#[instrument(skip(prog_track))]
91pub async fn write_file(
92    prog_track: &'static progress::Progress,
93    path: std::path::PathBuf,
94    mut filesize: usize,
95    bufsize: usize,
96    chunk_size: u64,
97) -> Result<Summary, Error> {
98    let open_file_guard = throttle::open_file_permit().await;
99    let admission = open_file_guard.admission();
100    crate::safedir::with_fd_admission(admission, async move {
101        let _open_file_guard = open_file_guard;
102        throttle::get_file_iops_tokens(chunk_size, filesize as u64).await;
103        let _ops_guard = prog_track.ops.guard();
104        let original_filesize = filesize;
105        // The file open is the single metadata syscall in this path; wrap it
106        // with the cwnd permit + probe so filegen participates in the same
107        // adaptive control loop as copy/rm/link. Use the `_no_rate` variant
108        // because filegen gates the ops-throttle at task-spawn time (see
109        // `filegen` below) — going through the rate-gating helper here
110        // would consume two tokens per file and halve the effective rate.
111        let open_path = path.clone();
112        let file = crate::safedir::run_metadata_probed_blocking_no_rate(
113            congestion::Side::Destination,
114            congestion::MetadataOp::OpenCreate,
115            move || {
116                std::fs::OpenOptions::new()
117                    .write(true)
118                    .create(true)
119                    .truncate(false)
120                    .open(open_path)
121            },
122        )
123        .await
124        .with_context(|| format!("Error opening {:?}", &path))
125        .map_err(|err| Error::new(err, Default::default()))?;
126        let mut file = file;
127        let mut bytes = None;
128        while filesize > 0 {
129            let writesize = std::cmp::min(filesize, bufsize);
130            let write_path = path.clone();
131            let chunk_output = crate::safedir::run_fd_admitted_blocking(move || {
132                use std::io::Write as _;
133
134                #[cfg(test)]
135                let gate_visit = {
136                    use std::os::fd::AsRawFd as _;
137                    crate::testutils::wait_on_blocking_path_gate(&write_path, file.as_raw_fd())
138                };
139                // rebind the captured owner after the visit so unwinding drops the fd before the
140                // completion witness. the tuple below preserves that order when a completed output
141                // is abandoned.
142                let mut file = file;
143                let mut bytes =
144                    bytes.unwrap_or_else(|| allocate_write_buffer(&write_path, bufsize));
145                // rng state is thread-local and never crosses a blocking boundary.
146                rand::fill(&mut bytes[..]);
147                let result = file
148                    .write_all(&bytes[..writesize])
149                    .with_context(|| format!("Error writing to {:?}", &write_path));
150                #[cfg(test)]
151                {
152                    Ok((result, file, gate_visit, bytes))
153                }
154                #[cfg(not(test))]
155                {
156                    Ok((result, file, bytes))
157                }
158            })
159            .await
160            .with_context(|| format!("Error running file writer for {:?}", &path))
161            .map_err(|err| Error::new(err, Default::default()))?;
162            #[cfg(test)]
163            let (write_result, returned_file, _gate_visit, returned_bytes) = chunk_output;
164            #[cfg(not(test))]
165            let (write_result, returned_file, returned_bytes) = chunk_output;
166            file = returned_file;
167            bytes = Some(returned_bytes);
168            write_result.map_err(|err| Error::new(err, Default::default()))?;
169            filesize -= writesize;
170            prog_track.bytes_copied.add(writesize as u64);
171        }
172        let flush_path = path.clone();
173        let (flush_result, file) = crate::safedir::run_fd_admitted_blocking(move || {
174            use std::io::Write as _;
175
176            let mut file = file;
177            let result = file
178                .flush()
179                .with_context(|| format!("Error flushing {:?}", &flush_path));
180            Ok((result, file))
181        })
182        .await
183        .with_context(|| format!("Error running file writer for {:?}", &path))
184        .map_err(|err| Error::new(err, Default::default()))?;
185        flush_result.map_err(|err| Error::new(err, Default::default()))?;
186        drop(file);
187        prog_track.files_copied.inc();
188        Ok(Summary {
189            files_created: 1,
190            bytes_written: original_filesize as u64,
191            ..Default::default()
192        })
193    })
194    .await
195}
196
197#[async_recursion]
198#[instrument(skip(prog_track))]
199pub async fn filegen(
200    prog_track: &'static progress::Progress,
201    config: &FileGenConfig,
202) -> Result<Summary, Error> {
203    let FileGenConfig {
204        root,
205        dirwidth,
206        numfiles,
207        filesize,
208        writebuf,
209        chunk_size,
210        leaf_files,
211    } = config;
212    let numdirs = *dirwidth.first().unwrap_or(&0);
213    let mut join_set = tokio::task::JoinSet::new();
214    // generate directories and recurse into them
215    for i in 0..numdirs {
216        let path = root.join(format!("dir{i}"));
217        let next_dirwidth = dirwidth[1..].to_vec();
218        let recurse_config = FileGenConfig {
219            root: path.clone(),
220            dirwidth: next_dirwidth,
221            numfiles: *numfiles,
222            filesize: *filesize,
223            writebuf: *writebuf,
224            chunk_size: *chunk_size,
225            leaf_files: *leaf_files,
226        };
227        let recurse = || async move {
228            // Bracket the create_dir metadata syscall with the cwnd permit
229            // + probe so filegen participates in the same adaptive control
230            // loop as copy/rm/link.
231            crate::walk::run_metadata_probed(
232                congestion::Side::Destination,
233                congestion::MetadataOp::MkDir,
234                tokio::fs::create_dir(&path),
235            )
236            .await
237            .with_context(|| format!("Error creating directory {:?}", &path))
238            .map_err(|err| Error::new(err, Default::default()))?;
239            prog_track.directories_created.inc();
240            let dir_summary = Summary {
241                directories_created: 1,
242                ..Default::default()
243            };
244            let recurse_summary = filegen(prog_track, &recurse_config).await?;
245            Ok(dir_summary + recurse_summary)
246        };
247        join_set.spawn(recurse());
248    }
249    // generate files (only if we're not in leaf_files mode, or if we are a leaf directory)
250    // a directory is a leaf when dirwidth is empty (no more subdirectories to create)
251    let is_leaf = dirwidth.is_empty();
252    let should_generate_files = !leaf_files || is_leaf;
253    if should_generate_files {
254        for i in 0..*numfiles {
255            // await the replenished rate token before spawn so an enabled ops throttle paces task
256            // creation at the configured operation rate
257            throttle::get_ops_token().await;
258            let path = root.join(format!("file{i}"));
259            join_set.spawn(write_file(
260                prog_track,
261                path,
262                *filesize,
263                *writebuf,
264                *chunk_size,
265            ));
266        }
267    }
268    let mut success = true;
269    let mut last_error: Option<anyhow::Error> = None;
270    let mut filegen_summary = Summary::default();
271    while let Some(res) = join_set.join_next().await {
272        match res.map_err(|err| Error::new(err.into(), Default::default()))? {
273            Ok(summary) => filegen_summary = filegen_summary + summary,
274            Err(error) => {
275                tracing::error!("filegen: {:?} failed with: {:#}", root, &error);
276                filegen_summary = filegen_summary + error.summary;
277                if last_error.is_none() {
278                    last_error = Some(error.source);
279                }
280                success = false;
281            }
282        }
283    }
284    if !success {
285        let error = if let Some(error) = last_error {
286            error.context(format!("filegen: {:?} failed!", &root))
287        } else {
288            anyhow!("filegen: {:?} failed!", &root)
289        };
290        return Err(Error::new(error, filegen_summary));
291    }
292    Ok(filegen_summary)
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use crate::testutils;
299    use std::os::unix::fs::PermissionsExt;
300    use tracing_test::traced_test;
301
302    static PROGRESS: std::sync::LazyLock<progress::Progress> =
303        std::sync::LazyLock::new(progress::Progress::new);
304
305    #[tokio::test]
306    #[traced_test]
307    async fn test_basic_filegen() -> Result<(), anyhow::Error> {
308        let tmp_dir = testutils::create_temp_dir().await?;
309        let test_path = tmp_dir.as_path();
310        // generate 2 subdirectories with 3 files per directory (including root)
311        let config = FileGenConfig {
312            root: test_path.to_path_buf(),
313            dirwidth: vec![2],
314            numfiles: 3,
315            filesize: 100,
316            writebuf: 50,
317            chunk_size: 0,
318            leaf_files: false,
319        };
320        let summary = filegen(&PROGRESS, &config).await?;
321        // verify summary
322        // files: 3 (in root) + 3 (in dir0) + 3 (in dir1) = 9 files
323        // directories: 2 (dir0, dir1)
324        // bytes: 100 bytes × 9 files = 900 bytes
325        assert_eq!(summary.files_created, 9);
326        assert_eq!(summary.directories_created, 2);
327        assert_eq!(summary.bytes_written, 900);
328        // verify files were actually created
329        assert!(test_path.join("file0").exists()); // root level files
330        assert!(test_path.join("dir0").join("file0").exists());
331        assert!(test_path.join("dir0").join("file1").exists());
332        assert!(test_path.join("dir0").join("file2").exists());
333        assert!(test_path.join("dir1").join("file0").exists());
334        assert!(test_path.join("dir1").join("file1").exists());
335        assert!(test_path.join("dir1").join("file2").exists());
336        // verify file sizes
337        let metadata = tokio::fs::metadata(test_path.join("dir0").join("file0")).await?;
338        assert_eq!(metadata.len(), 100);
339        // cleanup
340        tokio::fs::remove_dir_all(test_path).await?;
341        Ok(())
342    }
343
344    #[tokio::test]
345    #[traced_test]
346    async fn test_nested_filegen() -> Result<(), anyhow::Error> {
347        let tmp_dir = testutils::create_temp_dir().await?;
348        let test_path = tmp_dir.as_path();
349        // generate nested structure: 2 top-level dirs, each with 3 subdirs, 4 files per dir, 50 bytes each
350        let config = FileGenConfig {
351            root: test_path.to_path_buf(),
352            dirwidth: vec![2, 3],
353            numfiles: 4,
354            filesize: 50,
355            writebuf: 25,
356            chunk_size: 0,
357            leaf_files: false,
358        };
359        let summary = filegen(&PROGRESS, &config).await?;
360        // calculate expected values:
361        // directories: 2 top-level + (2 × 3) subdirs = 8 total
362        // files: 4 (in root) + 4×2 (in dir0, dir1) + 4×2×3 (in all leaf dirs) = 4 + 8 + 24 = 36 files
363        // bytes: 50 bytes × 36 files = 1800 bytes
364        assert_eq!(summary.files_created, 36);
365        assert_eq!(summary.directories_created, 8);
366        assert_eq!(summary.bytes_written, 1800);
367        // spot check some files exist
368        assert!(test_path.join("file0").exists()); // root files
369        assert!(test_path.join("dir0").join("file0").exists()); // top-level dir files
370        assert!(test_path.join("dir0").join("dir0").join("file0").exists());
371        assert!(test_path.join("dir0").join("dir2").join("file3").exists());
372        assert!(test_path.join("dir1").join("dir1").join("file2").exists());
373        // cleanup
374        tokio::fs::remove_dir_all(test_path).await?;
375        Ok(())
376    }
377
378    #[tokio::test]
379    #[traced_test]
380    async fn test_deeply_nested_filegen() -> Result<(), anyhow::Error> {
381        let tmp_dir = testutils::create_temp_dir().await?;
382        let test_path = tmp_dir.as_path();
383        // generate 3 levels: 2,2,2 with 2 files each, 10 bytes per file
384        let config = FileGenConfig {
385            root: test_path.to_path_buf(),
386            dirwidth: vec![2, 2, 2],
387            numfiles: 2,
388            filesize: 10,
389            writebuf: 10,
390            chunk_size: 0,
391            leaf_files: false,
392        };
393        let summary = filegen(&PROGRESS, &config).await?;
394        // directories: 2 + (2×2) + (2×2×2) = 2 + 4 + 8 = 14 dirs
395        // files: 2 (root) + 2×2 (level 1) + 2×2×2 (level 2) + 2×2×2×2 (level 3) = 2 + 4 + 8 + 16 = 30 files
396        // bytes: 10 bytes × 30 files = 300 bytes
397        assert_eq!(summary.files_created, 30);
398        assert_eq!(summary.directories_created, 14);
399        assert_eq!(summary.bytes_written, 300);
400        // verify deep nesting works
401        assert!(test_path.join("file0").exists()); // root files
402        assert!(
403            test_path
404                .join("dir0")
405                .join("dir0")
406                .join("dir0")
407                .join("file0")
408                .exists()
409        );
410        assert!(
411            test_path
412                .join("dir1")
413                .join("dir1")
414                .join("dir1")
415                .join("file1")
416                .exists()
417        );
418        // cleanup
419        tokio::fs::remove_dir_all(test_path).await?;
420        Ok(())
421    }
422
423    #[tokio::test]
424    #[traced_test]
425    async fn test_single_file() -> Result<(), anyhow::Error> {
426        let tmp_dir = testutils::create_temp_dir().await?;
427        let test_path = tmp_dir.as_path();
428        // generate just files, no directories
429        let config = FileGenConfig {
430            root: test_path.to_path_buf(),
431            dirwidth: vec![],
432            numfiles: 5,
433            filesize: 200,
434            writebuf: 100,
435            chunk_size: 0,
436            leaf_files: false,
437        };
438        let summary = filegen(&PROGRESS, &config).await?;
439        assert_eq!(summary.files_created, 5);
440        assert_eq!(summary.directories_created, 0);
441        assert_eq!(summary.bytes_written, 1000); // 200 × 5
442        for i in 0..5 {
443            // verify files
444            let file_path = test_path.join(format!("file{i}"));
445            assert!(file_path.exists());
446            let metadata = tokio::fs::metadata(&file_path).await?;
447            assert_eq!(metadata.len(), 200);
448        }
449        // cleanup
450        tokio::fs::remove_dir_all(test_path).await?;
451        Ok(())
452    }
453
454    #[tokio::test]
455    #[traced_test]
456    async fn test_zero_files() -> Result<(), anyhow::Error> {
457        let tmp_dir = testutils::create_temp_dir().await?;
458        let test_path = tmp_dir.as_path();
459        // generate only directories, no files
460        let config = FileGenConfig {
461            root: test_path.to_path_buf(),
462            dirwidth: vec![3, 2],
463            numfiles: 0,
464            filesize: 100,
465            writebuf: 50,
466            chunk_size: 0,
467            leaf_files: false,
468        };
469        let summary = filegen(&PROGRESS, &config).await?;
470        // directories: 3 + (3×2) = 9 dirs
471        assert_eq!(summary.files_created, 0);
472        assert_eq!(summary.directories_created, 9);
473        assert_eq!(summary.bytes_written, 0);
474        // verify directories exist but no files
475        assert!(test_path.join("dir0").join("dir0").exists());
476        assert!(test_path.join("dir2").join("dir1").exists());
477        assert!(!test_path.join("dir0").join("file0").exists());
478        // cleanup
479        tokio::fs::remove_dir_all(test_path).await?;
480        Ok(())
481    }
482
483    #[tokio::test]
484    #[traced_test]
485    async fn test_leaf_files_only() -> Result<(), anyhow::Error> {
486        let tmp_dir = testutils::create_temp_dir().await?;
487        let test_path = tmp_dir.as_path();
488        // generate with leaf_files=true, meaning files only in deepest directories
489        let config = FileGenConfig {
490            root: test_path.to_path_buf(),
491            dirwidth: vec![2, 3],
492            numfiles: 4,
493            filesize: 50,
494            writebuf: 25,
495            chunk_size: 0,
496            leaf_files: true,
497        };
498        let summary = filegen(&PROGRESS, &config).await?;
499        // directories: 2 top-level + (2 × 3) subdirs = 8 total
500        // files: ONLY in leaf dirs (6 leaf dirs) × 4 files each = 24 files
501        // bytes: 50 bytes × 24 files = 1200 bytes
502        assert_eq!(summary.files_created, 24);
503        assert_eq!(summary.directories_created, 8);
504        assert_eq!(summary.bytes_written, 1200);
505        // verify NO files in root or intermediate directories
506        assert!(!test_path.join("file0").exists()); // no root files
507        assert!(!test_path.join("dir0").join("file0").exists()); // no intermediate files
508        assert!(!test_path.join("dir1").join("file0").exists());
509        // verify files ONLY in leaf directories
510        assert!(test_path.join("dir0").join("dir0").join("file0").exists());
511        assert!(test_path.join("dir0").join("dir0").join("file3").exists());
512        assert!(test_path.join("dir0").join("dir2").join("file0").exists());
513        assert!(test_path.join("dir1").join("dir1").join("file0").exists());
514        // cleanup
515        tokio::fs::remove_dir_all(test_path).await?;
516        Ok(())
517    }
518
519    #[tokio::test]
520    #[traced_test]
521    async fn test_permission_error_includes_root_cause() -> Result<(), anyhow::Error> {
522        let tmp_dir = testutils::create_temp_dir().await?;
523        let root = tmp_dir.join("readonly");
524        tokio::fs::create_dir(&root).await?;
525        tokio::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o555)).await?;
526
527        let config = FileGenConfig {
528            root: root.clone(),
529            dirwidth: Vec::new(),
530            numfiles: 1,
531            filesize: 10,
532            writebuf: 10,
533            chunk_size: 0,
534            leaf_files: false,
535        };
536        let result = filegen(&PROGRESS, &config).await;
537
538        // restore permissions to allow cleanup
539        tokio::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o755)).await?;
540
541        assert!(
542            result.is_err(),
543            "filegen inside read-only directory should fail"
544        );
545        let err = result.unwrap_err();
546        let err_msg = format!("{:#}", err.source);
547        assert!(
548            err_msg.to_lowercase().contains("permission denied") || err_msg.contains("EACCES"),
549            "Error message must include permission denied text. Got: {}",
550            err_msg
551        );
552        Ok(())
553    }
554
555    mod max_files_in_flight_tests {
556        use super::*;
557
558        static CANCELLATION_PROGRESS: std::sync::LazyLock<progress::Progress> =
559            std::sync::LazyLock::new(progress::Progress::new);
560        static REUSE_PROGRESS: std::sync::LazyLock<progress::Progress> =
561            std::sync::LazyLock::new(progress::Progress::new);
562
563        #[tokio::test(flavor = "current_thread")]
564        async fn write_file_reuses_one_blocking_buffer_across_chunks() -> anyhow::Result<()> {
565            let root = testutils::create_temp_dir().await?;
566            let path = root.join("reused-buffer");
567            let bufsize = 4096;
568            let filesize = bufsize * 3;
569            let admission = testutils::AdmissionLimit::new().await;
570            admission.set_files_in_flight(1);
571            let gate = testutils::BlockingPathGate::install(path.clone());
572            gate.release_all();
573            let timeout = std::time::Duration::from_secs(20);
574            let runtime_thread = std::thread::current().id();
575
576            let write_result = match admission
577                .run_with_timeout(
578                    timeout,
579                    write_file(&REUSE_PROGRESS, path.clone(), filesize, bufsize, 0),
580                )
581                .await
582            {
583                Ok(Ok(summary)) => Ok(summary),
584                Ok(Err(error)) => Err(error.source),
585                Err(error) => Err(anyhow::Error::new(error)
586                    .context("multi-chunk write_file did not finish in time")),
587            };
588            let hit_count = gate.hit_count();
589            let allocation_count = gate.allocation_count();
590            let allocation_thread = gate.allocation_thread();
591            let final_len = tokio::fs::metadata(&path).await.map(|meta| meta.len());
592            drop(gate);
593            drop(admission);
594            let cleanup_result = tokio::fs::remove_dir_all(root).await;
595
596            let summary = write_result?;
597            let final_len = final_len?;
598            cleanup_result?;
599            assert_eq!(hit_count, 3, "write_file did not submit one job per chunk");
600            assert_eq!(
601                allocation_count, 1,
602                "write_file allocated a new buffer instead of reusing the returned one"
603            );
604            assert_ne!(
605                allocation_thread,
606                Some(runtime_thread),
607                "filegen allocated its user-sized buffer on the async runtime thread"
608            );
609            assert_eq!(summary.files_created, 1);
610            assert_eq!(summary.bytes_written, filesize as u64);
611            assert_eq!(final_len, filesize as u64);
612            Ok(())
613        }
614
615        #[tokio::test(flavor = "current_thread")]
616        async fn cancelled_write_file_stops_after_one_buffer_and_retains_capacity()
617        -> anyhow::Result<()> {
618            let root = testutils::create_temp_dir().await?;
619            let path = root.join("cancelled-file");
620            let bufsize = 4096;
621            let filesize = bufsize * 3;
622            let gate = testutils::BlockingPathGate::install(path.clone());
623            let admission = testutils::AdmissionLimit::new().await;
624            admission.set_files_in_flight(1);
625            let timeout = std::time::Duration::from_secs(20);
626            let runtime_thread = std::thread::current().id();
627            let bytes_before = CANCELLATION_PROGRESS.bytes_copied.get();
628            let files_before = CANCELLATION_PROGRESS.files_copied.get();
629            let task = tokio::spawn(write_file(
630                &CANCELLATION_PROGRESS,
631                path.clone(),
632                filesize,
633                bufsize,
634                0,
635            ));
636            let observations =
637                testutils::cancel_at_blocking_path(admission, gate, task, timeout, |_| {
638                    (
639                        CANCELLATION_PROGRESS.bytes_copied.get(),
640                        CANCELLATION_PROGRESS.files_copied.get(),
641                    )
642                })
643                .await;
644            let final_len = tokio::fs::metadata(&path).await.map(|meta| meta.len());
645            let final_bytes = CANCELLATION_PROGRESS.bytes_copied.get();
646            let final_files = CANCELLATION_PROGRESS.files_copied.get();
647            let cleanup_result = tokio::fs::remove_dir_all(root).await;
648
649            let (observations, (bytes_at_cancellation, files_at_cancellation)) = observations?;
650            let final_len = final_len?;
651            cleanup_result?;
652
653            assert!(observations.waiter_was_cancelled);
654            assert!(
655                observations.admission_was_retained_while_work_gated,
656                "cancelling write_file released capacity while its fd-owning job was live"
657            );
658            assert!(
659                observations.fd_was_open_while_work_gated,
660                "write_file closed its fd while the job was gated"
661            );
662            assert_eq!(observations.hit_count_before_release, 1);
663            assert_eq!(
664                observations.allocation_count_before_release, 0,
665                "filegen allocated its user-sized buffer before the admitted work gate released"
666            );
667            assert_eq!(bytes_at_cancellation, bytes_before);
668            assert_eq!(files_at_cancellation, files_before);
669            assert!(
670                observations.fd_was_closed_at_output_drop_start,
671                "write_file reached its output-drop boundary before its destination fd closed"
672            );
673            assert!(
674                observations.admission_was_retained_at_output_drop_start,
675                "write_file released capacity before its abandoned output dropped"
676            );
677            assert_eq!(
678                observations.final_hit_count, 1,
679                "cancellation submitted another buffer"
680            );
681            assert_eq!(
682                observations.final_allocation_count, 1,
683                "filegen did not allocate exactly one reusable buffer in admitted blocking work"
684            );
685            assert_ne!(
686                observations.allocation_thread,
687                Some(runtime_thread),
688                "filegen allocated its user-sized buffer on the async runtime thread"
689            );
690            assert_eq!(
691                final_len, bufsize as u64,
692                "cancellation must finish at most the already-submitted buffer"
693            );
694            assert_eq!(
695                final_bytes, bytes_before,
696                "an abandoned buffer must not advance byte progress"
697            );
698            assert_eq!(
699                final_files, files_before,
700                "a cancelled file must not be counted complete"
701            );
702            Ok(())
703        }
704    }
705}