github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/cache/workspace_test.go (about)

     1  // Copyright 2020 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 cache
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"os"
    11  	"strings"
    12  	"testing"
    13  
    14  	"golang.org/x/mod/modfile"
    15  	"github.com/powerman/golang-tools/internal/lsp/fake"
    16  	"github.com/powerman/golang-tools/internal/lsp/source"
    17  	"github.com/powerman/golang-tools/internal/span"
    18  )
    19  
    20  // osFileSource is a fileSource that just reads from the operating system.
    21  type osFileSource struct {
    22  	overlays map[span.URI]fakeOverlay
    23  }
    24  
    25  type fakeOverlay struct {
    26  	source.VersionedFileHandle
    27  	uri     span.URI
    28  	content string
    29  	err     error
    30  	saved   bool
    31  }
    32  
    33  func (o fakeOverlay) Saved() bool { return o.saved }
    34  
    35  func (o fakeOverlay) Read() ([]byte, error) {
    36  	if o.err != nil {
    37  		return nil, o.err
    38  	}
    39  	return []byte(o.content), nil
    40  }
    41  
    42  func (o fakeOverlay) URI() span.URI {
    43  	return o.uri
    44  }
    45  
    46  // change updates the file source with the given file content. For convenience,
    47  // empty content signals a deletion. If saved is true, these changes are
    48  // persisted to disk.
    49  func (s *osFileSource) change(ctx context.Context, uri span.URI, content string, saved bool) (*fileChange, error) {
    50  	if content == "" {
    51  		delete(s.overlays, uri)
    52  		if saved {
    53  			if err := os.Remove(uri.Filename()); err != nil {
    54  				return nil, err
    55  			}
    56  		}
    57  		fh, err := s.GetFile(ctx, uri)
    58  		if err != nil {
    59  			return nil, err
    60  		}
    61  		data, err := fh.Read()
    62  		return &fileChange{exists: err == nil, content: data, fileHandle: &closedFile{fh}}, nil
    63  	}
    64  	if s.overlays == nil {
    65  		s.overlays = map[span.URI]fakeOverlay{}
    66  	}
    67  	s.overlays[uri] = fakeOverlay{uri: uri, content: content, saved: saved}
    68  	return &fileChange{
    69  		exists:     content != "",
    70  		content:    []byte(content),
    71  		fileHandle: s.overlays[uri],
    72  	}, nil
    73  }
    74  
    75  func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
    76  	if overlay, ok := s.overlays[uri]; ok {
    77  		return overlay, nil
    78  	}
    79  	fi, statErr := os.Stat(uri.Filename())
    80  	if statErr != nil {
    81  		return &fileHandle{
    82  			err: statErr,
    83  			uri: uri,
    84  		}, nil
    85  	}
    86  	fh, err := readFile(ctx, uri, fi)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	return fh, nil
    91  }
    92  
    93  type wsState struct {
    94  	source  workspaceSource
    95  	modules []string
    96  	dirs    []string
    97  	sum     string
    98  }
    99  
   100  type wsChange struct {
   101  	content string
   102  	saved   bool
   103  }
   104  
   105  func TestWorkspaceModule(t *testing.T) {
   106  	tests := []struct {
   107  		desc         string
   108  		initial      string // txtar-encoded
   109  		legacyMode   bool
   110  		initialState wsState
   111  		updates      map[string]wsChange
   112  		wantChanged  bool
   113  		wantReload   bool
   114  		finalState   wsState
   115  	}{
   116  		{
   117  			desc: "legacy mode",
   118  			initial: `
   119  -- go.mod --
   120  module mod.com
   121  -- go.sum --
   122  golang.org/x/mod v0.3.0 h1:deadbeef
   123  -- a/go.mod --
   124  module moda.com`,
   125  			legacyMode: true,
   126  			initialState: wsState{
   127  				modules: []string{"./go.mod"},
   128  				source:  legacyWorkspace,
   129  				dirs:    []string{"."},
   130  				sum:     "golang.org/x/mod v0.3.0 h1:deadbeef\n",
   131  			},
   132  		},
   133  		{
   134  			desc: "nested module",
   135  			initial: `
   136  -- go.mod --
   137  module mod.com
   138  -- a/go.mod --
   139  module moda.com`,
   140  			initialState: wsState{
   141  				modules: []string{"./go.mod", "a/go.mod"},
   142  				source:  fileSystemWorkspace,
   143  				dirs:    []string{".", "a"},
   144  			},
   145  		},
   146  		{
   147  			desc: "removing module",
   148  			initial: `
   149  -- a/go.mod --
   150  module moda.com
   151  -- a/go.sum --
   152  golang.org/x/mod v0.3.0 h1:deadbeef
   153  -- b/go.mod --
   154  module modb.com
   155  -- b/go.sum --
   156  golang.org/x/mod v0.3.0 h1:beefdead`,
   157  			initialState: wsState{
   158  				modules: []string{"a/go.mod", "b/go.mod"},
   159  				source:  fileSystemWorkspace,
   160  				dirs:    []string{".", "a", "b"},
   161  				sum:     "golang.org/x/mod v0.3.0 h1:beefdead\ngolang.org/x/mod v0.3.0 h1:deadbeef\n",
   162  			},
   163  			updates: map[string]wsChange{
   164  				"gopls.mod": {`module gopls-workspace
   165  
   166  require moda.com v0.0.0-goplsworkspace
   167  replace moda.com => $SANDBOX_WORKDIR/a`, true},
   168  			},
   169  			wantChanged: true,
   170  			wantReload:  true,
   171  			finalState: wsState{
   172  				modules: []string{"a/go.mod"},
   173  				source:  goplsModWorkspace,
   174  				dirs:    []string{".", "a"},
   175  				sum:     "golang.org/x/mod v0.3.0 h1:deadbeef\n",
   176  			},
   177  		},
   178  		{
   179  			desc: "adding module",
   180  			initial: `
   181  -- gopls.mod --
   182  require moda.com v0.0.0-goplsworkspace
   183  replace moda.com => $SANDBOX_WORKDIR/a
   184  -- a/go.mod --
   185  module moda.com
   186  -- b/go.mod --
   187  module modb.com`,
   188  			initialState: wsState{
   189  				modules: []string{"a/go.mod"},
   190  				source:  goplsModWorkspace,
   191  				dirs:    []string{".", "a"},
   192  			},
   193  			updates: map[string]wsChange{
   194  				"gopls.mod": {`module gopls-workspace
   195  
   196  require moda.com v0.0.0-goplsworkspace
   197  require modb.com v0.0.0-goplsworkspace
   198  
   199  replace moda.com => $SANDBOX_WORKDIR/a
   200  replace modb.com => $SANDBOX_WORKDIR/b`, true},
   201  			},
   202  			wantChanged: true,
   203  			wantReload:  true,
   204  			finalState: wsState{
   205  				modules: []string{"a/go.mod", "b/go.mod"},
   206  				source:  goplsModWorkspace,
   207  				dirs:    []string{".", "a", "b"},
   208  			},
   209  		},
   210  		{
   211  			desc: "deleting gopls.mod",
   212  			initial: `
   213  -- gopls.mod --
   214  module gopls-workspace
   215  
   216  require moda.com v0.0.0-goplsworkspace
   217  replace moda.com => $SANDBOX_WORKDIR/a
   218  -- a/go.mod --
   219  module moda.com
   220  -- b/go.mod --
   221  module modb.com`,
   222  			initialState: wsState{
   223  				modules: []string{"a/go.mod"},
   224  				source:  goplsModWorkspace,
   225  				dirs:    []string{".", "a"},
   226  			},
   227  			updates: map[string]wsChange{
   228  				"gopls.mod": {"", true},
   229  			},
   230  			wantChanged: true,
   231  			wantReload:  true,
   232  			finalState: wsState{
   233  				modules: []string{"a/go.mod", "b/go.mod"},
   234  				source:  fileSystemWorkspace,
   235  				dirs:    []string{".", "a", "b"},
   236  			},
   237  		},
   238  		{
   239  			desc: "broken module parsing",
   240  			initial: `
   241  -- a/go.mod --
   242  module moda.com
   243  
   244  require gopls.test v0.0.0-goplsworkspace
   245  replace gopls.test => ../../gopls.test // (this path shouldn't matter)
   246  -- b/go.mod --
   247  module modb.com`,
   248  			initialState: wsState{
   249  				modules: []string{"a/go.mod", "b/go.mod"},
   250  				source:  fileSystemWorkspace,
   251  				dirs:    []string{".", "a", "b", "../gopls.test"},
   252  			},
   253  			updates: map[string]wsChange{
   254  				"a/go.mod": {`modul moda.com
   255  
   256  require gopls.test v0.0.0-goplsworkspace
   257  replace gopls.test => ../../gopls.test2`, false},
   258  			},
   259  			wantChanged: true,
   260  			wantReload:  false,
   261  			finalState: wsState{
   262  				modules: []string{"a/go.mod", "b/go.mod"},
   263  				source:  fileSystemWorkspace,
   264  				// finalDirs should be unchanged: we should preserve dirs in the presence
   265  				// of a broken modfile.
   266  				dirs: []string{".", "a", "b", "../gopls.test"},
   267  			},
   268  		},
   269  	}
   270  
   271  	for _, test := range tests {
   272  		t.Run(test.desc, func(t *testing.T) {
   273  			ctx := context.Background()
   274  			dir, err := fake.Tempdir(fake.UnpackTxt(test.initial))
   275  			if err != nil {
   276  				t.Fatal(err)
   277  			}
   278  			defer os.RemoveAll(dir)
   279  			root := span.URIFromPath(dir)
   280  
   281  			fs := &osFileSource{}
   282  			excludeNothing := func(string) bool { return false }
   283  			w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode)
   284  			if err != nil {
   285  				t.Fatal(err)
   286  			}
   287  			rel := fake.RelativeTo(dir)
   288  			checkState(ctx, t, fs, rel, w, test.initialState)
   289  
   290  			// Apply updates.
   291  			if test.updates != nil {
   292  				changes := make(map[span.URI]*fileChange)
   293  				for k, v := range test.updates {
   294  					content := strings.ReplaceAll(v.content, "$SANDBOX_WORKDIR", string(rel))
   295  					uri := span.URIFromPath(rel.AbsPath(k))
   296  					changes[uri], err = fs.change(ctx, uri, content, v.saved)
   297  					if err != nil {
   298  						t.Fatal(err)
   299  					}
   300  				}
   301  				got, gotChanged, gotReload := w.invalidate(ctx, changes, fs)
   302  				if gotChanged != test.wantChanged {
   303  					t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged)
   304  				}
   305  				if gotReload != test.wantReload {
   306  					t.Errorf("w.invalidate(): got reload %t, want %t", gotReload, test.wantReload)
   307  				}
   308  				checkState(ctx, t, fs, rel, got, test.finalState)
   309  			}
   310  		})
   311  	}
   312  }
   313  
   314  func workspaceFromTxtar(t *testing.T, files string) (*workspace, func(), error) {
   315  	ctx := context.Background()
   316  	dir, err := fake.Tempdir(fake.UnpackTxt(files))
   317  	if err != nil {
   318  		return nil, func() {}, err
   319  	}
   320  	cleanup := func() {
   321  		os.RemoveAll(dir)
   322  	}
   323  	root := span.URIFromPath(dir)
   324  
   325  	fs := &osFileSource{}
   326  	excludeNothing := func(string) bool { return false }
   327  	workspace, err := newWorkspace(ctx, root, fs, excludeNothing, false, false)
   328  	return workspace, cleanup, err
   329  }
   330  
   331  func TestWorkspaceParseError(t *testing.T) {
   332  	w, cleanup, err := workspaceFromTxtar(t, `
   333  -- go.work --
   334  go 1.18
   335  
   336  usa ./typo
   337  -- typo/go.mod --
   338  module foo
   339  `)
   340  	defer cleanup()
   341  	if err != nil {
   342  		t.Fatalf("error creating workspace: %v; want no error", err)
   343  	}
   344  	w.buildMu.Lock()
   345  	built, buildErr := w.built, w.buildErr
   346  	w.buildMu.Unlock()
   347  	if !built || buildErr == nil {
   348  		t.Fatalf("built, buildErr: got %v, %v; want true, non-nil", built, buildErr)
   349  	}
   350  	var errList modfile.ErrorList
   351  	if !errors.As(buildErr, &errList) {
   352  		t.Fatalf("expected error to be an errorlist; got %v", buildErr)
   353  	}
   354  	if len(errList) != 1 {
   355  		t.Fatalf("expected errorList to have one element; got %v elements", len(errList))
   356  	}
   357  	parseErr := errList[0]
   358  	if parseErr.Pos.Line != 3 {
   359  		t.Fatalf("expected error to be on line 3; got %v", parseErr.Pos.Line)
   360  	}
   361  }
   362  
   363  func TestWorkspaceMissingModFile(t *testing.T) {
   364  	w, cleanup, err := workspaceFromTxtar(t, `
   365  -- go.work --
   366  go 1.18
   367  
   368  use ./missing
   369  `)
   370  	defer cleanup()
   371  	if err != nil {
   372  		t.Fatalf("error creating workspace: %v; want no error", err)
   373  	}
   374  	w.buildMu.Lock()
   375  	built, buildErr := w.built, w.buildErr
   376  	w.buildMu.Unlock()
   377  	if !built || buildErr == nil {
   378  		t.Fatalf("built, buildErr: got %v, %v; want true, non-nil", built, buildErr)
   379  	}
   380  }
   381  
   382  func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fake.RelativeTo, got *workspace, want wsState) {
   383  	t.Helper()
   384  	if got.moduleSource != want.source {
   385  		t.Errorf("module source = %v, want %v", got.moduleSource, want.source)
   386  	}
   387  	modules := make(map[span.URI]struct{})
   388  	for k := range got.getActiveModFiles() {
   389  		modules[k] = struct{}{}
   390  	}
   391  	for _, modPath := range want.modules {
   392  		path := rel.AbsPath(modPath)
   393  		uri := span.URIFromPath(path)
   394  		if _, ok := modules[uri]; !ok {
   395  			t.Errorf("missing module %q", uri)
   396  		}
   397  		delete(modules, uri)
   398  	}
   399  	for remaining := range modules {
   400  		t.Errorf("unexpected module %q", remaining)
   401  	}
   402  	gotDirs := got.dirs(ctx, fs)
   403  	gotM := make(map[span.URI]bool)
   404  	for _, dir := range gotDirs {
   405  		gotM[dir] = true
   406  	}
   407  	for _, dir := range want.dirs {
   408  		path := rel.AbsPath(dir)
   409  		uri := span.URIFromPath(path)
   410  		if !gotM[uri] {
   411  			t.Errorf("missing dir %q", uri)
   412  		}
   413  		delete(gotM, uri)
   414  	}
   415  	for remaining := range gotM {
   416  		t.Errorf("unexpected dir %q", remaining)
   417  	}
   418  	gotSumBytes, err := got.sumFile(ctx, fs)
   419  	if err != nil {
   420  		t.Fatal(err)
   421  	}
   422  	if gotSum := string(gotSumBytes); gotSum != want.sum {
   423  		t.Errorf("got final sum %q, want %q", gotSum, want.sum)
   424  	}
   425  }