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 }