mirror of
https://github.com/openai/codex.git
synced 2026-09-11 20:36:49 +00:00
## What changed - Add the `apply_patch_preserve_line_endings` feature, disabled by default, to preserve CRLF, CR, and mixed line endings when `apply_patch` updates files. - Apply the feature consistently to built-in patch handling and patches invoked through shell, user-shell, unified-exec, and app-server command execution. - Keep the active feature configuration authoritative over inherited, shell snapshot, and client-provided environment values. ## Testing - Cover line-ending behavior with the feature enabled and disabled for custom tool calls, shell heredocs, command execution, and the `apply_patch` CLI. GitOrigin-RevId: 531a7c66761959c650270559f57941929f03e6c4
134 lines
4.3 KiB
Rust
134 lines
4.3 KiB
Rust
use anyhow::Context;
|
|
use codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR;
|
|
use codex_utils_cargo_bin::find_resource;
|
|
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_marker = find_resource!("tests/fixtures/scenarios/.gitattributes")?;
|
|
let scenarios_dir = scenarios_marker
|
|
.parent()
|
|
.context("scenario marker should have a parent directory")?;
|
|
for scenario in fs::read_dir(scenarios_dir)
|
|
.with_context(|| format!("failed to read {}", scenarios_dir.display()))?
|
|
{
|
|
let scenario = scenario?;
|
|
let path = scenario.path();
|
|
if path.is_dir() {
|
|
run_apply_patch_scenario(&path)
|
|
.with_context(|| format!("failed to run scenario {}", path.display()))?;
|
|
}
|
|
}
|
|
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_path = dir.join("patch.txt");
|
|
let patch = fs::read_to_string(&patch_path)
|
|
.with_context(|| format!("failed to read {}", patch_path.display()))?;
|
|
|
|
// 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)
|
|
.env(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR, "1")
|
|
.current_dir(tmp.path())
|
|
.output()
|
|
.with_context(|| format!("failed to run scenario {}", dir.display()))?;
|
|
|
|
// 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(())
|
|
}
|