// Copyright 2020 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" "testing" "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/span" ) // osFileSource is a fileSource that just reads from the operating system. type osFileSource struct{} func (s osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { fi, statErr := os.Stat(uri.Filename()) if statErr != nil { return &fileHandle{ err: statErr, uri: uri, }, nil } fh, err := readFile(ctx, uri, fi.ModTime()) if err != nil { return nil, err } return fh, nil } func TestWorkspaceModule(t *testing.T) { tests := []struct { desc string initial string // txtar-encoded legacyMode bool initialSource workspaceSource initialModules []string initialDirs []string updates map[string]string finalSource workspaceSource finalModules []string finalDirs []string }{ { desc: "legacy mode", initial: ` -- go.mod -- module mod.com -- a/go.mod -- module moda.com`, legacyMode: true, initialModules: []string{"./go.mod"}, initialSource: legacyWorkspace, initialDirs: []string{"."}, }, { desc: "nested module", initial: ` -- go.mod -- module mod.com -- a/go.mod -- module moda.com`, initialModules: []string{"./go.mod", "a/go.mod"}, initialSource: fileSystemWorkspace, initialDirs: []string{".", "a"}, }, { desc: "removing module", initial: ` -- a/go.mod -- module moda.com -- b/go.mod -- module modb.com`, initialModules: []string{"a/go.mod", "b/go.mod"}, initialSource: fileSystemWorkspace, initialDirs: []string{".", "a", "b"}, updates: map[string]string{ "gopls.mod": `module gopls-workspace require moda.com v0.0.0-goplsworkspace replace moda.com => $SANDBOX_WORKDIR/a`, }, finalModules: []string{"a/go.mod"}, finalSource: goplsModWorkspace, finalDirs: []string{".", "a"}, }, { desc: "adding module", initial: ` -- gopls.mod -- require moda.com v0.0.0-goplsworkspace replace moda.com => $SANDBOX_WORKDIR/a -- a/go.mod -- module moda.com -- b/go.mod -- module modb.com`, initialModules: []string{"a/go.mod"}, initialSource: goplsModWorkspace, initialDirs: []string{".", "a"}, updates: map[string]string{ "gopls.mod": `module gopls-workspace require moda.com v0.0.0-goplsworkspace require modb.com v0.0.0-goplsworkspace replace moda.com => $SANDBOX_WORKDIR/a replace modb.com => $SANDBOX_WORKDIR/b`, }, finalModules: []string{"a/go.mod", "b/go.mod"}, finalSource: goplsModWorkspace, finalDirs: []string{".", "a", "b"}, }, { desc: "deleting gopls.mod", initial: ` -- gopls.mod -- module gopls-workspace require moda.com v0.0.0-goplsworkspace replace moda.com => $SANDBOX_WORKDIR/a -- a/go.mod -- module moda.com -- b/go.mod -- module modb.com`, initialModules: []string{"a/go.mod"}, initialSource: goplsModWorkspace, initialDirs: []string{".", "a"}, updates: map[string]string{ "gopls.mod": "", }, finalModules: []string{"a/go.mod", "b/go.mod"}, finalSource: fileSystemWorkspace, finalDirs: []string{".", "a", "b"}, }, { desc: "broken module parsing", initial: ` -- a/go.mod -- module moda.com require gopls.test v0.0.0-goplsworkspace replace gopls.test => ../../gopls.test // (this path shouldn't matter) -- b/go.mod -- module modb.com`, initialModules: []string{"a/go.mod", "b/go.mod"}, initialSource: fileSystemWorkspace, initialDirs: []string{".", "a", "b", "../gopls.test"}, updates: map[string]string{ "a/go.mod": `modul moda.com require gopls.test v0.0.0-goplsworkspace replace gopls.test => ../../gopls.test2`, }, finalModules: []string{"a/go.mod", "b/go.mod"}, finalSource: fileSystemWorkspace, // finalDirs should be unchanged: we should preserve dirs in the presence // of a broken modfile. finalDirs: []string{".", "a", "b", "../gopls.test"}, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { ctx := context.Background() dir, err := fake.Tempdir(test.initial) if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) root := span.URIFromPath(dir) fs := osFileSource{} wm, err := newWorkspace(ctx, root, fs, !test.legacyMode) if err != nil { t.Fatal(err) } rel := fake.RelativeTo(dir) checkWorkspaceModule(t, rel, wm, test.initialSource, test.initialModules) gotDirs := wm.dirs(ctx, fs) checkWorkspaceDirs(t, rel, gotDirs, test.initialDirs) if test.updates != nil { changes := make(map[span.URI]*fileChange) for k, v := range test.updates { if v == "" { // for convenience, use this to signal a deletion. TODO: more doc err := os.Remove(rel.AbsPath(k)) if err != nil { t.Fatal(err) } } else { fake.WriteFileData(k, []byte(v), rel) } uri := span.URIFromPath(rel.AbsPath(k)) fh, err := fs.GetFile(ctx, uri) if err != nil { t.Fatal(err) } content, err := fh.Read() changes[uri] = &fileChange{ content: content, exists: err == nil, fileHandle: &closedFile{fh}, } } wm, _ := wm.invalidate(ctx, changes) checkWorkspaceModule(t, rel, wm, test.finalSource, test.finalModules) gotDirs := wm.dirs(ctx, fs) checkWorkspaceDirs(t, rel, gotDirs, test.finalDirs) } }) } } func checkWorkspaceModule(t *testing.T, rel fake.RelativeTo, got *workspace, wantSource workspaceSource, want []string) { t.Helper() if got.moduleSource != wantSource { t.Errorf("module source = %v, want %v", got.moduleSource, wantSource) } modules := make(map[span.URI]struct{}) for k := range got.activeModFiles() { modules[k] = struct{}{} } for _, modPath := range want { path := rel.AbsPath(modPath) uri := span.URIFromPath(path) if _, ok := modules[uri]; !ok { t.Errorf("missing module %q", uri) } delete(modules, uri) } for remaining := range modules { t.Errorf("unexpected module %q", remaining) } } func checkWorkspaceDirs(t *testing.T, rel fake.RelativeTo, got []span.URI, want []string) { t.Helper() gotM := make(map[span.URI]bool) for _, dir := range got { gotM[dir] = true } for _, dir := range want { path := rel.AbsPath(dir) uri := span.URIFromPath(path) if !gotM[uri] { t.Errorf("missing dir %q", uri) } delete(gotM, uri) } for remaining := range gotM { t.Errorf("unexpected dir %q", remaining) } }