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