apply-patch/tests/suite/scenarios.rs
126 lines
use codex_utils_cargo_bin::repo_root;use pretty_assertions::assert_eq;use std::collections::BTreeMap;use std::fs;use std::path::Path;use std::path::PathBuf;use std::process::Command;use tempfile::tempdir;#[test]fn test_apply_patch_scenarios() -> anyhow::Result<()> { let scenarios_dir = repo_root()? .join("codex-rs") .join("apply-patch") .join("tests") .join("fixtures") .join("scenarios"); for scenario in fs::read_dir(scenarios_dir)? { let scenario = scenario?; let path = scenario.path(); if path.is_dir() { run_apply_patch_scenario(&path)?; } } Ok(())}/// Reads a scenario directory, copies the input files to a temporary directory, runs apply-patch,/// and asserts that the final state matches the expected state exactly.fn run_apply_patch_scenario(dir: &Path) -> anyhow::Result<()> { let tmp = tempdir()?; // Copy the input files to the temporary directory let input_dir = dir.join("input"); if input_dir.is_dir() { copy_dir_recursive(&input_dir, tmp.path())?; } // Read the patch.txt file let patch = fs::read_to_string(dir.join("patch.txt"))?; // Run apply_patch in the temporary directory. We intentionally do not assert // on the exit status here; the scenarios are specified purely in terms of // final filesystem state, which we compare below. Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?) .arg(patch) .current_dir(tmp.path()) .output()?; // Assert that the final state matches the expected state exactly let expected_dir = dir.join("expected"); let expected_snapshot = snapshot_dir(&expected_dir)?; let actual_snapshot = snapshot_dir(tmp.path())?; assert_eq!( actual_snapshot, expected_snapshot, "Scenario {} did not match expected final state", dir.display() ); Ok(())}#[derive(Debug, Clone, PartialEq, Eq)]enum Entry { File(Vec<u8>), Dir,}fn snapshot_dir(root: &Path) -> anyhow::Result<BTreeMap<PathBuf, Entry>> { let mut entries = BTreeMap::new(); if root.is_dir() { snapshot_dir_recursive(root, root, &mut entries)?; } Ok(entries)}fn snapshot_dir_recursive( base: &Path, dir: &Path, entries: &mut BTreeMap<PathBuf, Entry>,) -> anyhow::Result<()> { for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); let Some(stripped) = path.strip_prefix(base).ok() else { continue; }; let rel = stripped.to_path_buf(); // Under Buck2, files in `__srcs` are often materialized as symlinks. // Use `metadata()` (follows symlinks) so our fixture snapshots work // under both Cargo and Buck2. let metadata = fs::metadata(&path)?; if metadata.is_dir() { entries.insert(rel.clone(), Entry::Dir); snapshot_dir_recursive(base, &path, entries)?; } else if metadata.is_file() { let contents = fs::read(&path)?; entries.insert(rel, Entry::File(contents)); } } Ok(())}fn copy_dir_recursive(src: &Path, dst: &Path) -> anyhow::Result<()> { for entry in fs::read_dir(src)? { let entry = entry?; let path = entry.path(); let dest_path = dst.join(entry.file_name()); // See note in `snapshot_dir_recursive` about Buck2 symlink trees. let metadata = fs::metadata(&path)?; if metadata.is_dir() { fs::create_dir_all(&dest_path)?; copy_dir_recursive(&path, &dest_path)?; } else if metadata.is_file() { if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent)?; } fs::copy(&path, &dest_path)?; } } Ok(())}