golang.org/x/tools/gopls@v0.15.3/internal/cache/session_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cache
     6  
     7  import (
     8  	"context"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"testing"
    13  
    14  	"github.com/google/go-cmp/cmp"
    15  	"golang.org/x/tools/gopls/internal/protocol"
    16  	"golang.org/x/tools/gopls/internal/settings"
    17  	"golang.org/x/tools/gopls/internal/test/integration/fake"
    18  	"golang.org/x/tools/internal/testenv"
    19  )
    20  
    21  func TestZeroConfigAlgorithm(t *testing.T) {
    22  	testenv.NeedsExec(t) // executes the Go command
    23  	t.Setenv("GOPACKAGESDRIVER", "off")
    24  
    25  	type viewSummary struct {
    26  		// fields exported for cmp.Diff
    27  		Type ViewType
    28  		Root string
    29  		Env  []string
    30  	}
    31  
    32  	type folderSummary struct {
    33  		dir     string
    34  		options func(dir string) map[string]any // options may refer to the temp dir
    35  	}
    36  
    37  	includeReplaceInWorkspace := func(string) map[string]any {
    38  		return map[string]any{
    39  			"includeReplaceInWorkspace": true,
    40  		}
    41  	}
    42  
    43  	type test struct {
    44  		name    string
    45  		files   map[string]string // use a map rather than txtar as file content is tiny
    46  		folders []folderSummary
    47  		open    []string // open files
    48  		want    []viewSummary
    49  	}
    50  
    51  	tests := []test{
    52  		// TODO(rfindley): add a test for GOPACKAGESDRIVER.
    53  		// Doing so doesn't yet work using options alone (user env is not honored)
    54  
    55  		// TODO(rfindley): add a test for degenerate cases, such as missing
    56  		// workspace folders (once we decide on the correct behavior).
    57  		{
    58  			"basic go.work workspace",
    59  			map[string]string{
    60  				"go.work":  "go 1.18\nuse (\n\t./a\n\t./b\n)\n",
    61  				"a/go.mod": "module golang.org/a\ngo 1.18\n",
    62  				"b/go.mod": "module golang.org/b\ngo 1.18\n",
    63  			},
    64  			[]folderSummary{{dir: "."}},
    65  			nil,
    66  			[]viewSummary{{GoWorkView, ".", nil}},
    67  		},
    68  		{
    69  			"basic go.mod workspace",
    70  			map[string]string{
    71  				"go.mod": "module golang.org/a\ngo 1.18\n",
    72  			},
    73  			[]folderSummary{{dir: "."}},
    74  			nil,
    75  			[]viewSummary{{GoModView, ".", nil}},
    76  		},
    77  		{
    78  			"basic GOPATH workspace",
    79  			map[string]string{
    80  				"src/golang.org/a/a.go": "package a",
    81  				"src/golang.org/b/b.go": "package b",
    82  			},
    83  			[]folderSummary{{
    84  				dir: "src",
    85  				options: func(dir string) map[string]any {
    86  					return map[string]any{
    87  						"env": map[string]any{
    88  							"GOPATH": dir,
    89  						},
    90  					}
    91  				},
    92  			}},
    93  			[]string{"src/golang.org/a//a.go", "src/golang.org/b/b.go"},
    94  			[]viewSummary{{GOPATHView, "src", nil}},
    95  		},
    96  		{
    97  			"basic AdHoc workspace",
    98  			map[string]string{
    99  				"foo.go": "package foo",
   100  			},
   101  			[]folderSummary{{dir: "."}},
   102  			nil,
   103  			[]viewSummary{{AdHocView, ".", nil}},
   104  		},
   105  		{
   106  			"multi-folder workspace",
   107  			map[string]string{
   108  				"a/go.mod": "module golang.org/a\ngo 1.18\n",
   109  				"b/go.mod": "module golang.org/b\ngo 1.18\n",
   110  			},
   111  			[]folderSummary{{dir: "a"}, {dir: "b"}},
   112  			nil,
   113  			[]viewSummary{{GoModView, "a", nil}, {GoModView, "b", nil}},
   114  		},
   115  		{
   116  			"multi-module workspace",
   117  			map[string]string{
   118  				"a/go.mod": "module golang.org/a\ngo 1.18\n",
   119  				"b/go.mod": "module golang.org/b\ngo 1.18\n",
   120  			},
   121  			[]folderSummary{{dir: "."}},
   122  			nil,
   123  			[]viewSummary{{AdHocView, ".", nil}},
   124  		},
   125  		{
   126  			"zero-config open module",
   127  			map[string]string{
   128  				"a/go.mod": "module golang.org/a\ngo 1.18\n",
   129  				"a/a.go":   "package a",
   130  				"b/go.mod": "module golang.org/b\ngo 1.18\n",
   131  				"b/b.go":   "package b",
   132  			},
   133  			[]folderSummary{{dir: "."}},
   134  			[]string{"a/a.go"},
   135  			[]viewSummary{
   136  				{AdHocView, ".", nil},
   137  				{GoModView, "a", nil},
   138  			},
   139  		},
   140  		{
   141  			"zero-config open modules",
   142  			map[string]string{
   143  				"a/go.mod": "module golang.org/a\ngo 1.18\n",
   144  				"a/a.go":   "package a",
   145  				"b/go.mod": "module golang.org/b\ngo 1.18\n",
   146  				"b/b.go":   "package b",
   147  			},
   148  			[]folderSummary{{dir: "."}},
   149  			[]string{"a/a.go", "b/b.go"},
   150  			[]viewSummary{
   151  				{AdHocView, ".", nil},
   152  				{GoModView, "a", nil},
   153  				{GoModView, "b", nil},
   154  			},
   155  		},
   156  		{
   157  			"unified workspace",
   158  			map[string]string{
   159  				"go.work":  "go 1.18\nuse (\n\t./a\n\t./b\n)\n",
   160  				"a/go.mod": "module golang.org/a\ngo 1.18\n",
   161  				"a/a.go":   "package a",
   162  				"b/go.mod": "module golang.org/b\ngo 1.18\n",
   163  				"b/b.go":   "package b",
   164  			},
   165  			[]folderSummary{{dir: "."}},
   166  			[]string{"a/a.go", "b/b.go"},
   167  			[]viewSummary{{GoWorkView, ".", nil}},
   168  		},
   169  		{
   170  			"go.work from env",
   171  			map[string]string{
   172  				"nested/go.work": "go 1.18\nuse (\n\t../a\n\t../b\n)\n",
   173  				"a/go.mod":       "module golang.org/a\ngo 1.18\n",
   174  				"a/a.go":         "package a",
   175  				"b/go.mod":       "module golang.org/b\ngo 1.18\n",
   176  				"b/b.go":         "package b",
   177  			},
   178  			[]folderSummary{{
   179  				dir: ".",
   180  				options: func(dir string) map[string]any {
   181  					return map[string]any{
   182  						"env": map[string]any{
   183  							"GOWORK": filepath.Join(dir, "nested", "go.work"),
   184  						},
   185  					}
   186  				},
   187  			}},
   188  			[]string{"a/a.go", "b/b.go"},
   189  			[]viewSummary{{GoWorkView, ".", nil}},
   190  		},
   191  		{
   192  			"independent module view",
   193  			map[string]string{
   194  				"go.work":  "go 1.18\nuse (\n\t./a\n)\n", // not using b
   195  				"a/go.mod": "module golang.org/a\ngo 1.18\n",
   196  				"a/a.go":   "package a",
   197  				"b/go.mod": "module golang.org/a\ngo 1.18\n",
   198  				"b/b.go":   "package b",
   199  			},
   200  			[]folderSummary{{dir: "."}},
   201  			[]string{"a/a.go", "b/b.go"},
   202  			[]viewSummary{
   203  				{GoWorkView, ".", nil},
   204  				{GoModView, "b", []string{"GOWORK=off"}},
   205  			},
   206  		},
   207  		{
   208  			"multiple go.work",
   209  			map[string]string{
   210  				"go.work":    "go 1.18\nuse (\n\t./a\n\t./b\n)\n",
   211  				"a/go.mod":   "module golang.org/a\ngo 1.18\n",
   212  				"a/a.go":     "package a",
   213  				"b/go.work":  "go 1.18\nuse (\n\t.\n\t./c\n)\n",
   214  				"b/go.mod":   "module golang.org/b\ngo 1.18\n",
   215  				"b/b.go":     "package b",
   216  				"b/c/go.mod": "module golang.org/c\ngo 1.18\n",
   217  			},
   218  			[]folderSummary{{dir: "."}},
   219  			[]string{"a/a.go", "b/b.go", "b/c/c.go"},
   220  			[]viewSummary{{GoWorkView, ".", nil}, {GoWorkView, "b", nil}},
   221  		},
   222  		{
   223  			"multiple go.work, c unused",
   224  			map[string]string{
   225  				"go.work":    "go 1.18\nuse (\n\t./a\n\t./b\n)\n",
   226  				"a/go.mod":   "module golang.org/a\ngo 1.18\n",
   227  				"a/a.go":     "package a",
   228  				"b/go.work":  "go 1.18\nuse (\n\t.\n)\n",
   229  				"b/go.mod":   "module golang.org/b\ngo 1.18\n",
   230  				"b/b.go":     "package b",
   231  				"b/c/go.mod": "module golang.org/c\ngo 1.18\n",
   232  			},
   233  			[]folderSummary{{dir: "."}},
   234  			[]string{"a/a.go", "b/b.go", "b/c/c.go"},
   235  			[]viewSummary{{GoWorkView, ".", nil}, {GoModView, "b/c", []string{"GOWORK=off"}}},
   236  		},
   237  		{
   238  			"go.mod with nested replace",
   239  			map[string]string{
   240  				"go.mod":   "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b",
   241  				"a.go":     "package a",
   242  				"b/go.mod": "module golang.org/b\ngo 1.18\n",
   243  				"b/b.go":   "package b",
   244  			},
   245  			[]folderSummary{{dir: ".", options: includeReplaceInWorkspace}},
   246  			[]string{"a/a.go", "b/b.go"},
   247  			[]viewSummary{{GoModView, ".", nil}},
   248  		},
   249  		{
   250  			"go.mod with parent replace, parent folder",
   251  			map[string]string{
   252  				"go.mod":   "module golang.org/a",
   253  				"a.go":     "package a",
   254  				"b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../",
   255  				"b/b.go":   "package b",
   256  			},
   257  			[]folderSummary{{dir: ".", options: includeReplaceInWorkspace}},
   258  			[]string{"a/a.go", "b/b.go"},
   259  			[]viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}},
   260  		},
   261  		{
   262  			"go.mod with multiple replace",
   263  			map[string]string{
   264  				"go.mod": `
   265  module golang.org/root
   266  
   267  require (
   268  	golang.org/a v1.2.3
   269  	golang.org/b v1.2.3
   270  	golang.org/c v1.2.3
   271  )
   272  
   273  replace (
   274  	golang.org/b => ./b
   275  	golang.org/c => ./c
   276  	// Note: d is not replaced
   277  )
   278  `,
   279  				"a.go":     "package a",
   280  				"b/go.mod": "module golang.org/b\ngo 1.18",
   281  				"b/b.go":   "package b",
   282  				"c/go.mod": "module golang.org/c\ngo 1.18",
   283  				"c/c.go":   "package c",
   284  				"d/go.mod": "module golang.org/d\ngo 1.18",
   285  				"d/d.go":   "package d",
   286  			},
   287  			[]folderSummary{{dir: ".", options: includeReplaceInWorkspace}},
   288  			[]string{"b/b.go", "c/c.go", "d/d.go"},
   289  			[]viewSummary{{GoModView, ".", nil}, {GoModView, "d", nil}},
   290  		},
   291  		{
   292  			"go.mod with replace outside the workspace",
   293  			map[string]string{
   294  				"go.mod":   "module golang.org/a\ngo 1.18",
   295  				"a.go":     "package a",
   296  				"b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../",
   297  				"b/b.go":   "package b",
   298  			},
   299  			[]folderSummary{{dir: "b"}},
   300  			[]string{"a.go", "b/b.go"},
   301  			[]viewSummary{{GoModView, "b", nil}},
   302  		},
   303  		{
   304  			"go.mod with replace directive; workspace replace off",
   305  			map[string]string{
   306  				"go.mod":   "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b",
   307  				"a.go":     "package a",
   308  				"b/go.mod": "module golang.org/b\ngo 1.18\n",
   309  				"b/b.go":   "package b",
   310  			},
   311  			[]folderSummary{{
   312  				dir: ".",
   313  				options: func(string) map[string]any {
   314  					return map[string]any{
   315  						"includeReplaceInWorkspace": false,
   316  					}
   317  				},
   318  			}},
   319  			[]string{"a/a.go", "b/b.go"},
   320  			[]viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}},
   321  		},
   322  	}
   323  
   324  	for _, test := range tests {
   325  		ctx := context.Background()
   326  		t.Run(test.name, func(t *testing.T) {
   327  			dir := writeFiles(t, test.files)
   328  			rel := fake.RelativeTo(dir)
   329  			fs := newMemoizedFS()
   330  
   331  			toURI := func(path string) protocol.DocumentURI {
   332  				return protocol.URIFromPath(rel.AbsPath(path))
   333  			}
   334  
   335  			var folders []*Folder
   336  			for _, f := range test.folders {
   337  				opts := settings.DefaultOptions()
   338  				if f.options != nil {
   339  					results := settings.SetOptions(opts, f.options(dir))
   340  					for _, r := range results {
   341  						if r.Error != nil {
   342  							t.Fatalf("setting option %v: %v", r.Name, r.Error)
   343  						}
   344  					}
   345  				}
   346  				env, err := FetchGoEnv(ctx, toURI(f.dir), opts)
   347  				if err != nil {
   348  					t.Fatalf("FetchGoEnv failed: %v", err)
   349  				}
   350  				folders = append(folders, &Folder{
   351  					Dir:     toURI(f.dir),
   352  					Name:    path.Base(f.dir),
   353  					Options: opts,
   354  					Env:     *env,
   355  				})
   356  			}
   357  
   358  			var openFiles []protocol.DocumentURI
   359  			for _, path := range test.open {
   360  				openFiles = append(openFiles, toURI(path))
   361  			}
   362  
   363  			defs, err := selectViewDefs(ctx, fs, folders, openFiles)
   364  			if err != nil {
   365  				t.Fatal(err)
   366  			}
   367  			var got []viewSummary
   368  			for _, def := range defs {
   369  				got = append(got, viewSummary{
   370  					Type: def.Type(),
   371  					Root: rel.RelPath(def.root.Path()),
   372  					Env:  def.EnvOverlay(),
   373  				})
   374  			}
   375  			if diff := cmp.Diff(test.want, got); diff != "" {
   376  				t.Errorf("selectViews() mismatch (-want +got):\n%s", diff)
   377  			}
   378  		})
   379  	}
   380  }
   381  
   382  // TODO(rfindley): this function could be meaningfully factored with the
   383  // various other test helpers of this nature.
   384  func writeFiles(t *testing.T, files map[string]string) string {
   385  	root := t.TempDir()
   386  
   387  	// This unfortunate step is required because gopls output
   388  	// expands symbolic links in its input file names (arguably it
   389  	// should not), and on macOS the temp dir is in /var -> private/var.
   390  	root, err := filepath.EvalSymlinks(root)
   391  	if err != nil {
   392  		t.Fatal(err)
   393  	}
   394  
   395  	for name, content := range files {
   396  		filename := filepath.Join(root, name)
   397  		if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil {
   398  			t.Fatal(err)
   399  		}
   400  		if err := os.WriteFile(filename, []byte(content), 0666); err != nil {
   401  			t.Fatal(err)
   402  		}
   403  	}
   404  	return root
   405  }