1 package apidiff
2
3 import (
4 "bufio"
5 "fmt"
6 "go/types"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "runtime"
11 "sort"
12 "strings"
13 "testing"
14
15 "github.com/google/go-cmp/cmp"
16 "golang.org/x/tools/go/packages"
17 "golang.org/x/tools/go/packages/packagestest"
18 )
19
20 func TestModuleChanges(t *testing.T) {
21 packagestest.TestAll(t, testModuleChanges)
22 }
23
24 func testModuleChanges(t *testing.T, x packagestest.Exporter) {
25 e := packagestest.Export(t, x, []packagestest.Module{
26 {
27 Name: "example.com/moda",
28 Files: map[string]any{
29 "foo/foo.go": "package foo\n\nconst Version = 1",
30 "foo/baz/baz.go": "package baz",
31 },
32 },
33 {
34 Name: "example.com/modb",
35 Files: map[string]any{
36 "foo/foo.go": "package foo\n\nconst Version = 2\nconst Other = 1",
37 "bar/bar.go": "package bar",
38 },
39 },
40 })
41 defer e.Cleanup()
42
43 a, err := loadModule(t, e.Config, "example.com/moda")
44 if err != nil {
45 t.Fatal(err)
46 }
47 b, err := loadModule(t, e.Config, "example.com/modb")
48 if err != nil {
49 t.Fatal(err)
50 }
51 report := ModuleChanges(a, b)
52 if len(report.Changes) == 0 {
53 t.Fatal("expected some changes, but got none")
54 }
55 wanti := []string{
56 "./foo.Version: value changed from 1 to 2",
57 "package example.com/moda/foo/baz: removed",
58 }
59 sort.Strings(wanti)
60
61 got := report.messages(false)
62 sort.Strings(got)
63
64 if diff := cmp.Diff(wanti, got); diff != "" {
65 t.Errorf("incompatibles: mismatch (-want, +got)\n%s", diff)
66 }
67
68 wantc := []string{
69 "./foo.Other: added",
70 "package example.com/modb/bar: added",
71 }
72 sort.Strings(wantc)
73
74 got = report.messages(true)
75 sort.Strings(got)
76
77 if diff := cmp.Diff(wantc, got); diff != "" {
78 t.Errorf("compatibles: mismatch (-want, +got)\n%s", diff)
79 }
80 }
81
82 func TestChanges(t *testing.T) {
83 testfiles, err := filepath.Glob(filepath.Join("testdata", "*.go"))
84 if err != nil {
85 t.Fatal(err)
86 }
87 for _, testfile := range testfiles {
88 name := strings.TrimSuffix(filepath.Base(testfile), ".go")
89 t.Run(name, func(t *testing.T) {
90 dir := filepath.Join(t.TempDir(), "go")
91 wanti, wantc := splitIntoPackages(t, testfile, dir)
92 sort.Strings(wanti)
93 sort.Strings(wantc)
94
95 oldpkg, err := loadPackage(t, "apidiff/old", dir)
96 if err != nil {
97 t.Fatal(err)
98 }
99 newpkg, err := loadPackage(t, "apidiff/new", dir)
100 if err != nil {
101 t.Fatal(err)
102 }
103
104 report := Changes(oldpkg.Types, newpkg.Types)
105
106 got := report.messages(false)
107 if diff := cmp.Diff(wanti, got); diff != "" {
108 t.Errorf("incompatibles: mismatch (-want, +got)\n%s", diff)
109 }
110 got = report.messages(true)
111 if diff := cmp.Diff(wantc, got); diff != "" {
112 t.Errorf("compatibles: mismatch (-want, +got)\n%s", diff)
113 }
114 })
115 }
116 }
117
118 func splitIntoPackages(t *testing.T, file, dir string) (incompatibles, compatibles []string) {
119
120
121
122
123 f, err := os.Open(file)
124 if err != nil {
125 t.Fatal(err)
126 }
127 defer f.Close()
128
129 if err := os.MkdirAll(filepath.Join(dir, "src", "apidiff"), 0700); err != nil {
130 t.Fatal(err)
131 }
132 if err := os.WriteFile(filepath.Join(dir, "src", "apidiff", "go.mod"), []byte("module apidiff\ngo 1.18\n"), 0600); err != nil {
133 t.Fatal(err)
134 }
135
136 oldd := filepath.Join(dir, "src/apidiff/old")
137 newd := filepath.Join(dir, "src/apidiff/new")
138 if err := os.MkdirAll(oldd, 0700); err != nil {
139 t.Fatal(err)
140 }
141 if err := os.Mkdir(newd, 0700); err != nil && !os.IsExist(err) {
142 t.Fatal(err)
143 }
144
145 oldf, err := os.Create(filepath.Join(oldd, "old.go"))
146 if err != nil {
147 t.Fatal(err)
148 }
149 defer func() {
150 if err := oldf.Close(); err != nil {
151 t.Fatal(err)
152 }
153 }()
154
155 newf, err := os.Create(filepath.Join(newd, "new.go"))
156 if err != nil {
157 t.Fatal(err)
158 }
159 defer func() {
160 if err := newf.Close(); err != nil {
161 t.Fatal(err)
162 }
163 }()
164
165 wl := func(f *os.File, line string) {
166 if _, err := fmt.Fprintln(f, line); err != nil {
167 t.Fatal(err)
168 }
169 }
170 writeBoth := func(line string) { wl(oldf, line); wl(newf, line) }
171 writeln := writeBoth
172 s := bufio.NewScanner(f)
173 for s.Scan() {
174 line := s.Text()
175 tl := strings.TrimSpace(line)
176 switch {
177 case tl == "// old":
178 writeln = func(line string) { wl(oldf, line) }
179 case tl == "// new":
180 writeln = func(line string) { wl(newf, line) }
181 case tl == "// both":
182 writeln = writeBoth
183 case strings.HasPrefix(tl, "// i "):
184 incompatibles = append(incompatibles, strings.TrimSpace(tl[4:]))
185 case strings.HasPrefix(tl, "// c "):
186 compatibles = append(compatibles, strings.TrimSpace(tl[4:]))
187 default:
188 writeln(line)
189 }
190 }
191 if s.Err() != nil {
192 t.Fatal(s.Err())
193 }
194 return
195 }
196
197
198 func loadModule(t *testing.T, cfg *packages.Config, modulePath string) (*Module, error) {
199 needsGoPackages(t)
200
201 cfg.Mode = cfg.Mode | packages.LoadTypes
202 loaded, err := packages.Load(cfg, fmt.Sprintf("%s/...", modulePath))
203 if err != nil {
204 return nil, err
205 }
206 if len(loaded) == 0 {
207 return nil, fmt.Errorf("found no packages for module %s", modulePath)
208 }
209 var tpkgs []*types.Package
210 for _, p := range loaded {
211 if len(p.Errors) > 0 {
212
213 return nil, p.Errors[0]
214 }
215 tpkgs = append(tpkgs, p.Types)
216 }
217
218 return &Module{Path: modulePath, Packages: tpkgs}, nil
219 }
220
221 func loadPackage(t *testing.T, importPath, goPath string) (*packages.Package, error) {
222 needsGoPackages(t)
223
224 cfg := &packages.Config{
225 Mode: packages.LoadTypes,
226 }
227 if goPath != "" {
228 cfg.Env = append(os.Environ(), "GOPATH="+goPath)
229 cfg.Dir = filepath.Join(goPath, "src", filepath.FromSlash(importPath))
230 }
231 pkgs, err := packages.Load(cfg, importPath)
232 if err != nil {
233 return nil, err
234 }
235 if len(pkgs[0].Errors) > 0 {
236 return nil, pkgs[0].Errors[0]
237 }
238 return pkgs[0], nil
239 }
240
241 func TestExportedFields(t *testing.T) {
242 pkg, err := loadPackage(t, "golang.org/x/exp/apidiff/testdata/exported_fields", "")
243 if err != nil {
244 t.Fatal(err)
245 }
246 typeof := func(name string) types.Type {
247 return pkg.Types.Scope().Lookup(name).Type()
248 }
249
250 s := typeof("S")
251 su := s.(*types.Named).Underlying().(*types.Struct)
252
253 ef := exportedSelectableFields(su)
254 wants := []struct {
255 name string
256 typ types.Type
257 }{
258 {"A1", typeof("A1")},
259 {"D", types.Typ[types.Bool]},
260 {"E", types.Typ[types.Int]},
261 {"F", typeof("F")},
262 {"S", types.NewPointer(s)},
263 }
264
265 if got, want := len(ef), len(wants); got != want {
266 t.Errorf("got %d fields, want %d\n%+v", got, want, ef)
267 }
268 for _, w := range wants {
269 if got := ef[w.name]; got != nil && !types.Identical(got.Type(), w.typ) {
270 t.Errorf("%s: got %v, want %v", w.name, got.Type(), w.typ)
271 }
272 }
273 }
274
275
276
277
278
279 func needsGoPackages(t *testing.T) {
280 t.Helper()
281
282 tool := os.Getenv("GOPACKAGESDRIVER")
283 switch tool {
284 case "off":
285
286 tool = "go"
287 case "":
288 if _, err := exec.LookPath("gopackagesdriver"); err == nil {
289 tool = "gopackagesdriver"
290 } else {
291 tool = "go"
292 }
293 }
294
295 needsTool(t, tool)
296 }
297
298
299
300
301 func needsTool(t *testing.T, tool string) {
302 _, err := exec.LookPath(tool)
303 if err == nil {
304 return
305 }
306
307 t.Helper()
308 if allowMissingTool(tool) {
309 t.Skipf("skipping because %s tool not available: %v", tool, err)
310 } else {
311 t.Fatalf("%s tool not available: %v", tool, err)
312 }
313 }
314
315 func allowMissingTool(tool string) bool {
316 if runtime.GOOS == "android" {
317
318
319 return true
320 }
321
322 if tool == "go" && os.Getenv("GO_BUILDER_NAME") == "illumos-amd64-joyent" {
323
324 return true
325 }
326
327
328
329
330
331 return !packageMainIsDevel()
332 }
333
334
335
336
337
338
339 var packageMainIsDevel = func() bool { return true }
340
View as plain text