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