// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package difftest supplies a set of tests that will operate on any // implementation of a diff algorithm as exposed by // "golang.org/x/tools/internal/lsp/diff" package difftest import ( "flag" "fmt" "io/ioutil" "os" "os/exec" "strings" "testing" "golang.org/x/tools/internal/lsp/diff" "golang.org/x/tools/internal/span" ) const ( fileA = "from" fileB = "to" unifiedPrefix = "--- " + fileA + "\n+++ " + fileB + "\n" ) var verifyDiff = flag.Bool("verify-diff", false, "Check that the unified diff output matches `diff -u`") func DiffTest(t *testing.T, compute diff.ComputeEdits) { t.Helper() for _, test := range []struct { name, in, out, unified string nodiff bool }{{ name: "empty", in: "", out: "", }, { name: "no_diff", in: "gargantuan\n", out: "gargantuan\n", }, { name: "replace_all", in: "gord\n", out: "gourd\n", unified: unifiedPrefix + ` @@ -1 +1 @@ -gord +gourd `[1:], }, { name: "insert_rune", in: "gord\n", out: "gourd\n", unified: unifiedPrefix + ` @@ -1 +1 @@ -gord +gourd `[1:], }, { name: "delete_rune", in: "groat\n", out: "goat\n", unified: unifiedPrefix + ` @@ -1 +1 @@ -groat +goat `[1:], }, { name: "replace_rune", in: "loud\n", out: "lord\n", unified: unifiedPrefix + ` @@ -1 +1 @@ -loud +lord `[1:], }, { name: "insert_line", in: "one\nthree\n", out: "one\ntwo\nthree\n", unified: unifiedPrefix + ` @@ -1,2 +1,3 @@ one +two three `[1:], }, { name: "replace_no_newline", in: "A", out: "B", unified: unifiedPrefix + ` @@ -1 +1 @@ -A \ No newline at end of file +B \ No newline at end of file `[1:], }, { name: "delete_front", in: "A\nB\nC\nA\nB\nB\nA\n", out: "C\nB\nA\nB\nA\nC\n", unified: unifiedPrefix + ` @@ -1,7 +1,6 @@ -A -B C +B A B -B A +C `[1:], nodiff: true, // diff algorithm produces different delete/insert pattern }, { name: "replace_last_line", in: "A\nB\n", out: "A\nC\n\n", unified: unifiedPrefix + ` @@ -1,2 +1,3 @@ A -B +C + `[1:], }, { name: "mulitple_replace", in: "A\nB\nC\nD\nE\nF\nG\n", out: "A\nH\nI\nJ\nE\nF\nK\n", unified: unifiedPrefix + ` @@ -1,7 +1,7 @@ A -B -C -D +H +I +J E F -G +K `[1:], }} { t.Run(test.name, func(t *testing.T) { t.Helper() edits := compute(span.FileURI("/"+test.name), test.in, test.out) got := diff.ApplyEdits(test.in, edits) unified := fmt.Sprint(diff.ToUnified("from", "to", test.in, edits)) if got != test.out { t.Errorf("got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.out) } if unified != test.unified { t.Errorf("got diff:\n%v\nexpected:\n%v", unified, test.unified) } if *verifyDiff && !test.nodiff { diff, err := getDiffOutput(test.in, test.out) if err != nil { t.Fatal(err) } if len(diff) > 0 { diff = unifiedPrefix + diff } if diff != test.unified { t.Errorf("unified:\n%q\ndiff -u:\n%q", test.unified, diff) } } }) } } func getDiffOutput(a, b string) (string, error) { fileA, err := ioutil.TempFile("", "myers.in") if err != nil { return "", err } defer os.Remove(fileA.Name()) if _, err := fileA.Write([]byte(a)); err != nil { return "", err } if err := fileA.Close(); err != nil { return "", err } fileB, err := ioutil.TempFile("", "myers.in") if err != nil { return "", err } defer os.Remove(fileB.Name()) if _, err := fileB.Write([]byte(b)); err != nil { return "", err } if err := fileB.Close(); err != nil { return "", err } cmd := exec.Command("diff", "-u", fileA.Name(), fileB.Name()) out, err := cmd.CombinedOutput() if err != nil { if _, ok := err.(*exec.ExitError); !ok { return "", fmt.Errorf("failed to run diff -u %v %v: %v\n%v", fileA.Name(), fileB.Name(), err, string(out)) } } diff := string(out) if len(diff) <= 0 { return diff, nil } bits := strings.SplitN(diff, "\n", 3) if len(bits) != 3 { return "", fmt.Errorf("diff output did not have file prefix:\n%s", diff) } return bits[2], nil }