203 lines
5.8 KiB
Go
203 lines
5.8 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|