package patch import ( "math/rand" "os" "os/exec" "path/filepath" "strings" "testing" ) func roundTrip(t *testing.T, name, old, new string) { t.Helper() d, err := Unified("a/f", "b/f", old, new, 3) if err != nil { t.Fatalf("%s: Unified: %v", name, err) } if old == new { if d != "" { t.Errorf("%s: identical inputs must give empty diff", name) } return } fps, err := Parse(d) if err != nil { t.Fatalf("%s: Parse: %v\n%s", name, err, d) } got, err := fps[0].Apply(old) if err != nil { t.Fatalf("%s: Apply: %v\n%s", name, err, d) } if got != new { t.Fatalf("%s: round trip mismatch\nwant %q\ngot %q\ndiff:\n%s", name, new, got, d) } } func TestRoundTripCases(t *testing.T) { cases := map[string][2]string{ "change middle": {"a\nb\nc\nd\ne\n", "a\nb\nX\nd\ne\n"}, "insert start": {"a\nb\n", "new\na\nb\n"}, "insert end": {"a\nb\n", "a\nb\nnew\n"}, "delete all": {"a\nb\n", ""}, "from empty": {"", "a\nb\n"}, "drop final newline": {"a\nb\n", "a\nb"}, "add final newline": {"a\nb", "a\nb\n"}, "no newline change": {"a\nb", "a\nc"}, "two distant hunks": {strings.Repeat("x\n", 20) + "a\n" + strings.Repeat("y\n", 20) + "b\n", strings.Repeat("x\n", 20) + "A\n" + strings.Repeat("y\n", 20) + "B\n"}, "sshd port": {"# comment\nPort 22\nPermitRootLogin yes\n", "# comment\nPort 2222\nPermitRootLogin no\n"}, "identical": {"a\n", "a\n"}, "blank context lines": {"a\n\n\nb\n", "a\n\n\nc\n"}, } for name, c := range cases { roundTrip(t, name, c[0], c[1]) } } func TestRoundTripRandom(t *testing.T) { r := rand.New(rand.NewSource(42)) words := []string{"alpha", "beta", "gamma", "", "delta", "listen 80;", "}"} gen := func(n int) []string { var l []string for i := 0; i < n; i++ { l = append(l, words[r.Intn(len(words))]) } return l } for i := 0; i < 300; i++ { a := gen(r.Intn(40)) b := append([]string(nil), a...) for j := r.Intn(6); j > 0; j-- { switch op := r.Intn(3); { case op == 0 && len(b) > 0: k := r.Intn(len(b)) b = append(b[:k], b[k+1:]...) case op == 1: k := r.Intn(len(b) + 1) b = append(b[:k], append([]string{words[r.Intn(len(words))]}, b[k:]...)...) case len(b) > 0: b[r.Intn(len(b))] = "changed" } } old, new := strings.Join(a, "\n"), strings.Join(b, "\n") if r.Intn(2) == 0 && old != "" { old += "\n" } if r.Intn(2) == 0 && new != "" { new += "\n" } roundTrip(t, "random", old, new) } } func TestApplyWithOffset(t *testing.T) { d, _ := Unified("a/f", "b/f", "a\nb\nc\n", "a\nB\nc\n", 1) fps, _ := Parse(d) // El archivo real tiene líneas extra al principio: el hunk se desplaza. got, err := fps[0].Apply("x\ny\nz\na\nb\nc\n") if err != nil || got != "x\ny\nz\na\nB\nc\n" { t.Fatalf("got %q err %v", got, err) } if _, err := fps[0].Apply("a\nDIFFERENT\nc\n"); err == nil { t.Error("context mismatch must fail") } } func TestParseGitDiffAndTargets(t *testing.T) { d := `diff --git a/src/main.c b/src/main.c index 83db48f..bf269f4 100644 --- a/src/main.c 2026-09-27 10:00:00 +++ b/src/main.c 2026-09-27 10:05:00 @@ -1,3 +1,3 @@ int main() { - return 1; + return 0; } diff --git a/NEWS b/NEWS new file mode 100644 --- /dev/null +++ b/NEWS @@ -0,0 +1 @@ +fixed --- a/old.txt +++ /dev/null @@ -1 +0,0 @@ -bye ` fps, err := Parse(d) if err != nil { t.Fatal(err) } if len(fps) != 3 { t.Fatalf("files = %d", len(fps)) } if fps[0].Target(-1) != "src/main.c" || fps[0].Target(0) != "b/src/main.c" || fps[0].Target(2) != "main.c" { t.Errorf("targets: %q %q %q", fps[0].Target(-1), fps[0].Target(0), fps[0].Target(2)) } if !fps[1].IsNew || fps[1].Target(-1) != "NEWS" { t.Errorf("new file: %+v", fps[1]) } if !fps[2].IsDelete || fps[2].Target(-1) != "old.txt" { t.Errorf("delete: %+v %q", fps[2], fps[2].Target(-1)) } got, err := fps[0].Apply("int main() {\n return 1;\n}\n") if err != nil || !strings.Contains(got, "return 0;") { t.Errorf("apply: %q %v", got, err) } if got, err := fps[1].Apply(""); err != nil || got != "fixed\n" { t.Errorf("new file: %q %v", got, err) } if got, err := fps[2].Apply("bye\n"); err != nil || got != "" { t.Errorf("delete: %q %v", got, err) } if _, err := fps[1].Apply("exists\n"); err == nil { t.Error("creating an existing file must fail") } } func TestParseErrors(t *testing.T) { for _, d := range []string{ "no diff here", "--- a/f\n+++ b/f\n@@ -1,2 +1,2 @@\n a\n", // truncado "--- a/f\n+++ b/f\n@@ bad @@\n", "--- a/f\n+++ b/f\n", // sin hunks "--- a/f\n+++ b/f\n@@ -1 +1 @@\n?weird\n", } { if _, err := Parse(d); err == nil { t.Errorf("expected error for %q", d) } } } // Compatibilidad con diff(1) y patch(1) del sistema, si están instalados. func TestSystemToolsCompatibility(t *testing.T) { dir := t.TempDir() old := "server {\n listen 80;\n server_name a;\n}\n" new := "server {\n listen 443 ssl;\n server_name a;\n ssl_certificate x;\n}\n" oldPath, newPath := filepath.Join(dir, "old"), filepath.Join(dir, "new") os.WriteFile(oldPath, []byte(old), 0o644) os.WriteFile(newPath, []byte(new), 0o644) if _, err := exec.LookPath("diff"); err == nil { out, _ := exec.Command("diff", "-u", oldPath, newPath).Output() fps, err := Parse(string(out)) if err != nil { t.Fatalf("parse diff -u output: %v", err) } if got, err := fps[0].Apply(old); err != nil || got != new { t.Errorf("apply diff -u output: %q %v", got, err) } } if _, err := exec.LookPath("patch"); err == nil { d, _ := Unified("old", "old", old, new, 3) cmd := exec.Command("patch", "-s", oldPath) cmd.Stdin = strings.NewReader(d) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("patch(1) rejected our diff: %v %s", err, out) } if got, _ := os.ReadFile(oldPath); string(got) != new { t.Errorf("patch(1) result mismatch") } } }