diff --git a/internal/lsp/diff/diff_test.go b/internal/lsp/diff/diff_test.go index dde5ec0394..68ab1e8420 100644 --- a/internal/lsp/diff/diff_test.go +++ b/internal/lsp/diff/diff_test.go @@ -4,27 +4,16 @@ import ( "testing" "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/lsp/diff/difftest" ) func TestApplyEdits(t *testing.T) { - var testCases = []struct { - before string - edits []diff.TextEdit - want string - }{ - {"", nil, ""}, - {"X", []diff.TextEdit{{newSpan(0, 1), "Y"}}, "Y"}, - {" X ", []diff.TextEdit{{newSpan(1, 2), "Y"}}, " Y "}, - {" X X ", []diff.TextEdit{{newSpan(1, 2), "Y"}, {newSpan(3, 4), "Z"}}, " Y Z "}, - } - for _, tc := range testCases { - if got := diff.ApplyEdits(tc.before, tc.edits); got != tc.want { - t.Errorf("applyEdits(%v, %v): got %v, want %v", tc.before, tc.edits, got, tc.want) - } + for _, tc := range difftest.TestCases { + t.Run(tc.Name, func(t *testing.T) { + t.Helper() + if got := diff.ApplyEdits(tc.In, tc.Edits); got != tc.Out { + t.Errorf("ApplyEdits edits got %q, want %q", got, tc.Out) + } + }) } } - -func newSpan(start, end int) span.Span { - return span.New("", span.NewPoint(0, 0, start), span.NewPoint(0, 0, end)) -} diff --git a/internal/lsp/diff/difftest/difftest.go b/internal/lsp/diff/difftest/difftest.go index 6de617feb9..da82704285 100644 --- a/internal/lsp/diff/difftest/difftest.go +++ b/internal/lsp/diff/difftest/difftest.go @@ -8,12 +8,7 @@ package difftest import ( - "flag" "fmt" - "io/ioutil" - "os" - "os/exec" - "strings" "testing" "golang.org/x/tools/internal/lsp/diff" @@ -21,88 +16,104 @@ import ( ) const ( - fileA = "from" - fileB = "to" - unifiedPrefix = "--- " + fileA + "\n+++ " + fileB + "\n" + 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 + ` +var TestCases = []struct { + Name, In, Out, Unified string + Edits []diff.TextEdit + NoDiff bool +}{{ + Name: "empty", + In: "", + Out: "", +}, { + Name: "no_diff", + In: "gargantuan\n", + Out: "gargantuan\n", +}, { + Name: "replace_all", + In: "fruit\n", + Out: "cheese\n", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-fruit ++cheese +`[1:], + Edits: []diff.TextEdit{{Span: newSpan(0, 5), NewText: "cheese"}}, +}, { + Name: "insert_rune", + 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 + ` + Edits: []diff.TextEdit{{Span: newSpan(2, 2), NewText: "u"}}, +}, { + 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 + ` + Edits: []diff.TextEdit{{Span: newSpan(1, 2), NewText: ""}}, +}, { + 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 + ` + Edits: []diff.TextEdit{{Span: newSpan(2, 3), NewText: "r"}}, +}, { + Name: "replace_partials", + In: "blanket\n", + Out: "bunker\n", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-blanket ++bunker +`[1:], + Edits: []diff.TextEdit{ + {Span: newSpan(1, 3), NewText: "u"}, + {Span: newSpan(6, 7), NewText: "r"}, + }, +}, { + 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 + ` + Edits: []diff.TextEdit{{Span: newSpan(4, 4), NewText: "two\n"}}, +}, { + 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 + ` + Edits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "B"}}, +}, { + 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 @@ -114,25 +125,32 @@ func DiffTest(t *testing.T, compute diff.ComputeEdits) { A +C `[1:], - nodiff: true, // diff algorithm produces different delete/insert pattern + Edits: []diff.TextEdit{ + {Span: newSpan(0, 4), NewText: ""}, + {Span: newSpan(6, 6), NewText: "B\n"}, + {Span: newSpan(10, 12), NewText: ""}, + {Span: newSpan(14, 14), NewText: "C\n"}, }, - { - name: "replace_last_line", - in: "A\nB\n", - out: "A\nC\n\n", - unified: unifiedPrefix + ` + 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 + ` + Edits: []diff.TextEdit{{Span: newSpan(2, 3), NewText: "C\n"}}, + }, + { + 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 @@ -146,71 +164,42 @@ func DiffTest(t *testing.T, compute diff.ComputeEdits) { -G +K `[1:], - }} { - t.Run(test.name, func(t *testing.T) { + Edits: []diff.TextEdit{ + {Span: newSpan(2, 8), NewText: "H\nI\nJ\n"}, + {Span: newSpan(12, 14), NewText: "K\n"}, + }, + }, +} + +func init() { + // expand all the spans to full versions + // we need them all to have their line number and column + for _, tc := range TestCases { + c := span.NewContentConverter("", []byte(tc.In)) + for i := range tc.Edits { + tc.Edits[i].Span, _ = tc.Edits[i].Span.WithAll(c) + } + } +} + +func DiffTest(t *testing.T, compute diff.ComputeEdits) { + t.Helper() + for _, test := range TestCases { + 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) + edits := compute(span.FileURI("/"+test.Name), test.In, test.Out) + got := diff.ApplyEdits(test.In, edits) + unified := fmt.Sprint(diff.ToUnified(FileA, FileB, 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) - } + if unified != test.Unified { + t.Errorf("got diff:\n%v\nexpected:\n%v", unified, test.Unified) } }) } } -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 +func newSpan(start, end int) span.Span { + return span.New("", span.NewPoint(0, 0, start), span.NewPoint(0, 0, end)) } diff --git a/internal/lsp/diff/difftest/difftest_test.go b/internal/lsp/diff/difftest/difftest_test.go new file mode 100644 index 0000000000..c6c7a85d9d --- /dev/null +++ b/internal/lsp/diff/difftest/difftest_test.go @@ -0,0 +1,83 @@ +// 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_test + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" + "testing" + + "golang.org/x/tools/internal/lsp/diff/difftest" + "golang.org/x/tools/internal/testenv" +) + +func VerifyUnifiedTest(t *testing.T) { + testenv.NeedsTool(t, "diff") + for _, test := range difftest.TestCases { + t.Run(test.Name, func(t *testing.T) { + t.Helper() + if test.NoDiff { + t.Skip("diff tool produces expected different results") + } + diff, err := getDiffOutput(test.In, test.Out) + if err != nil { + t.Fatal(err) + } + if len(diff) > 0 { + diff = difftest.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 +}