756-tempfile-testing — Tempfile Testing
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
TempDir RAII type that creates a unique temporary directory on constructionDrop to recursively delete the directory after the test completesTempDir for realistic filesystem testingCode Example
pub struct TempDir {
path: PathBuf,
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}Key Differences
TempDir::Drop guarantees cleanup even on panic; OCaml requires explicit try ... finally or a with_temp_dir bracket function.Filename.temp_file uses a similar internal counter.tempfile crate is the standard — it uses OS-provided secure temp creation; OCaml uses Filename.temp_dir from stdlib.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);
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Cleanup | Fun.protect ~finally | Drop trait |
| Temp path | Filename.temp_dir | std::env::temp_dir() |
| File I/O | Out_channel | std::fs |
| Uniqueness | Random suffix | pid + timestamp |
Exercises
TempDir with a create_subdir(name) method that creates a subdirectory and returns its path, for testing code that reads from a directory tree.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.TempDir/config.toml file, including testing error behavior when the file is missing or malformed.