blob: fe4e55e3d7411c6ed191345769a8f6e06026d799 [file] [log] [blame] [edit]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cache
import (
"context"
"os"
"path"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/testenv"
)
func TestZeroConfigAlgorithm(t *testing.T) {
testenv.NeedsExec(t) // executes the Go command
t.Setenv("GOPACKAGESDRIVER", "off")
type viewSummary struct {
// fields exported for cmp.Diff
Type ViewType
Root string
Env []string
}
type folderSummary struct {
dir string
options func(dir string) map[string]any // options may refer to the temp dir
}
includeReplaceInWorkspace := func(string) map[string]any {
return map[string]any{
"includeReplaceInWorkspace": true,
}
}
type test struct {
name string
files map[string]string // use a map rather than txtar as file content is tiny
folders []folderSummary
open []string // open files
want []viewSummary
}
tests := []test{
// TODO(rfindley): add a test for GOPACKAGESDRIVER.
// Doing so doesn't yet work using options alone (user env is not honored)
// TODO(rfindley): add a test for degenerate cases, such as missing
// workspace folders (once we decide on the correct behavior).
{
"basic go.work workspace",
map[string]string{
"go.work": "go 1.18\nuse (\n\t./a\n\t./b\n)\n",
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
},
[]folderSummary{{dir: "."}},
nil,
[]viewSummary{{GoWorkView, ".", nil}},
},
{
"basic go.mod workspace",
map[string]string{
"go.mod": "module golang.org/a\ngo 1.18\n",
},
[]folderSummary{{dir: "."}},
nil,
[]viewSummary{{GoModView, ".", nil}},
},
{
"basic GOPATH workspace",
map[string]string{
"src/golang.org/a/a.go": "package a",
"src/golang.org/b/b.go": "package b",
},
[]folderSummary{{
dir: "src",
options: func(dir string) map[string]any {
return map[string]any{
"env": map[string]any{
"GOPATH": dir,
},
}
},
}},
[]string{"src/golang.org/a//a.go", "src/golang.org/b/b.go"},
[]viewSummary{{GOPATHView, "src", nil}},
},
{
"basic AdHoc workspace",
map[string]string{
"foo.go": "package foo",
},
[]folderSummary{{dir: "."}},
nil,
[]viewSummary{{AdHocView, ".", nil}},
},
{
"multi-folder workspace",
map[string]string{
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
},
[]folderSummary{{dir: "a"}, {dir: "b"}},
nil,
[]viewSummary{{GoModView, "a", nil}, {GoModView, "b", nil}},
},
{
"multi-module workspace",
map[string]string{
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
},
[]folderSummary{{dir: "."}},
nil,
[]viewSummary{{AdHocView, ".", nil}},
},
{
"zero-config open module",
map[string]string{
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"a/a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
"b/b.go": "package b",
},
[]folderSummary{{dir: "."}},
[]string{"a/a.go"},
[]viewSummary{
{AdHocView, ".", nil},
{GoModView, "a", nil},
},
},
{
"zero-config open modules",
map[string]string{
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"a/a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
"b/b.go": "package b",
},
[]folderSummary{{dir: "."}},
[]string{"a/a.go", "b/b.go"},
[]viewSummary{
{AdHocView, ".", nil},
{GoModView, "a", nil},
{GoModView, "b", nil},
},
},
{
"unified workspace",
map[string]string{
"go.work": "go 1.18\nuse (\n\t./a\n\t./b\n)\n",
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"a/a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
"b/b.go": "package b",
},
[]folderSummary{{dir: "."}},
[]string{"a/a.go", "b/b.go"},
[]viewSummary{{GoWorkView, ".", nil}},
},
{
"go.work from env",
map[string]string{
"nested/go.work": "go 1.18\nuse (\n\t../a\n\t../b\n)\n",
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"a/a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
"b/b.go": "package b",
},
[]folderSummary{{
dir: ".",
options: func(dir string) map[string]any {
return map[string]any{
"env": map[string]any{
"GOWORK": filepath.Join(dir, "nested", "go.work"),
},
}
},
}},
[]string{"a/a.go", "b/b.go"},
[]viewSummary{{GoWorkView, ".", nil}},
},
{
"independent module view",
map[string]string{
"go.work": "go 1.18\nuse (\n\t./a\n)\n", // not using b
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"a/a.go": "package a",
"b/go.mod": "module golang.org/a\ngo 1.18\n",
"b/b.go": "package b",
},
[]folderSummary{{dir: "."}},
[]string{"a/a.go", "b/b.go"},
[]viewSummary{
{GoWorkView, ".", nil},
{GoModView, "b", []string{"GOWORK=off"}},
},
},
{
"multiple go.work",
map[string]string{
"go.work": "go 1.18\nuse (\n\t./a\n\t./b\n)\n",
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"a/a.go": "package a",
"b/go.work": "go 1.18\nuse (\n\t.\n\t./c\n)\n",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
"b/b.go": "package b",
"b/c/go.mod": "module golang.org/c\ngo 1.18\n",
},
[]folderSummary{{dir: "."}},
[]string{"a/a.go", "b/b.go", "b/c/c.go"},
[]viewSummary{{GoWorkView, ".", nil}, {GoWorkView, "b", nil}},
},
{
"multiple go.work, c unused",
map[string]string{
"go.work": "go 1.18\nuse (\n\t./a\n\t./b\n)\n",
"a/go.mod": "module golang.org/a\ngo 1.18\n",
"a/a.go": "package a",
"b/go.work": "go 1.18\nuse (\n\t.\n)\n",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
"b/b.go": "package b",
"b/c/go.mod": "module golang.org/c\ngo 1.18\n",
},
[]folderSummary{{dir: "."}},
[]string{"a/a.go", "b/b.go", "b/c/c.go"},
[]viewSummary{{GoWorkView, ".", nil}, {GoModView, "b/c", []string{"GOWORK=off"}}},
},
{
"go.mod with nested replace",
map[string]string{
"go.mod": "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b",
"a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
"b/b.go": "package b",
},
[]folderSummary{{dir: ".", options: includeReplaceInWorkspace}},
[]string{"a/a.go", "b/b.go"},
[]viewSummary{{GoModView, ".", nil}},
},
{
"go.mod with parent replace, parent folder",
map[string]string{
"go.mod": "module golang.org/a",
"a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../",
"b/b.go": "package b",
},
[]folderSummary{{dir: ".", options: includeReplaceInWorkspace}},
[]string{"a/a.go", "b/b.go"},
[]viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}},
},
{
"go.mod with multiple replace",
map[string]string{
"go.mod": `
module golang.org/root
require (
golang.org/a v1.2.3
golang.org/b v1.2.3
golang.org/c v1.2.3
)
replace (
golang.org/b => ./b
golang.org/c => ./c
// Note: d is not replaced
)
`,
"a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18",
"b/b.go": "package b",
"c/go.mod": "module golang.org/c\ngo 1.18",
"c/c.go": "package c",
"d/go.mod": "module golang.org/d\ngo 1.18",
"d/d.go": "package d",
},
[]folderSummary{{dir: ".", options: includeReplaceInWorkspace}},
[]string{"b/b.go", "c/c.go", "d/d.go"},
[]viewSummary{{GoModView, ".", nil}, {GoModView, "d", nil}},
},
{
"go.mod with replace outside the workspace",
map[string]string{
"go.mod": "module golang.org/a\ngo 1.18",
"a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../",
"b/b.go": "package b",
},
[]folderSummary{{dir: "b"}},
[]string{"a.go", "b/b.go"},
[]viewSummary{{GoModView, "b", nil}},
},
{
"go.mod with replace directive; workspace replace off",
map[string]string{
"go.mod": "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b",
"a.go": "package a",
"b/go.mod": "module golang.org/b\ngo 1.18\n",
"b/b.go": "package b",
},
[]folderSummary{{
dir: ".",
options: func(string) map[string]any {
return map[string]any{
"includeReplaceInWorkspace": false,
}
},
}},
[]string{"a/a.go", "b/b.go"},
[]viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}},
},
}
for _, test := range tests {
ctx := context.Background()
t.Run(test.name, func(t *testing.T) {
dir := writeFiles(t, test.files)
rel := fake.RelativeTo(dir)
fs := newMemoizedFS()
toURI := func(path string) protocol.DocumentURI {
return protocol.URIFromPath(rel.AbsPath(path))
}
var folders []*Folder
for _, f := range test.folders {
opts := settings.DefaultOptions()
if f.options != nil {
for _, err := range opts.Set(f.options(dir)) {
t.Fatal(err)
}
}
env, err := FetchGoEnv(ctx, toURI(f.dir), opts)
if err != nil {
t.Fatalf("FetchGoEnv failed: %v", err)
}
folders = append(folders, &Folder{
Dir: toURI(f.dir),
Name: path.Base(f.dir),
Options: opts,
Env: *env,
})
}
var openFiles []protocol.DocumentURI
for _, path := range test.open {
openFiles = append(openFiles, toURI(path))
}
defs, err := selectViewDefs(ctx, fs, folders, openFiles)
if err != nil {
t.Fatal(err)
}
var got []viewSummary
for _, def := range defs {
got = append(got, viewSummary{
Type: def.Type(),
Root: rel.RelPath(def.root.Path()),
Env: def.EnvOverlay(),
})
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("selectViews() mismatch (-want +got):\n%s", diff)
}
})
}
}
// TODO(rfindley): this function could be meaningfully factored with the
// various other test helpers of this nature.
func writeFiles(t *testing.T, files map[string]string) string {
root := t.TempDir()
// This unfortunate step is required because gopls output
// expands symbolic links in its input file names (arguably it
// should not), and on macOS the temp dir is in /var -> private/var.
root, err := filepath.EvalSymlinks(root)
if err != nil {
t.Fatal(err)
}
for name, content := range files {
filename := filepath.Join(root, name)
if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filename, []byte(content), 0666); err != nil {
t.Fatal(err)
}
}
return root
}