// 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 initialSum string updates map[string]string finalSource workspaceSource finalModules []string finalDirs []string finalSum string }{ { desc: "legacy mode", initial: ` -- go.mod -- module mod.com -- go.sum -- golang.org/x/mod v0.3.0 h1:deadbeef -- a/go.mod -- module moda.com`, legacyMode: true, initialModules: []string{"./go.mod"}, initialSource: legacyWorkspace, initialDirs: []string{"."}, initialSum: "golang.org/x/mod v0.3.0 h1:deadbeef\n", }, { 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 -- a/go.sum -- golang.org/x/mod v0.3.0 h1:deadbeef -- b/go.mod -- module modb.com -- b/go.sum -- golang.org/x/mod v0.3.0 h1:beefdead`, initialModules: []string{"a/go.mod", "b/go.mod"}, initialSource: fileSystemWorkspace, initialDirs: []string{".", "a", "b"}, initialSum: "golang.org/x/mod v0.3.0 h1:beefdead\ngolang.org/x/mod v0.3.0 h1:deadbeef\n", 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"}, finalSum: "golang.org/x/mod v0.3.0 h1:deadbeef\n", }, { 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{} excludeNothing := func(string) bool { return false } w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode) if err != nil { t.Fatal(err) } rel := fake.RelativeTo(dir) checkWorkspaceModule(t, rel, w, test.initialSource, test.initialModules) gotDirs := w.dirs(ctx, fs) checkWorkspaceDirs(t, rel, gotDirs, test.initialDirs) // Verify the initial sum. gotSumBytes, err := w.sumFile(ctx, fs) if err != nil { t.Fatal(err) } if gotSum := string(gotSumBytes); gotSum != test.initialSum { t.Errorf("got initial sum %q, want %q", gotSum, test.initialSum) } // Apply updates. 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}, } } w, _ := w.invalidate(ctx, changes) checkWorkspaceModule(t, rel, w, test.finalSource, test.finalModules) gotDirs := w.dirs(ctx, fs) checkWorkspaceDirs(t, rel, gotDirs, test.finalDirs) // Verify that the final sumfile reflects any changes (for example, // that modules may have gone out of scope). gotSumBytes, err := w.sumFile(ctx, fs) if err != nil { t.Fatal(err) } if gotSum := string(gotSumBytes); gotSum != test.finalSum { t.Errorf("got final sum %q, want %q", gotSum, test.finalSum) } } }) } } 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.getActiveModFiles() { 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) } }