golang.org/x/tools/gopls@v0.15.3/internal/test/integration/workspace/zero_config_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 workspace 6 7 import ( 8 "strings" 9 "testing" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/google/go-cmp/cmp/cmpopts" 13 "golang.org/x/tools/gopls/internal/cache" 14 "golang.org/x/tools/gopls/internal/protocol/command" 15 16 . "golang.org/x/tools/gopls/internal/test/integration" 17 ) 18 19 func TestAddAndRemoveGoWork(t *testing.T) { 20 // Use a workspace with a module in the root directory to exercise the case 21 // where a go.work is added to the existing root directory. This verifies 22 // that we're detecting changes to the module source, not just the root 23 // directory. 24 const nomod = ` 25 -- go.mod -- 26 module a.com 27 28 go 1.16 29 -- main.go -- 30 package main 31 32 func main() {} 33 -- b/go.mod -- 34 module b.com 35 36 go 1.16 37 -- b/main.go -- 38 package main 39 40 func main() {} 41 ` 42 WithOptions( 43 Modes(Default), 44 ).Run(t, nomod, func(t *testing.T, env *Env) { 45 env.OpenFile("main.go") 46 env.OpenFile("b/main.go") 47 48 summary := func(typ cache.ViewType, root, folder string) command.View { 49 return command.View{ 50 Type: typ.String(), 51 Root: env.Sandbox.Workdir.URI(root), 52 Folder: env.Sandbox.Workdir.URI(folder), 53 } 54 } 55 checkViews := func(want ...command.View) { 56 got := env.Views() 57 if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(command.View{}, "ID")); diff != "" { 58 t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff) 59 } 60 } 61 62 // Zero-config gopls makes this work. 63 env.AfterChange( 64 NoDiagnostics(ForFile("main.go")), 65 NoDiagnostics(env.AtRegexp("b/main.go", "package (main)")), 66 ) 67 checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) 68 69 env.WriteWorkspaceFile("go.work", `go 1.16 70 71 use ( 72 . 73 b 74 ) 75 `) 76 env.AfterChange(NoDiagnostics()) 77 checkViews(summary(cache.GoWorkView, ".", ".")) 78 79 // Removing the go.work file should put us back where we started. 80 env.RemoveWorkspaceFile("go.work") 81 82 // Again, zero-config gopls makes this work. 83 env.AfterChange( 84 NoDiagnostics(ForFile("main.go")), 85 NoDiagnostics(env.AtRegexp("b/main.go", "package (main)")), 86 ) 87 checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) 88 89 // Close and reopen b, to ensure the views are adjusted accordingly. 90 env.CloseBuffer("b/main.go") 91 env.AfterChange() 92 checkViews(summary(cache.GoModView, ".", ".")) 93 94 env.OpenFile("b/main.go") 95 env.AfterChange() 96 checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) 97 }) 98 } 99 100 func TestOpenAndClosePorts(t *testing.T) { 101 // This test checks that as we open and close files requiring a different 102 // port, the set of Views is adjusted accordingly. 103 const files = ` 104 -- go.mod -- 105 module a.com/a 106 107 go 1.20 108 109 -- a_linux.go -- 110 package a 111 112 -- a_darwin.go -- 113 package a 114 115 -- a_windows.go -- 116 package a 117 ` 118 119 WithOptions( 120 EnvVars{ 121 "GOOS": "linux", // assume that linux is the default GOOS 122 }, 123 ).Run(t, files, func(t *testing.T, env *Env) { 124 summary := func(envOverlay ...string) command.View { 125 return command.View{ 126 Type: cache.GoModView.String(), 127 Root: env.Sandbox.Workdir.URI("."), 128 Folder: env.Sandbox.Workdir.URI("."), 129 EnvOverlay: envOverlay, 130 } 131 } 132 checkViews := func(want ...command.View) { 133 got := env.Views() 134 if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(command.View{}, "ID")); diff != "" { 135 t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff) 136 } 137 } 138 checkViews(summary()) 139 env.OpenFile("a_linux.go") 140 checkViews(summary()) 141 env.OpenFile("a_darwin.go") 142 checkViews( 143 summary(), 144 summary("GOARCH=amd64", "GOOS=darwin"), 145 ) 146 env.OpenFile("a_windows.go") 147 checkViews( 148 summary(), 149 summary("GOARCH=amd64", "GOOS=darwin"), 150 summary("GOARCH=amd64", "GOOS=windows"), 151 ) 152 env.CloseBuffer("a_darwin.go") 153 checkViews( 154 summary(), 155 summary("GOARCH=amd64", "GOOS=windows"), 156 ) 157 env.CloseBuffer("a_linux.go") 158 checkViews( 159 summary(), 160 summary("GOARCH=amd64", "GOOS=windows"), 161 ) 162 env.CloseBuffer("a_windows.go") 163 checkViews(summary()) 164 }) 165 } 166 167 func TestCriticalErrorsInOrphanedFiles(t *testing.T) { 168 // This test checks that as we open and close files requiring a different 169 // port, the set of Views is adjusted accordingly. 170 const files = ` 171 -- go.mod -- 172 modul golang.org/lsptests/broken 173 174 go 1.20 175 176 -- a.go -- 177 package broken 178 179 const C = 0 180 ` 181 182 Run(t, files, func(t *testing.T, env *Env) { 183 env.OpenFile("a.go") 184 env.AfterChange( 185 Diagnostics(env.AtRegexp("go.mod", "modul")), 186 Diagnostics(env.AtRegexp("a.go", "broken"), WithMessage("initialization failed")), 187 ) 188 }) 189 } 190 191 func TestGoModReplace(t *testing.T) { 192 // This test checks that we treat locally replaced modules as workspace 193 // modules, according to the "includeReplaceInWorkspace" setting. 194 const files = ` 195 -- moda/go.mod -- 196 module golang.org/a 197 198 require golang.org/b v1.2.3 199 200 replace golang.org/b => ../modb 201 202 go 1.20 203 204 -- moda/a.go -- 205 package a 206 207 import "golang.org/b" 208 209 const A = b.B 210 211 -- modb/go.mod -- 212 module golang.org/b 213 214 go 1.20 215 216 -- modb/b.go -- 217 package b 218 219 const B = 1 220 ` 221 222 for useReplace, expectation := range map[bool]Expectation{ 223 true: FileWatchMatching("modb"), 224 false: NoFileWatchMatching("modb"), 225 } { 226 WithOptions( 227 WorkspaceFolders("moda"), 228 Settings{ 229 "includeReplaceInWorkspace": useReplace, 230 }, 231 ).Run(t, files, func(t *testing.T, env *Env) { 232 env.OnceMet( 233 InitialWorkspaceLoad, 234 expectation, 235 ) 236 }) 237 } 238 } 239 240 func TestDisableZeroConfig(t *testing.T) { 241 // This test checks that we treat locally replaced modules as workspace 242 // modules, according to the "includeReplaceInWorkspace" setting. 243 const files = ` 244 -- moda/go.mod -- 245 module golang.org/a 246 247 go 1.20 248 249 -- moda/a.go -- 250 package a 251 252 -- modb/go.mod -- 253 module golang.org/b 254 255 go 1.20 256 257 -- modb/b.go -- 258 package b 259 260 ` 261 262 WithOptions( 263 Settings{"zeroConfig": false}, 264 ).Run(t, files, func(t *testing.T, env *Env) { 265 env.OpenFile("moda/a.go") 266 env.OpenFile("modb/b.go") 267 env.AfterChange() 268 if got := env.Views(); len(got) != 1 || got[0].Type != cache.AdHocView.String() { 269 t.Errorf("Views: got %v, want one adhoc view", got) 270 } 271 }) 272 } 273 274 func TestVendorExcluded(t *testing.T) { 275 // Test that we don't create Views for vendored modules. 276 // 277 // We construct the vendor directory manually here, as `go mod vendor` will 278 // omit the go.mod file. This synthesizes the setup of Kubernetes, where the 279 // entire module is vendored through a symlinked directory. 280 const src = ` 281 -- go.mod -- 282 module example.com/a 283 284 go 1.18 285 286 require other.com/b v1.0.0 287 288 -- a.go -- 289 package a 290 import "other.com/b" 291 var _ b.B 292 293 -- vendor/modules.txt -- 294 # other.com/b v1.0.0 295 ## explicit; go 1.14 296 other.com/b 297 298 -- vendor/other.com/b/go.mod -- 299 module other.com/b 300 go 1.14 301 302 -- vendor/other.com/b/b.go -- 303 package b 304 type B int 305 306 func _() { 307 var V int // unused 308 } 309 ` 310 WithOptions( 311 Modes(Default), 312 ).Run(t, src, func(t *testing.T, env *Env) { 313 env.OpenFile("a.go") 314 env.AfterChange(NoDiagnostics()) 315 loc := env.GoToDefinition(env.RegexpSearch("a.go", `b\.(B)`)) 316 if !strings.Contains(string(loc.URI), "/vendor/") { 317 t.Fatalf("Definition(b.B) = %v, want vendored location", loc.URI) 318 } 319 env.AfterChange( 320 Diagnostics(env.AtRegexp("vendor/other.com/b/b.go", "V"), WithMessage("not used")), 321 ) 322 323 if views := env.Views(); len(views) != 1 { 324 t.Errorf("After opening /vendor/, got %d views, want 1. Views:\n%v", len(views), views) 325 } 326 }) 327 }