1package apidiff
2
3import (
4	"bufio"
5	"fmt"
6	"go/types"
7	"io/ioutil"
8	"os"
9	"os/exec"
10	"path/filepath"
11	"reflect"
12	"runtime"
13	"sort"
14	"strings"
15	"testing"
16
17	"golang.org/x/tools/go/packages"
18)
19
20func TestChanges(t *testing.T) {
21	dir, err := ioutil.TempDir("", "apidiff_test")
22	if err != nil {
23		t.Fatal(err)
24	}
25	dir = filepath.Join(dir, "go")
26	wanti, wantc := splitIntoPackages(t, dir)
27	defer os.RemoveAll(dir)
28	sort.Strings(wanti)
29	sort.Strings(wantc)
30
31	oldpkg, err := load(t, "apidiff/old", dir)
32	if err != nil {
33		t.Fatal(err)
34	}
35	newpkg, err := load(t, "apidiff/new", dir)
36	if err != nil {
37		t.Fatal(err)
38	}
39
40	report := Changes(oldpkg.Types, newpkg.Types)
41
42	got := report.messages(false)
43	if !reflect.DeepEqual(got, wanti) {
44		t.Errorf("incompatibles: got %v\nwant %v\n", got, wanti)
45	}
46	got = report.messages(true)
47	if !reflect.DeepEqual(got, wantc) {
48		t.Errorf("compatibles: got %v\nwant %v\n", got, wantc)
49	}
50}
51
52func splitIntoPackages(t *testing.T, dir string) (incompatibles, compatibles []string) {
53	// Read the input file line by line.
54	// Write a line into the old or new package,
55	// dependent on comments.
56	// Also collect expected messages.
57	f, err := os.Open("testdata/tests.go")
58	if err != nil {
59		t.Fatal(err)
60	}
61	defer f.Close()
62
63	if err := os.MkdirAll(filepath.Join(dir, "src", "apidiff"), 0700); err != nil {
64		t.Fatal(err)
65	}
66	if err := ioutil.WriteFile(filepath.Join(dir, "src", "apidiff", "go.mod"), []byte("module apidiff\n"), 0666); err != nil {
67		t.Fatal(err)
68	}
69
70	oldd := filepath.Join(dir, "src/apidiff/old")
71	newd := filepath.Join(dir, "src/apidiff/new")
72	if err := os.MkdirAll(oldd, 0700); err != nil {
73		t.Fatal(err)
74	}
75	if err := os.Mkdir(newd, 0700); err != nil && !os.IsExist(err) {
76		t.Fatal(err)
77	}
78
79	oldf, err := os.Create(filepath.Join(oldd, "old.go"))
80	if err != nil {
81		t.Fatal(err)
82	}
83	newf, err := os.Create(filepath.Join(newd, "new.go"))
84	if err != nil {
85		t.Fatal(err)
86	}
87
88	wl := func(f *os.File, line string) {
89		if _, err := fmt.Fprintln(f, line); err != nil {
90			t.Fatal(err)
91		}
92	}
93	writeBoth := func(line string) { wl(oldf, line); wl(newf, line) }
94	writeln := writeBoth
95	s := bufio.NewScanner(f)
96	for s.Scan() {
97		line := s.Text()
98		tl := strings.TrimSpace(line)
99		switch {
100		case tl == "// old":
101			writeln = func(line string) { wl(oldf, line) }
102		case tl == "// new":
103			writeln = func(line string) { wl(newf, line) }
104		case tl == "// both":
105			writeln = writeBoth
106		case strings.HasPrefix(tl, "// i "):
107			incompatibles = append(incompatibles, strings.TrimSpace(tl[4:]))
108		case strings.HasPrefix(tl, "// c "):
109			compatibles = append(compatibles, strings.TrimSpace(tl[4:]))
110		default:
111			writeln(line)
112		}
113	}
114	if s.Err() != nil {
115		t.Fatal(s.Err())
116	}
117	return
118}
119
120func load(t *testing.T, importPath, goPath string) (*packages.Package, error) {
121	needsGoPackages(t)
122
123	cfg := &packages.Config{
124		Mode: packages.LoadTypes,
125	}
126	if goPath != "" {
127		cfg.Env = append(os.Environ(), "GOPATH="+goPath)
128		cfg.Dir = filepath.Join(goPath, "src", filepath.FromSlash(importPath))
129	}
130	pkgs, err := packages.Load(cfg, importPath)
131	if err != nil {
132		return nil, err
133	}
134	if len(pkgs[0].Errors) > 0 {
135		return nil, pkgs[0].Errors[0]
136	}
137	return pkgs[0], nil
138}
139
140func TestExportedFields(t *testing.T) {
141	pkg, err := load(t, "golang.org/x/exp/apidiff/testdata/exported_fields", "")
142	if err != nil {
143		t.Fatal(err)
144	}
145	typeof := func(name string) types.Type {
146		return pkg.Types.Scope().Lookup(name).Type()
147	}
148
149	s := typeof("S")
150	su := s.(*types.Named).Underlying().(*types.Struct)
151
152	ef := exportedSelectableFields(su)
153	wants := []struct {
154		name string
155		typ  types.Type
156	}{
157		{"A1", typeof("A1")},
158		{"D", types.Typ[types.Bool]},
159		{"E", types.Typ[types.Int]},
160		{"F", typeof("F")},
161		{"S", types.NewPointer(s)},
162	}
163
164	if got, want := len(ef), len(wants); got != want {
165		t.Errorf("got %d fields, want %d\n%+v", got, want, ef)
166	}
167	for _, w := range wants {
168		if got := ef[w.name]; got != nil && !types.Identical(got.Type(), w.typ) {
169			t.Errorf("%s: got %v, want %v", w.name, got.Type(), w.typ)
170		}
171	}
172}
173
174// needsGoPackages skips t if the go/packages driver (or 'go' tool) implied by
175// the current process environment is not present in the path.
176//
177// Copied and adapted from golang.org/x/tools/internal/testenv.
178func needsGoPackages(t *testing.T) {
179	t.Helper()
180
181	tool := os.Getenv("GOPACKAGESDRIVER")
182	switch tool {
183	case "off":
184		// "off" forces go/packages to use the go command.
185		tool = "go"
186	case "":
187		if _, err := exec.LookPath("gopackagesdriver"); err == nil {
188			tool = "gopackagesdriver"
189		} else {
190			tool = "go"
191		}
192	}
193
194	needsTool(t, tool)
195}
196
197// needsTool skips t if the named tool is not present in the path.
198//
199// Copied and adapted from golang.org/x/tools/internal/testenv.
200func needsTool(t *testing.T, tool string) {
201	_, err := exec.LookPath(tool)
202	if err == nil {
203		return
204	}
205
206	t.Helper()
207	if allowMissingTool(tool) {
208		t.Skipf("skipping because %s tool not available: %v", tool, err)
209	} else {
210		t.Fatalf("%s tool not available: %v", tool, err)
211	}
212}
213
214func allowMissingTool(tool string) bool {
215	if runtime.GOOS == "android" {
216		// Android builds generally run tests on a separate machine from the build,
217		// so don't expect any external tools to be available.
218		return true
219	}
220
221	if tool == "go" && os.Getenv("GO_BUILDER_NAME") == "illumos-amd64-joyent" {
222		// Work around a misconfigured builder (see https://golang.org/issue/33950).
223		return true
224	}
225
226	// If a developer is actively working on this test, we expect them to have all
227	// of its dependencies installed. However, if it's just a dependency of some
228	// other module (for example, being run via 'go test all'), we should be more
229	// tolerant of unusual environments.
230	return !packageMainIsDevel()
231}
232
233// packageMainIsDevel reports whether the module containing package main
234// is a development version (if module information is available).
235//
236// Builds in GOPATH mode and builds that lack module information are assumed to
237// be development versions.
238var packageMainIsDevel = func() bool { return true }
239