๐Ÿฆ€ Functional Rust

756: Testing with Temporary Files and Directories

Difficulty: 2 Level: Intermediate RAII temporary directories for filesystem tests โ€” automatic cleanup even on panic, with per-test isolation.

The Problem This Solves

Functions that read from or write to disk require a real filesystem to test properly. But tests shouldn't leave debris: leftover files in `/tmp` accumulate, pollute other test runs, and can cause flaky failures when a test assumes a clean directory. The solution is a `TempDir` struct: it creates a unique directory in `new()`, your test uses it, and `Drop` removes it when the test ends โ€” even if the test panics. This is the RAII pattern applied to filesystem resources. The same approach powers the `tempfile` crate (the standard library for this in production Rust), but understanding the hand-rolled version makes the pattern clear. It's also more portable across environments where you can't add dependencies. Every test gets its own unique directory (using `AtomicU64` + process ID), so parallel test execution (`cargo test -- --test-threads 4`) never has contention. The test writes into its own isolated space, and cleanup is automatic.

The Intuition

A `TempDir` struct holds a `PathBuf`. `new()` creates a directory at a unique path under `std::env::temp_dir()`. `path()` and `child()` return paths inside it. `Drop::drop()` calls `fs::remove_dir_all()` โ€” the equivalent of `rm -rf`. Since `Drop` runs on scope exit and on panic, you get guaranteed cleanup with no try/finally needed.

How It Works in Rust

static COUNTER: AtomicU64 = AtomicU64::new(0);

pub struct TempDir { path: PathBuf }

impl TempDir {
 pub fn new() -> io::Result<Self> {
     // Unique name: combines process ID + atomic counter
     let id = COUNTER.fetch_add(1, Ordering::Relaxed);
     let name = format!("rust_test_{}_{}", std::process::id(), id);
     let path = std::env::temp_dir().join(name);
     fs::create_dir_all(&path)?;
     Ok(TempDir { path })
 }

 pub fn path(&self) -> &Path { &self.path }
 pub fn child(&self, name: &str) -> PathBuf { self.path.join(name) }
}

impl Drop for TempDir {
 fn drop(&mut self) {
     if self.path.exists() {
         let _ = fs::remove_dir_all(&self.path);  // ignore errors in Drop
     }
 }
}

// Using it in a test
#[test]
fn test_write_and_read() {
 let dir = TempDir::new().unwrap();        // creates /tmp/rust_test_12345_0/
 let file = dir.child("data.txt");

 write_lines(&file, &["line1", "line2"]).unwrap();
 assert_eq!(count_lines(&file).unwrap(), 2);
 // dir drops here โ†’ /tmp/rust_test_12345_0/ deleted automatically
}

#[test]
fn cleanup_on_drop() {
 let path = {
     let dir = TempDir::new().unwrap();
     let p = dir.path().to_path_buf();
     assert!(p.exists());
     p   // dir dropped here
 };
 assert!(!path.exists()); // directory is gone
}
`let _ = fs::remove_dir_all(...)` in `Drop` ignores errors โ€” it's considered bad practice to panic in `Drop`, since panicking during a panic causes abort.

What This Unlocks

Key Differences

ConceptOCamlRust
Temp directory`Filename.temp_dir` + manual cleanup`TempDir` struct with `Drop` โ€” automatic
Unique ID generation`Unix.getpid ()` + counter`std::process::id()` + `AtomicU64`
Panic-safe cleanup`Fun.protect ~finally:``Drop` โ€” always runs, no special syntax
Ignore errors in Drop`try _ with _ -> ()``let _ = expr;` โ€” explicit discard
/// 756: Testing with Temporary Files and Directories
/// RAII TempDir: auto-cleanup even on panic.

use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

// โ”€โ”€ TempDir: RAII temp directory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

static COUNTER: AtomicU64 = AtomicU64::new(0);

pub struct TempDir {
    path: PathBuf,
}

impl TempDir {
    /// Create a new unique temp directory.
    pub fn new() -> io::Result<Self> {
        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
        let name = format!("rust_test_{}_{}", std::process::id(), id);
        let path = std::env::temp_dir().join(name);
        fs::create_dir_all(&path)?;
        Ok(TempDir { path })
    }

    /// The path to this temp directory.
    pub fn path(&self) -> &Path { &self.path }

    /// Create a file inside this temp dir.
    pub fn child(&self, name: &str) -> PathBuf {
        self.path.join(name)
    }

    /// Create a subdirectory inside this temp dir.
    pub fn subdir(&self, name: &str) -> io::Result<PathBuf> {
        let p = self.path.join(name);
        fs::create_dir_all(&p)?;
        Ok(p)
    }
}

impl Drop for TempDir {
    /// Cleanup runs automatically โ€” even on panic.
    fn drop(&mut self) {
        if self.path.exists() {
            let _ = fs::remove_dir_all(&self.path);
        }
    }
}

// โ”€โ”€ Functions under test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub fn write_lines(path: &Path, lines: &[&str]) -> io::Result<()> {
    let content = lines.join("\n");
    fs::write(path, content)
}

pub fn count_lines(path: &Path) -> io::Result<usize> {
    let content = fs::read_to_string(path)?;
    Ok(content.lines().count())
}

pub fn append_line(path: &Path, line: &str) -> io::Result<()> {
    use std::io::Write;
    let mut f = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)?;
    writeln!(f, "{}", line)
}

pub fn copy_file(src: &Path, dst: &Path) -> io::Result<u64> {
    fs::copy(src, dst)
}

fn main() {
    let dir = TempDir::new().expect("failed to create temp dir");
    println!("Temp dir: {}", dir.path().display());

    let file = dir.child("hello.txt");
    write_lines(&file, &["Hello", "World"]).unwrap();
    println!("Lines: {}", count_lines(&file).unwrap());

    append_line(&file, "Goodbye").unwrap();
    println!("After append: {}", count_lines(&file).unwrap());

    println!("TempDir will be cleaned up when dir goes out of scope.");
    // dir dropped here โ†’ cleanup
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn temp_dir_is_created() {
        let dir = TempDir::new().unwrap();
        assert!(dir.path().exists(), "temp dir should exist");
        assert!(dir.path().is_dir(), "should be a directory");
    }

    #[test]
    fn temp_dir_cleaned_up_on_drop() {
        let path = {
            let dir = TempDir::new().unwrap();
            let p = dir.path().to_path_buf();
            assert!(p.exists());
            p   // dir dropped here
        };
        assert!(!path.exists(), "temp dir should be removed after drop");
    }

    #[test]
    fn write_and_read_file() {
        let dir = TempDir::new().unwrap();
        let file = dir.child("data.txt");

        write_lines(&file, &["line1", "line2", "line3"]).unwrap();

        let content = fs::read_to_string(&file).unwrap();
        assert!(content.contains("line1"));
        assert!(content.contains("line3"));
    }

    #[test]
    fn count_lines_correct() {
        let dir = TempDir::new().unwrap();
        let file = dir.child("lines.txt");
        write_lines(&file, &["a", "b", "c", "d"]).unwrap();
        assert_eq!(count_lines(&file).unwrap(), 4);
    }

    #[test]
    fn append_increases_line_count() {
        let dir = TempDir::new().unwrap();
        let file = dir.child("append.txt");
        write_lines(&file, &["first"]).unwrap();
        append_line(&file, "second").unwrap();
        append_line(&file, "third").unwrap();
        assert_eq!(count_lines(&file).unwrap(), 3);
    }

    #[test]
    fn copy_file_creates_duplicate() {
        let dir = TempDir::new().unwrap();
        let src = dir.child("src.txt");
        let dst = dir.child("dst.txt");

        fs::write(&src, "content").unwrap();
        copy_file(&src, &dst).unwrap();

        assert!(dst.exists());
        assert_eq!(
            fs::read_to_string(&src).unwrap(),
            fs::read_to_string(&dst).unwrap()
        );
    }

    #[test]
    fn subdir_is_created() {
        let dir = TempDir::new().unwrap();
        let sub = dir.subdir("nested/deep").unwrap();
        assert!(sub.exists());
        assert!(sub.is_dir());
    }

    #[test]
    fn each_test_has_isolated_temp_dir() {
        let d1 = TempDir::new().unwrap();
        let d2 = TempDir::new().unwrap();
        assert_ne!(d1.path(), d2.path(), "each TempDir should be unique");
    }
}
(* 756: Tempfile Testing โ€” OCaml *)

let unique_id =
  let counter = ref 0 in
  fun () -> incr counter; !counter

let create_temp_dir () =
  let tmp = Filename.get_temp_dir_name () in
  let name = Printf.sprintf "ocaml_test_%d_%d"
    (Unix.getpid ()) (unique_id ()) in
  let path = Filename.concat tmp name in
  Unix.mkdir path 0o700;
  path

let remove_dir_all path =
  (* Recursively remove directory *)
  let rec rm p =
    let entries = Sys.readdir p in
    Array.iter (fun entry ->
      let full = Filename.concat p entry in
      if Sys.is_directory full then rm full
      else Sys.remove full
    ) entries;
    Unix.rmdir p
  in
  if Sys.file_exists path then rm path

let with_temp_dir f =
  let dir = create_temp_dir () in
  let result = (try Ok (f dir) with e -> Error e) in
  remove_dir_all dir;
  match result with
  | Ok v  -> v
  | Error e -> raise e

let write_file path content =
  let oc = open_out path in
  output_string oc content;
  close_out oc

let read_file path =
  let ic = open_in path in
  let n = in_channel_length ic in
  let s = Bytes.create n in
  really_input ic s 0 n;
  close_in ic;
  Bytes.to_string s

let () =
  (* Test: write and read a file in temp dir *)
  with_temp_dir (fun dir ->
    let file = Filename.concat dir "test.txt" in
    write_file file "Hello, tempfile!";
    let content = read_file file in
    assert (content = "Hello, tempfile!");
    Printf.printf "Temp dir test passed: %s\n" dir
  );

  (* Dir should be cleaned up *)
  Printf.printf "Cleanup: temp dirs removed\n"