I'm trying to put together a small program, written in Rust, that accepts a glob like "**/*.json" and returns all files that match it in the current directory.
I wish to write an integration test for this program, but I'm not sure how. The problem is that the test is always executed in the current project directory, while I want to use tempfile
to create a temporary folder and set it up according to the test's requirements.
Here is what I've got (some parts omitted for brevity):
// ./src/main.rs
use anyhow::*;
use clap::Parser;
use glob::{MatchOptions, Pattern};
use ignore::WalkBuilder;
use std::path::Path;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// File patterns to process
#[arg(required = true)]
include: Vec<PathBuf>,
}
fn main() {
let args = Args::parse();
let files = list_files(args.include);
println!("Found {:?} file(s).", files.unwrap());
}
fn list_files(include_patterns: Vec<PathBuf>) -> Result<Vec<PathBuf>> {
let mut walk = WalkBuilder::new(".");
walk.hidden(true).ignore(true).git_global(true);
let include_patterns: Vec<Pattern> = create_patterns(include_patterns)?;
let mut matching_files = Vec::new();
for entry in walk.build() {
let entry = entry.context("Failed to read directory entry")?;
if entry.file_type().map_or(false, |ft| ft.is_file()) {
let path = entry.path();
let relative_path = path.strip_prefix(".").unwrap_or(path);
// Create both versions of the path for matching
let relative_path_with_dot = PathBuf::from(".").join(relative_path);
if matches_patterns(&include_patterns, &relative_path_with_dot)
|| matches_patterns(&include_patterns, relative_path)
{
matching_files.push(path.to_path_buf());
}
}
}
Ok(matching_files)
}
fn matches_patterns(patterns: &[Pattern], path: &Path) -> bool {
let options = MatchOptions {
case_sensitive: false,
require_literal_separator: false,
require_literal_leading_dot: false,
};
patterns
.iter()
.any(|pattern| pattern.matches_path_with(path, options))
}
fn create_patterns(patterns: Vec<PathBuf>) -> Result<Vec<Pattern>> {
patterns
.into_iter()
.map(|path| {
let pattern_str = path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid pattern path"))?
.trim_matches('"'); // Remove surrounding quotes if present
Pattern::new(pattern_str).map_err(|e| e.into())
})
.collect()
}
And the test:
// ./tests/cli.rs
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::fs::{self};
use std::process::Command;
use tempfile::TempDir;
#[test]
fn correctly_formats_single_file() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
fs::write(
temp_path.join("sample.json"),
r#"
{
"c": 3,
"b": 2,
"a": 1
}"#,
)
.unwrap();
let mut cmd = Command::cargo_bin("repro")?;
cmd.arg("./sample.json");
cmd.assert()
.success()
.stdout(predicate::str::contains("Processed 1 file(s) in"));
Ok(())
}
Cargo.toml:
[package]
name = "repro"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.0", features = ["derive"] }
glob = "0.3"
ignore = "0.4"
anyhow = "1.0"
[dev-dependencies]
tempfile = "3.2"
assert_cmd = "2.0.14"
predicates = "3.1.0"
This is a simplified version of what I want to do, but the gist is that I want to write a test that sets up a file and asserts that the program can find it, and then write another test that sets up a file but passes a pattern that excludes it and asserts it's correctly excluded.
One thing I did try was to use set_current_dir
, which works, but different tests that do that are affecting each other's result.
Well, don't mind me. I pulled a classing by not reading the docs. assert_cmd has a current_dir
method...
Here is how the tests looks now:
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::fs::{self};
use std::process::Command;
use tempfile::TempDir;
#[test]
fn correctly_formats_single_file() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
fs::write(
temp_path.join("sample.json"),
r#"
{
"c": 3,
"b": 2,
"a": 1
}"#,
)
.unwrap();
let mut cmd = Command::cargo_bin("repro")?;
cmd.arg("./sample.json").current_dir(temp_path);
cmd.assert()
.success()
.stdout(predicate::str::contains("Processed 1 file(s) in"));
Ok(())
}