golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/apidiff/apidiff_test.go (about) 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 // Read the input file line by line. 120 // Write a line into the old or new package, 121 // dependent on comments. 122 // Also collect expected messages. 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 // Copied from cmd/apidiff/main.go. 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 // TODO: use errors.Join once Go 1.21 is released. 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 // needsGoPackages skips t if the go/packages driver (or 'go' tool) implied by 276 // the current process environment is not present in the path. 277 // 278 // Copied and adapted from golang.org/x/tools/internal/testenv. 279 func needsGoPackages(t *testing.T) { 280 t.Helper() 281 282 tool := os.Getenv("GOPACKAGESDRIVER") 283 switch tool { 284 case "off": 285 // "off" forces go/packages to use the go command. 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 // needsTool skips t if the named tool is not present in the path. 299 // 300 // Copied and adapted from golang.org/x/tools/internal/testenv. 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 // Android builds generally run tests on a separate machine from the build, 318 // so don't expect any external tools to be available. 319 return true 320 } 321 322 if tool == "go" && os.Getenv("GO_BUILDER_NAME") == "illumos-amd64-joyent" { 323 // Work around a misconfigured builder (see https://golang.org/issue/33950). 324 return true 325 } 326 327 // If a developer is actively working on this test, we expect them to have all 328 // of its dependencies installed. However, if it's just a dependency of some 329 // other module (for example, being run via 'go test all'), we should be more 330 // tolerant of unusual environments. 331 return !packageMainIsDevel() 332 } 333 334 // packageMainIsDevel reports whether the module containing package main 335 // is a development version (if module information is available). 336 // 337 // Builds in GOPATH mode and builds that lack module information are assumed to 338 // be development versions. 339 var packageMainIsDevel = func() bool { return true }