rust

Setting base path in rust integration test


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.


Solution

  • 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(())
    }