ExamplesBy LevelBy TopicLearning Paths
756 Fundamental

756-tempfile-testing — Tempfile Testing

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "756-tempfile-testing — Tempfile Testing" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Code that reads or writes files cannot be tested with in-memory mocks alone — you need real filesystem semantics. Key difference from OCaml: 1. **Cleanup guarantee**: Rust's `TempDir::Drop` guarantees cleanup even on panic; OCaml requires explicit `try ... finally` or a `with_temp_dir` bracket function.

Tutorial

The Problem

Code that reads or writes files cannot be tested with in-memory mocks alone — you need real filesystem semantics. Temporary directories solve this: they provide real file paths, get cleaned up on test completion, and are isolated per test process. Without proper cleanup, failing tests leave debris in /tmp that accumulates over time. The Drop-based TempDir type ensures cleanup even when tests panic.

🎯 Learning Outcomes

  • • Implement a TempDir RAII type that creates a unique temporary directory on construction
  • • Use Drop to recursively delete the directory after the test completes
  • • Create and read files within the TempDir for realistic filesystem testing
  • • Generate unique directory names using PID + nanosecond timestamp to avoid collisions
  • • Test file-processing code (CSV reading, log rotation, config loading) against real files
  • Code Example

    pub struct TempDir {
        path: PathBuf,
    }
    
    impl Drop for TempDir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.path);
        }
    }

    Key Differences

  • Cleanup guarantee: Rust's TempDir::Drop guarantees cleanup even on panic; OCaml requires explicit try ... finally or a with_temp_dir bracket function.
  • Uniqueness: Rust uses PID + nanoseconds; OCaml's Filename.temp_file uses a similar internal counter.
  • Ecosystem: Rust's tempfile crate is the standard — it uses OS-provided secure temp creation; OCaml uses Filename.temp_dir from stdlib.
  • Parallel tests: Rust's parallel tests each get their own TempDir with unique names; OCaml's sequential tests can reuse the same named temp dir safely.
  • OCaml Approach

    OCaml's Filename.temp_dir and Filename.temp_file create temporary files. Cleanup requires explicit Sys.remove or FileUtil.rm from the fileutils library. Jane Street's Core.Unix provides with_temp_dir as a bracket that guarantees cleanup even on exception. The Bos library wraps filesystem operations with typed paths and safer error handling.

    Full Source

    #![allow(clippy::all)]
    //! # Tempfile Testing
    //!
    //! Testing with temporary files and directories.
    
    use std::fs::{self, File};
    use std::io::{Read, Write};
    use std::path::{Path, PathBuf};
    
    /// A temporary directory that cleans up on drop
    pub struct TempDir {
        path: PathBuf,
    }
    
    impl TempDir {
        /// Create a new temporary directory
        pub fn new(prefix: &str) -> std::io::Result<Self> {
            let path = std::env::temp_dir().join(format!(
                "{}-{}-{}",
                prefix,
                std::process::id(),
                std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .unwrap()
                    .as_nanos()
            ));
            fs::create_dir_all(&path)?;
            Ok(TempDir { path })
        }
    
        /// Get the path to the temp directory
        pub fn path(&self) -> &Path {
            &self.path
        }
    
        /// Create a file in the temp directory
        pub fn create_file(&self, name: &str, content: &str) -> std::io::Result<PathBuf> {
            let file_path = self.path.join(name);
            let mut file = File::create(&file_path)?;
            file.write_all(content.as_bytes())?;
            Ok(file_path)
        }
    
        /// Read a file from the temp directory
        pub fn read_file(&self, name: &str) -> std::io::Result<String> {
            let file_path = self.path.join(name);
            let mut content = String::new();
            File::open(&file_path)?.read_to_string(&mut content)?;
            Ok(content)
        }
    }
    
    impl Drop for TempDir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.path);
        }
    }
    
    /// Process a config file
    pub fn process_config(path: &Path) -> std::io::Result<Vec<(String, String)>> {
        let content = fs::read_to_string(path)?;
        let mut pairs = Vec::new();
        for line in content.lines() {
            if let Some((key, value)) = line.split_once('=') {
                pairs.push((key.trim().to_string(), value.trim().to_string()));
            }
        }
        Ok(pairs)
    }
    
    /// Write a config file
    pub fn write_config(path: &Path, pairs: &[(String, String)]) -> std::io::Result<()> {
        let content: String = pairs
            .iter()
            .map(|(k, v)| format!("{}={}\n", k, v))
            .collect();
        fs::write(path, content)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_temp_dir_created() {
            let temp = TempDir::new("test").unwrap();
            assert!(temp.path().exists());
        }
    
        #[test]
        fn test_temp_dir_cleanup() {
            let path = {
                let temp = TempDir::new("cleanup").unwrap();
                temp.path().to_path_buf()
            };
            // After drop, directory should be gone
            assert!(!path.exists());
        }
    
        #[test]
        fn test_create_and_read_file() {
            let temp = TempDir::new("rw").unwrap();
            temp.create_file("test.txt", "Hello, World!").unwrap();
            let content = temp.read_file("test.txt").unwrap();
            assert_eq!(content, "Hello, World!");
        }
    
        #[test]
        fn test_process_config() {
            let temp = TempDir::new("config").unwrap();
            let path = temp
                .create_file("config.ini", "key1=value1\nkey2=value2\n")
                .unwrap();
            let pairs = process_config(&path).unwrap();
            assert_eq!(pairs.len(), 2);
            assert_eq!(pairs[0], ("key1".to_string(), "value1".to_string()));
        }
    
        #[test]
        fn test_write_config() {
            let temp = TempDir::new("write").unwrap();
            let path = temp.path().join("out.ini");
            let pairs = vec![
                ("a".to_string(), "1".to_string()),
                ("b".to_string(), "2".to_string()),
            ];
            write_config(&path, &pairs).unwrap();
            let content = fs::read_to_string(&path).unwrap();
            assert!(content.contains("a=1"));
            assert!(content.contains("b=2"));
        }
    
        #[test]
        fn test_roundtrip() {
            let temp = TempDir::new("roundtrip").unwrap();
            let path = temp.path().join("config.ini");
            let original = vec![
                ("host".to_string(), "localhost".to_string()),
                ("port".to_string(), "8080".to_string()),
            ];
            write_config(&path, &original).unwrap();
            let loaded = process_config(&path).unwrap();
            assert_eq!(original, loaded);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_temp_dir_created() {
            let temp = TempDir::new("test").unwrap();
            assert!(temp.path().exists());
        }
    
        #[test]
        fn test_temp_dir_cleanup() {
            let path = {
                let temp = TempDir::new("cleanup").unwrap();
                temp.path().to_path_buf()
            };
            // After drop, directory should be gone
            assert!(!path.exists());
        }
    
        #[test]
        fn test_create_and_read_file() {
            let temp = TempDir::new("rw").unwrap();
            temp.create_file("test.txt", "Hello, World!").unwrap();
            let content = temp.read_file("test.txt").unwrap();
            assert_eq!(content, "Hello, World!");
        }
    
        #[test]
        fn test_process_config() {
            let temp = TempDir::new("config").unwrap();
            let path = temp
                .create_file("config.ini", "key1=value1\nkey2=value2\n")
                .unwrap();
            let pairs = process_config(&path).unwrap();
            assert_eq!(pairs.len(), 2);
            assert_eq!(pairs[0], ("key1".to_string(), "value1".to_string()));
        }
    
        #[test]
        fn test_write_config() {
            let temp = TempDir::new("write").unwrap();
            let path = temp.path().join("out.ini");
            let pairs = vec![
                ("a".to_string(), "1".to_string()),
                ("b".to_string(), "2".to_string()),
            ];
            write_config(&path, &pairs).unwrap();
            let content = fs::read_to_string(&path).unwrap();
            assert!(content.contains("a=1"));
            assert!(content.contains("b=2"));
        }
    
        #[test]
        fn test_roundtrip() {
            let temp = TempDir::new("roundtrip").unwrap();
            let path = temp.path().join("config.ini");
            let original = vec![
                ("host".to_string(), "localhost".to_string()),
                ("port".to_string(), "8080".to_string()),
            ];
            write_config(&path, &original).unwrap();
            let loaded = process_config(&path).unwrap();
            assert_eq!(original, loaded);
        }
    }

    Deep Comparison

    OCaml vs Rust: Tempfile Testing

    RAII Temp Directory

    Rust

    pub struct TempDir {
        path: PathBuf,
    }
    
    impl Drop for TempDir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.path);
        }
    }
    

    OCaml

    let with_temp_dir f =
      let dir = Filename.temp_dir "test" "" in
      Fun.protect ~finally:(fun () -> 
        Sys.command (Printf.sprintf "rm -rf %s" dir) |> ignore
      ) (fun () -> f dir)
    

    Creating Test Files

    Rust

    let temp = TempDir::new("test").unwrap();
    let path = temp.create_file("config.ini", "key=value\n").unwrap();
    

    OCaml

    let () = with_temp_dir (fun dir ->
      let path = Filename.concat dir "config.ini" in
      Out_channel.write_all path ~data:"key=value\n"
    )
    

    Key Differences

    AspectOCamlRust
    CleanupFun.protect ~finallyDrop trait
    Temp pathFilename.temp_dirstd::env::temp_dir()
    File I/OOut_channelstd::fs
    UniquenessRandom suffixpid + timestamp

    Exercises

  • Extend TempDir with a create_subdir(name) method that creates a subdirectory and returns its path, for testing code that reads from a directory tree.
  • Implement a log_rotation_test that creates 5 log files, runs a rotation function (rename oldest, create new), and verifies the final directory contains exactly 5 files with correct names.
  • Write a test for a config file parser that loads from a real TempDir/config.toml file, including testing error behavior when the file is missing or malformed.
  • Open Source Repos