1// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// licence that can be found in the LICENSE file.
4
5package rename
6
7import (
8	"fmt"
9	"go/build"
10	"go/token"
11	"io/ioutil"
12	"path/filepath"
13	"reflect"
14	"regexp"
15	"strings"
16	"testing"
17
18	"golang.org/x/tools/go/buildutil"
19)
20
21func TestErrors(t *testing.T) {
22	tests := []struct {
23		ctxt     *build.Context
24		from, to string
25		want     string // regexp to match error, or "OK"
26	}{
27		// Simple example.
28		{
29			ctxt: fakeContext(map[string][]string{
30				"foo": {`package foo; type T int`},
31				"bar": {`package bar`},
32				"main": {`package main
33
34import "foo"
35
36var _ foo.T
37`},
38			}),
39			from: "foo", to: "bar",
40			want: `invalid move destination: bar conflicts with directory .go.src.bar`,
41		},
42		// Subpackage already exists.
43		{
44			ctxt: fakeContext(map[string][]string{
45				"foo":     {`package foo; type T int`},
46				"foo/sub": {`package sub`},
47				"bar/sub": {`package sub`},
48				"main": {`package main
49
50import "foo"
51
52var _ foo.T
53`},
54			}),
55			from: "foo", to: "bar",
56			want: "invalid move destination: bar; package or subpackage bar/sub already exists",
57		},
58		// Invalid base name.
59		{
60			ctxt: fakeContext(map[string][]string{
61				"foo": {`package foo; type T int`},
62				"main": {`package main
63
64import "foo"
65
66var _ foo.T
67`},
68			}),
69			from: "foo", to: "bar-v2.0",
70			want: "invalid move destination: bar-v2.0; gomvpkg does not " +
71				"support move destinations whose base names are not valid " +
72				"go identifiers",
73		},
74		{
75			ctxt: fakeContext(map[string][]string{
76				"foo": {``},
77				"bar": {`package bar`},
78			}),
79			from: "foo", to: "bar",
80			want: `no initial packages were loaded`,
81		},
82	}
83
84	for _, test := range tests {
85		ctxt := test.ctxt
86
87		got := make(map[string]string)
88		writeFile = func(filename string, content []byte) error {
89			got[filename] = string(content)
90			return nil
91		}
92		moveDirectory = func(from, to string) error {
93			for path, contents := range got {
94				if strings.HasPrefix(path, from) {
95					newPath := strings.Replace(path, from, to, 1)
96					delete(got, path)
97					got[newPath] = contents
98				}
99			}
100			return nil
101		}
102
103		err := Move(ctxt, test.from, test.to, "")
104		prefix := fmt.Sprintf("-from %q -to %q", test.from, test.to)
105		if err == nil {
106			t.Errorf("%s: nil error. Expected error: %s", prefix, test.want)
107			continue
108		}
109		matched, err2 := regexp.MatchString(test.want, err.Error())
110		if err2 != nil {
111			t.Errorf("regexp.MatchString failed %s", err2)
112			continue
113		}
114		if !matched {
115			t.Errorf("%s: conflict does not match expectation:\n"+
116				"Error: %q\n"+
117				"Pattern: %q",
118				prefix, err.Error(), test.want)
119		}
120	}
121}
122
123func TestMoves(t *testing.T) {
124	tests := []struct {
125		ctxt         *build.Context
126		from, to     string
127		want         map[string]string
128		wantWarnings []string
129	}{
130		// Simple example.
131		{
132			ctxt: fakeContext(map[string][]string{
133				"foo": {`package foo; type T int`},
134				"main": {`package main
135
136import "foo"
137
138var _ foo.T
139`},
140			}),
141			from: "foo", to: "bar",
142			want: map[string]string{
143				"/go/src/main/0.go": `package main
144
145import "bar"
146
147var _ bar.T
148`,
149				"/go/src/bar/0.go": `package bar
150
151type T int
152`,
153			},
154		},
155
156		// Example with subpackage.
157		{
158			ctxt: fakeContext(map[string][]string{
159				"foo":     {`package foo; type T int`},
160				"foo/sub": {`package sub; type T int`},
161				"main": {`package main
162
163import "foo"
164import "foo/sub"
165
166var _ foo.T
167var _ sub.T
168`},
169			}),
170			from: "foo", to: "bar",
171			want: map[string]string{
172				"/go/src/main/0.go": `package main
173
174import "bar"
175import "bar/sub"
176
177var _ bar.T
178var _ sub.T
179`,
180				"/go/src/bar/0.go": `package bar
181
182type T int
183`,
184				"/go/src/bar/sub/0.go": `package sub; type T int`,
185			},
186		},
187
188		// References into subpackages
189		{
190			ctxt: fakeContext(map[string][]string{
191				"foo":   {`package foo; import "foo/a"; var _ a.T`},
192				"foo/a": {`package a; type T int`},
193				"foo/b": {`package b; import "foo/a"; var _ a.T`},
194			}),
195			from: "foo", to: "bar",
196			want: map[string]string{
197				"/go/src/bar/0.go": `package bar
198
199import "bar/a"
200
201var _ a.T
202`,
203				"/go/src/bar/a/0.go": `package a; type T int`,
204				"/go/src/bar/b/0.go": `package b
205
206import "bar/a"
207
208var _ a.T
209`,
210			},
211		},
212
213		// References into subpackages where directories have overlapped names
214		{
215			ctxt: fakeContext(map[string][]string{
216				"foo":    {},
217				"foo/a":  {`package a`},
218				"foo/aa": {`package bar`},
219				"foo/c":  {`package c; import _ "foo/bar";`},
220			}),
221			from: "foo/a", to: "foo/spam",
222			want: map[string]string{
223				"/go/src/foo/spam/0.go": `package spam
224`,
225				"/go/src/foo/aa/0.go": `package bar`,
226				"/go/src/foo/c/0.go":  `package c; import _ "foo/bar";`,
227			},
228		},
229
230		// External test packages
231		{
232			ctxt: buildutil.FakeContext(map[string]map[string]string{
233				"foo": {
234					"0.go":      `package foo; type T int`,
235					"0_test.go": `package foo_test; import "foo"; var _ foo.T`,
236				},
237				"baz": {
238					"0_test.go": `package baz_test; import "foo"; var _ foo.T`,
239				},
240			}),
241			from: "foo", to: "bar",
242			want: map[string]string{
243				"/go/src/bar/0.go": `package bar
244
245type T int
246`,
247				"/go/src/bar/0_test.go": `package bar_test
248
249import "bar"
250
251var _ bar.T
252`,
253				"/go/src/baz/0_test.go": `package baz_test
254
255import "bar"
256
257var _ bar.T
258`,
259			},
260		},
261		// package import comments
262		{
263			ctxt: fakeContext(map[string][]string{"foo": {`package foo // import "baz"`}}),
264			from: "foo", to: "bar",
265			want: map[string]string{"/go/src/bar/0.go": `package bar // import "bar"
266`},
267		},
268		{
269			ctxt: fakeContext(map[string][]string{"foo": {`package foo /* import "baz" */`}}),
270			from: "foo", to: "bar",
271			want: map[string]string{"/go/src/bar/0.go": `package bar /* import "bar" */
272`},
273		},
274		{
275			ctxt: fakeContext(map[string][]string{"foo": {`package foo       // import "baz"`}}),
276			from: "foo", to: "bar",
277			want: map[string]string{"/go/src/bar/0.go": `package bar // import "bar"
278`},
279		},
280		{
281			ctxt: fakeContext(map[string][]string{"foo": {`package foo
282// import " this is not an import comment`}}),
283			from: "foo", to: "bar",
284			want: map[string]string{"/go/src/bar/0.go": `package bar
285
286// import " this is not an import comment
287`},
288		},
289		{
290			ctxt: fakeContext(map[string][]string{"foo": {`package foo
291/* import " this is not an import comment */`}}),
292			from: "foo", to: "bar",
293			want: map[string]string{"/go/src/bar/0.go": `package bar
294
295/* import " this is not an import comment */
296`},
297		},
298		// Import name conflict generates a warning, not an error.
299		{
300			ctxt: fakeContext(map[string][]string{
301				"x": {},
302				"a": {`package a; type A int`},
303				"b": {`package b; type B int`},
304				"conflict": {`package conflict
305
306import "a"
307import "b"
308var _ a.A
309var _ b.B
310`},
311				"ok": {`package ok
312import "b"
313var _ b.B
314`},
315			}),
316			from: "b", to: "x/a",
317			want: map[string]string{
318				"/go/src/a/0.go": `package a; type A int`,
319				"/go/src/ok/0.go": `package ok
320
321import "x/a"
322
323var _ a.B
324`,
325				"/go/src/conflict/0.go": `package conflict
326
327import "a"
328import "x/a"
329
330var _ a.A
331var _ b.B
332`,
333				"/go/src/x/a/0.go": `package a
334
335type B int
336`,
337			},
338			wantWarnings: []string{
339				`/go/src/conflict/0.go:4:8: renaming this imported package name "b" to "a"`,
340				`/go/src/conflict/0.go:3:8: 	conflicts with imported package name in same block`,
341				`/go/src/conflict/0.go:3:8: skipping update of this file`,
342			},
343		},
344		// Rename with same base name.
345		{
346			ctxt: fakeContext(map[string][]string{
347				"x": {},
348				"y": {},
349				"x/foo": {`package foo
350
351type T int
352`},
353				"main": {`package main; import "x/foo"; var _ foo.T`},
354			}),
355			from: "x/foo", to: "y/foo",
356			want: map[string]string{
357				"/go/src/y/foo/0.go": `package foo
358
359type T int
360`,
361				"/go/src/main/0.go": `package main
362
363import "y/foo"
364
365var _ foo.T
366`,
367			},
368		},
369	}
370
371	for _, test := range tests {
372		ctxt := test.ctxt
373
374		got := make(map[string]string)
375		// Populate got with starting file set. rewriteFile and moveDirectory
376		// will mutate got to produce resulting file set.
377		buildutil.ForEachPackage(ctxt, func(importPath string, err error) {
378			if err != nil {
379				return
380			}
381			path := filepath.Join("/go/src", importPath, "0.go")
382			if !buildutil.FileExists(ctxt, path) {
383				return
384			}
385			f, err := ctxt.OpenFile(path)
386			if err != nil {
387				t.Errorf("unexpected error opening file: %s", err)
388				return
389			}
390			bytes, err := ioutil.ReadAll(f)
391			f.Close()
392			if err != nil {
393				t.Errorf("unexpected error reading file: %s", err)
394				return
395			}
396			got[path] = string(bytes)
397		})
398		var warnings []string
399		reportError = func(posn token.Position, message string) {
400			warning := fmt.Sprintf("%s:%d:%d: %s",
401				filepath.ToSlash(posn.Filename), // for MS Windows
402				posn.Line,
403				posn.Column,
404				message)
405			warnings = append(warnings, warning)
406
407		}
408		writeFile = func(filename string, content []byte) error {
409			got[filename] = string(content)
410			return nil
411		}
412		moveDirectory = func(from, to string) error {
413			for path, contents := range got {
414				if !(strings.HasPrefix(path, from) &&
415					(len(path) == len(from) || path[len(from)] == filepath.Separator)) {
416					continue
417				}
418				newPath := strings.Replace(path, from, to, 1)
419				delete(got, path)
420				got[newPath] = contents
421			}
422			return nil
423		}
424
425		err := Move(ctxt, test.from, test.to, "")
426		prefix := fmt.Sprintf("-from %q -to %q", test.from, test.to)
427		if err != nil {
428			t.Errorf("%s: unexpected error: %s", prefix, err)
429			continue
430		}
431
432		if !reflect.DeepEqual(warnings, test.wantWarnings) {
433			t.Errorf("%s: unexpected warnings:\n%s\nwant:\n%s",
434				prefix,
435				strings.Join(warnings, "\n"),
436				strings.Join(test.wantWarnings, "\n"))
437		}
438
439		for file, wantContent := range test.want {
440			k := filepath.FromSlash(file)
441			gotContent, ok := got[k]
442			delete(got, k)
443			if !ok {
444				// TODO(matloob): some testcases might have files that won't be
445				// rewritten
446				t.Errorf("%s: file %s not rewritten", prefix, file)
447				continue
448			}
449			if gotContent != wantContent {
450				t.Errorf("%s: rewritten file %s does not match expectation; got <<<%s>>>\n"+
451					"want <<<%s>>>", prefix, file, gotContent, wantContent)
452			}
453		}
454		// got should now be empty
455		for file := range got {
456			t.Errorf("%s: unexpected rewrite of file %s", prefix, file)
457		}
458	}
459}
460