| // 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 workspace |
| |
| import ( |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/cmpopts" |
| "golang.org/x/tools/gopls/internal/cache" |
| "golang.org/x/tools/gopls/internal/protocol/command" |
| |
| . "golang.org/x/tools/gopls/internal/test/integration" |
| ) |
| |
| func TestAddAndRemoveGoWork(t *testing.T) { |
| // Use a workspace with a module in the root directory to exercise the case |
| // where a go.work is added to the existing root directory. This verifies |
| // that we're detecting changes to the module source, not just the root |
| // directory. |
| const nomod = ` |
| -- go.mod -- |
| module a.com |
| |
| go 1.16 |
| -- main.go -- |
| package main |
| |
| func main() {} |
| -- b/go.mod -- |
| module b.com |
| |
| go 1.16 |
| -- b/main.go -- |
| package main |
| |
| func main() {} |
| ` |
| WithOptions( |
| Modes(Default), |
| ).Run(t, nomod, func(t *testing.T, env *Env) { |
| env.OpenFile("main.go") |
| env.OpenFile("b/main.go") |
| |
| summary := func(typ cache.ViewType, root, folder string) command.View { |
| return command.View{ |
| Type: typ.String(), |
| Root: env.Sandbox.Workdir.URI(root), |
| Folder: env.Sandbox.Workdir.URI(folder), |
| } |
| } |
| checkViews := func(want ...command.View) { |
| got := env.Views() |
| if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(command.View{}, "ID")); diff != "" { |
| t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff) |
| } |
| } |
| |
| // Zero-config gopls makes this work. |
| env.AfterChange( |
| NoDiagnostics(ForFile("main.go")), |
| NoDiagnostics(env.AtRegexp("b/main.go", "package (main)")), |
| ) |
| checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) |
| |
| env.WriteWorkspaceFile("go.work", `go 1.16 |
| |
| use ( |
| . |
| b |
| ) |
| `) |
| env.AfterChange(NoDiagnostics()) |
| checkViews(summary(cache.GoWorkView, ".", ".")) |
| |
| // Removing the go.work file should put us back where we started. |
| env.RemoveWorkspaceFile("go.work") |
| |
| // Again, zero-config gopls makes this work. |
| env.AfterChange( |
| NoDiagnostics(ForFile("main.go")), |
| NoDiagnostics(env.AtRegexp("b/main.go", "package (main)")), |
| ) |
| checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) |
| |
| // Close and reopen b, to ensure the views are adjusted accordingly. |
| env.CloseBuffer("b/main.go") |
| env.AfterChange() |
| checkViews(summary(cache.GoModView, ".", ".")) |
| |
| env.OpenFile("b/main.go") |
| env.AfterChange() |
| checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) |
| }) |
| } |
| |
| func TestOpenAndClosePorts(t *testing.T) { |
| // This test checks that as we open and close files requiring a different |
| // port, the set of Views is adjusted accordingly. |
| const files = ` |
| -- go.mod -- |
| module a.com/a |
| |
| go 1.20 |
| |
| -- a_linux.go -- |
| package a |
| |
| -- a_darwin.go -- |
| package a |
| |
| -- a_windows.go -- |
| package a |
| ` |
| |
| WithOptions( |
| EnvVars{ |
| "GOOS": "linux", // assume that linux is the default GOOS |
| }, |
| ).Run(t, files, func(t *testing.T, env *Env) { |
| summary := func(envOverlay ...string) command.View { |
| return command.View{ |
| Type: cache.GoModView.String(), |
| Root: env.Sandbox.Workdir.URI("."), |
| Folder: env.Sandbox.Workdir.URI("."), |
| EnvOverlay: envOverlay, |
| } |
| } |
| checkViews := func(want ...command.View) { |
| got := env.Views() |
| if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(command.View{}, "ID")); diff != "" { |
| t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff) |
| } |
| } |
| checkViews(summary()) |
| env.OpenFile("a_linux.go") |
| checkViews(summary()) |
| env.OpenFile("a_darwin.go") |
| checkViews( |
| summary(), |
| summary("GOARCH=amd64", "GOOS=darwin"), |
| ) |
| env.OpenFile("a_windows.go") |
| checkViews( |
| summary(), |
| summary("GOARCH=amd64", "GOOS=darwin"), |
| summary("GOARCH=amd64", "GOOS=windows"), |
| ) |
| env.CloseBuffer("a_darwin.go") |
| checkViews( |
| summary(), |
| summary("GOARCH=amd64", "GOOS=windows"), |
| ) |
| env.CloseBuffer("a_linux.go") |
| checkViews( |
| summary(), |
| summary("GOARCH=amd64", "GOOS=windows"), |
| ) |
| env.CloseBuffer("a_windows.go") |
| checkViews(summary()) |
| }) |
| } |
| |
| func TestCriticalErrorsInOrphanedFiles(t *testing.T) { |
| // This test checks that as we open and close files requiring a different |
| // port, the set of Views is adjusted accordingly. |
| const files = ` |
| -- go.mod -- |
| modul golang.org/lsptests/broken |
| |
| go 1.20 |
| |
| -- a.go -- |
| package broken |
| |
| const C = 0 |
| ` |
| |
| Run(t, files, func(t *testing.T, env *Env) { |
| env.OpenFile("a.go") |
| env.AfterChange( |
| Diagnostics(env.AtRegexp("go.mod", "modul")), |
| Diagnostics(env.AtRegexp("a.go", "broken"), WithMessage("initialization failed")), |
| ) |
| }) |
| } |
| |
| func TestGoModReplace(t *testing.T) { |
| // This test checks that we treat locally replaced modules as workspace |
| // modules, according to the "includeReplaceInWorkspace" setting. |
| const files = ` |
| -- moda/go.mod -- |
| module golang.org/a |
| |
| require golang.org/b v1.2.3 |
| |
| replace golang.org/b => ../modb |
| |
| go 1.20 |
| |
| -- moda/a.go -- |
| package a |
| |
| import "golang.org/b" |
| |
| const A = b.B |
| |
| -- modb/go.mod -- |
| module golang.org/b |
| |
| go 1.20 |
| |
| -- modb/b.go -- |
| package b |
| |
| const B = 1 |
| ` |
| |
| for useReplace, expectation := range map[bool]Expectation{ |
| true: FileWatchMatching("modb"), |
| false: NoFileWatchMatching("modb"), |
| } { |
| WithOptions( |
| WorkspaceFolders("moda"), |
| Settings{ |
| "includeReplaceInWorkspace": useReplace, |
| }, |
| ).Run(t, files, func(t *testing.T, env *Env) { |
| env.OnceMet( |
| InitialWorkspaceLoad, |
| expectation, |
| ) |
| }) |
| } |
| } |
| |
| func TestDisableZeroConfig(t *testing.T) { |
| // This test checks that we treat locally replaced modules as workspace |
| // modules, according to the "includeReplaceInWorkspace" setting. |
| const files = ` |
| -- moda/go.mod -- |
| module golang.org/a |
| |
| go 1.20 |
| |
| -- moda/a.go -- |
| package a |
| |
| -- modb/go.mod -- |
| module golang.org/b |
| |
| go 1.20 |
| |
| -- modb/b.go -- |
| package b |
| |
| ` |
| |
| WithOptions( |
| Settings{"zeroConfig": false}, |
| ).Run(t, files, func(t *testing.T, env *Env) { |
| env.OpenFile("moda/a.go") |
| env.OpenFile("modb/b.go") |
| env.AfterChange() |
| if got := env.Views(); len(got) != 1 || got[0].Type != cache.AdHocView.String() { |
| t.Errorf("Views: got %v, want one adhoc view", got) |
| } |
| }) |
| } |
| |
| func TestVendorExcluded(t *testing.T) { |
| // Test that we don't create Views for vendored modules. |
| // |
| // We construct the vendor directory manually here, as `go mod vendor` will |
| // omit the go.mod file. This synthesizes the setup of Kubernetes, where the |
| // entire module is vendored through a symlinked directory. |
| const src = ` |
| -- go.mod -- |
| module example.com/a |
| |
| go 1.18 |
| |
| require other.com/b v1.0.0 |
| |
| -- a.go -- |
| package a |
| import "other.com/b" |
| var _ b.B |
| |
| -- vendor/modules.txt -- |
| # other.com/b v1.0.0 |
| ## explicit; go 1.14 |
| other.com/b |
| |
| -- vendor/other.com/b/go.mod -- |
| module other.com/b |
| go 1.14 |
| |
| -- vendor/other.com/b/b.go -- |
| package b |
| type B int |
| |
| func _() { |
| var V int // unused |
| } |
| ` |
| WithOptions( |
| Modes(Default), |
| ).Run(t, src, func(t *testing.T, env *Env) { |
| env.OpenFile("a.go") |
| env.AfterChange(NoDiagnostics()) |
| loc := env.GoToDefinition(env.RegexpSearch("a.go", `b\.(B)`)) |
| if !strings.Contains(string(loc.URI), "/vendor/") { |
| t.Fatalf("Definition(b.B) = %v, want vendored location", loc.URI) |
| } |
| env.AfterChange( |
| Diagnostics(env.AtRegexp("vendor/other.com/b/b.go", "V"), WithMessage("not used")), |
| ) |
| |
| if views := env.Views(); len(views) != 1 { |
| t.Errorf("After opening /vendor/, got %d views, want 1. Views:\n%v", len(views), views) |
| } |
| }) |
| } |