golang.org/x/tools/gopls@v0.15.3/internal/test/marker/marker_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 marker
     6  
     7  // This file defines the marker test framework.
     8  // See doc.go for extensive documentation.
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"encoding/json"
    14  	"flag"
    15  	"fmt"
    16  	"go/token"
    17  	"go/types"
    18  	"io/fs"
    19  	"log"
    20  	"os"
    21  	"path"
    22  	"path/filepath"
    23  	"reflect"
    24  	"regexp"
    25  	"runtime"
    26  	"sort"
    27  	"strings"
    28  	"testing"
    29  
    30  	"github.com/google/go-cmp/cmp"
    31  
    32  	"golang.org/x/tools/go/expect"
    33  	"golang.org/x/tools/gopls/internal/cache"
    34  	"golang.org/x/tools/gopls/internal/debug"
    35  	"golang.org/x/tools/gopls/internal/hooks"
    36  	"golang.org/x/tools/gopls/internal/lsprpc"
    37  	"golang.org/x/tools/gopls/internal/protocol"
    38  	"golang.org/x/tools/gopls/internal/test/compare"
    39  	"golang.org/x/tools/gopls/internal/test/integration"
    40  	"golang.org/x/tools/gopls/internal/test/integration/fake"
    41  	"golang.org/x/tools/gopls/internal/util/bug"
    42  	"golang.org/x/tools/gopls/internal/util/safetoken"
    43  	"golang.org/x/tools/internal/diff"
    44  	"golang.org/x/tools/internal/diff/myers"
    45  	"golang.org/x/tools/internal/jsonrpc2"
    46  	"golang.org/x/tools/internal/jsonrpc2/servertest"
    47  	"golang.org/x/tools/internal/testenv"
    48  	"golang.org/x/tools/txtar"
    49  )
    50  
    51  var update = flag.Bool("update", false, "if set, update test data during marker tests")
    52  
    53  func TestMain(m *testing.M) {
    54  	bug.PanicOnBugs = true
    55  	testenv.ExitIfSmallMachine()
    56  	// Disable GOPACKAGESDRIVER, as it can cause spurious test failures.
    57  	os.Setenv("GOPACKAGESDRIVER", "off")
    58  	os.Exit(m.Run())
    59  }
    60  
    61  // Test runs the marker tests from the testdata directory.
    62  //
    63  // See package documentation for details on how marker tests work.
    64  //
    65  // These tests were inspired by (and in many places copied from) a previous
    66  // iteration of the marker tests built on top of the packagestest framework.
    67  // Key design decisions motivating this reimplementation are as follows:
    68  //   - The old tests had a single global session, causing interaction at a
    69  //     distance and several awkward workarounds.
    70  //   - The old tests could not be safely parallelized, because certain tests
    71  //     manipulated the server options
    72  //   - Relatedly, the old tests did not have a logic grouping of assertions into
    73  //     a single unit, resulting in clusters of files serving clusters of
    74  //     entangled assertions.
    75  //   - The old tests used locations in the source as test names and as the
    76  //     identity of golden content, meaning that a single edit could change the
    77  //     name of an arbitrary number of subtests, and making it difficult to
    78  //     manually edit golden content.
    79  //   - The old tests did not hew closely to LSP concepts, resulting in, for
    80  //     example, each marker implementation doing its own position
    81  //     transformations, and inventing its own mechanism for configuration.
    82  //   - The old tests had an ad-hoc session initialization process. The integration
    83  //     test environment has had more time devoted to its initialization, and has a
    84  //     more convenient API.
    85  //   - The old tests lacked documentation, and often had failures that were hard
    86  //     to understand. By starting from scratch, we can revisit these aspects.
    87  func Test(t *testing.T) {
    88  	if testing.Short() {
    89  		builder := os.Getenv("GO_BUILDER_NAME")
    90  		// Note that HasPrefix(builder, "darwin-" only matches legacy builders.
    91  		// LUCI builder names start with x_tools-goN.NN.
    92  		// We want to exclude solaris on both legacy and LUCI builders, as
    93  		// it is timing out.
    94  		if strings.HasPrefix(builder, "darwin-") || strings.Contains(builder, "solaris") {
    95  			t.Skip("golang/go#64473: skipping with -short: this test is too slow on darwin and solaris builders")
    96  		}
    97  	}
    98  	// The marker tests must be able to run go/packages.Load.
    99  	testenv.NeedsGoPackages(t)
   100  
   101  	const dir = "testdata"
   102  	tests, err := loadMarkerTests(dir)
   103  	if err != nil {
   104  		t.Fatal(err)
   105  	}
   106  
   107  	// Opt: use a shared cache.
   108  	cache := cache.New(nil)
   109  
   110  	for _, test := range tests {
   111  		test := test
   112  		t.Run(test.name, func(t *testing.T) {
   113  			t.Parallel()
   114  			if test.skipReason != "" {
   115  				t.Skip(test.skipReason)
   116  			}
   117  			for _, goos := range test.skipGOOS {
   118  				if runtime.GOOS == goos {
   119  					t.Skipf("skipping on %s due to -skip_goos", runtime.GOOS)
   120  				}
   121  			}
   122  
   123  			// TODO(rfindley): it may be more useful to have full support for build
   124  			// constraints.
   125  			if test.minGoVersion != "" {
   126  				var go1point int
   127  				if _, err := fmt.Sscanf(test.minGoVersion, "go1.%d", &go1point); err != nil {
   128  					t.Fatalf("parsing -min_go version: %v", err)
   129  				}
   130  				testenv.NeedsGo1Point(t, go1point)
   131  			}
   132  			if test.maxGoVersion != "" {
   133  				var go1point int
   134  				if _, err := fmt.Sscanf(test.maxGoVersion, "go1.%d", &go1point); err != nil {
   135  					t.Fatalf("parsing -max_go version: %v", err)
   136  				}
   137  				testenv.SkipAfterGo1Point(t, go1point)
   138  			}
   139  			if test.cgo {
   140  				testenv.NeedsTool(t, "cgo")
   141  			}
   142  			config := fake.EditorConfig{
   143  				Settings:         test.settings,
   144  				CapabilitiesJSON: test.capabilities,
   145  				Env:              test.env,
   146  			}
   147  			if _, ok := config.Settings["diagnosticsDelay"]; !ok {
   148  				if config.Settings == nil {
   149  					config.Settings = make(map[string]any)
   150  				}
   151  				config.Settings["diagnosticsDelay"] = "10ms"
   152  			}
   153  			// inv: config.Settings != nil
   154  
   155  			run := &markerTestRun{
   156  				test:       test,
   157  				env:        newEnv(t, cache, test.files, test.proxyFiles, test.writeGoSum, config),
   158  				settings:   config.Settings,
   159  				values:     make(map[expect.Identifier]any),
   160  				diags:      make(map[protocol.Location][]protocol.Diagnostic),
   161  				extraNotes: make(map[protocol.DocumentURI]map[string][]*expect.Note),
   162  			}
   163  			// TODO(rfindley): make it easier to clean up the integration test environment.
   164  			defer run.env.Editor.Shutdown(context.Background()) // ignore error
   165  			defer run.env.Sandbox.Close()                       // ignore error
   166  
   167  			// Open all files so that we operate consistently with LSP clients, and
   168  			// (pragmatically) so that we have a Mapper available via the fake
   169  			// editor.
   170  			//
   171  			// This also allows avoiding mutating the editor state in tests.
   172  			for file := range test.files {
   173  				run.env.OpenFile(file)
   174  			}
   175  			// Wait for the didOpen notifications to be processed, then collect
   176  			// diagnostics.
   177  			var diags map[string]*protocol.PublishDiagnosticsParams
   178  			run.env.AfterChange(integration.ReadAllDiagnostics(&diags))
   179  			for path, params := range diags {
   180  				uri := run.env.Sandbox.Workdir.URI(path)
   181  				for _, diag := range params.Diagnostics {
   182  					loc := protocol.Location{
   183  						URI: uri,
   184  						Range: protocol.Range{
   185  							Start: diag.Range.Start,
   186  							End:   diag.Range.Start, // ignore end positions
   187  						},
   188  					}
   189  					run.diags[loc] = append(run.diags[loc], diag)
   190  				}
   191  			}
   192  
   193  			var markers []marker
   194  			for _, note := range test.notes {
   195  				mark := marker{run: run, note: note}
   196  				if fn, ok := valueMarkerFuncs[note.Name]; ok {
   197  					fn(mark)
   198  				} else if _, ok := actionMarkerFuncs[note.Name]; ok {
   199  					markers = append(markers, mark) // save for later
   200  				} else {
   201  					uri := mark.uri()
   202  					if run.extraNotes[uri] == nil {
   203  						run.extraNotes[uri] = make(map[string][]*expect.Note)
   204  					}
   205  					run.extraNotes[uri][note.Name] = append(run.extraNotes[uri][note.Name], note)
   206  				}
   207  			}
   208  
   209  			// Invoke each remaining marker in the test.
   210  			for _, mark := range markers {
   211  				actionMarkerFuncs[mark.note.Name](mark)
   212  			}
   213  
   214  			// Any remaining (un-eliminated) diagnostics are an error.
   215  			if !test.ignoreExtraDiags {
   216  				for loc, diags := range run.diags {
   217  					for _, diag := range diags {
   218  						t.Errorf("%s: unexpected diagnostic: %q", run.fmtLoc(loc), diag.Message)
   219  					}
   220  				}
   221  			}
   222  
   223  			// TODO(rfindley): use these for whole-file marker tests.
   224  			for uri, extras := range run.extraNotes {
   225  				for name, extra := range extras {
   226  					if len(extra) > 0 {
   227  						t.Errorf("%s: %d unused %q markers", run.env.Sandbox.Workdir.URIToPath(uri), len(extra), name)
   228  					}
   229  				}
   230  			}
   231  
   232  			formatted, err := formatTest(test)
   233  			if err != nil {
   234  				t.Errorf("formatTest: %v", err)
   235  			} else if *update {
   236  				filename := filepath.Join(dir, test.name)
   237  				if err := os.WriteFile(filename, formatted, 0644); err != nil {
   238  					t.Error(err)
   239  				}
   240  			} else {
   241  				// On go 1.19 and later, verify that the testdata has not changed.
   242  				//
   243  				// On earlier Go versions, the golden test data varies due to different
   244  				// markdown escaping.
   245  				//
   246  				// Only check this if the test hasn't already failed, otherwise we'd
   247  				// report duplicate mismatches of golden data.
   248  				if testenv.Go1Point() >= 19 && !t.Failed() {
   249  					// Otherwise, verify that formatted content matches.
   250  					if diff := compare.NamedText("formatted", "on-disk", string(formatted), string(test.content)); diff != "" {
   251  						t.Errorf("formatted test does not match on-disk content:\n%s", diff)
   252  					}
   253  				}
   254  			}
   255  		})
   256  	}
   257  
   258  	if abs, err := filepath.Abs(dir); err == nil && t.Failed() {
   259  		t.Logf("(Filenames are relative to %s.)", abs)
   260  	}
   261  }
   262  
   263  // A marker holds state for the execution of a single @marker
   264  // annotation in the source.
   265  type marker struct {
   266  	run  *markerTestRun
   267  	note *expect.Note
   268  }
   269  
   270  // ctx returns the mark context.
   271  func (m marker) ctx() context.Context { return m.run.env.Ctx }
   272  
   273  // T returns the testing.TB for this mark.
   274  func (m marker) T() testing.TB { return m.run.env.T }
   275  
   276  // server returns the LSP server for the marker test run.
   277  func (m marker) editor() *fake.Editor { return m.run.env.Editor }
   278  
   279  // server returns the LSP server for the marker test run.
   280  func (m marker) server() protocol.Server { return m.run.env.Editor.Server }
   281  
   282  // uri returns the URI of the file containing the marker.
   283  func (mark marker) uri() protocol.DocumentURI {
   284  	return mark.run.env.Sandbox.Workdir.URI(mark.run.test.fset.File(mark.note.Pos).Name())
   285  }
   286  
   287  // document returns a protocol.TextDocumentIdentifier for the current file.
   288  func (mark marker) document() protocol.TextDocumentIdentifier {
   289  	return protocol.TextDocumentIdentifier{URI: mark.uri()}
   290  }
   291  
   292  // path returns the relative path to the file containing the marker.
   293  func (mark marker) path() string {
   294  	return mark.run.env.Sandbox.Workdir.RelPath(mark.run.test.fset.File(mark.note.Pos).Name())
   295  }
   296  
   297  // mapper returns a *protocol.Mapper for the current file.
   298  func (mark marker) mapper() *protocol.Mapper {
   299  	mapper, err := mark.editor().Mapper(mark.path())
   300  	if err != nil {
   301  		mark.T().Fatalf("failed to get mapper for current mark: %v", err)
   302  	}
   303  	return mapper
   304  }
   305  
   306  // errorf reports an error with a prefix indicating the position of the marker note.
   307  //
   308  // It formats the error message using mark.sprintf.
   309  func (mark marker) errorf(format string, args ...any) {
   310  	msg := mark.sprintf(format, args...)
   311  	// TODO(adonovan): consider using fmt.Fprintf(os.Stderr)+t.Fail instead of
   312  	// t.Errorf to avoid reporting uninteresting positions in the Go source of
   313  	// the driver. However, this loses the order of stderr wrt "FAIL: TestFoo"
   314  	// subtest dividers.
   315  	mark.T().Errorf("%s: %s", mark.run.fmtPos(mark.note.Pos), msg)
   316  }
   317  
   318  // valueMarkerFunc returns a wrapper around a function that allows it to be
   319  // called during the processing of value markers (e.g. @value(v, 123)) with marker
   320  // arguments converted to function parameters. The provided function's first
   321  // parameter must be of type 'marker', and it must return a value.
   322  //
   323  // Unlike action markers, which are executed for actions such as test
   324  // assertions, value markers are all evaluated first, and each computes
   325  // a value that is recorded by its identifier, which is the marker's first
   326  // argument. These values may be referred to from an action marker by
   327  // this identifier, e.g. @action(... , v, ...).
   328  //
   329  // For example, given a fn with signature
   330  //
   331  //	func(mark marker, label, details, kind string) CompletionItem
   332  //
   333  // The result of valueMarkerFunc can associated with @item notes, and invoked
   334  // as follows:
   335  //
   336  //	//@item(FooCompletion, "Foo", "func() int", "func")
   337  //
   338  // The provided fn should not mutate the test environment.
   339  func valueMarkerFunc(fn any) func(marker) {
   340  	ftype := reflect.TypeOf(fn)
   341  	if ftype.NumIn() == 0 || ftype.In(0) != markerType {
   342  		panic(fmt.Sprintf("value marker function %#v must accept marker as its first argument", ftype))
   343  	}
   344  	if ftype.NumOut() != 1 {
   345  		panic(fmt.Sprintf("value marker function %#v must have exactly 1 result", ftype))
   346  	}
   347  
   348  	return func(mark marker) {
   349  		if len(mark.note.Args) == 0 || !is[expect.Identifier](mark.note.Args[0]) {
   350  			mark.errorf("first argument to a value marker function must be an identifier")
   351  			return
   352  		}
   353  		id := mark.note.Args[0].(expect.Identifier)
   354  		if alt, ok := mark.run.values[id]; ok {
   355  			mark.errorf("%s already declared as %T", id, alt)
   356  			return
   357  		}
   358  		args := append([]any{mark}, mark.note.Args[1:]...)
   359  		argValues, err := convertArgs(mark, ftype, args)
   360  		if err != nil {
   361  			mark.errorf("converting args: %v", err)
   362  			return
   363  		}
   364  		results := reflect.ValueOf(fn).Call(argValues)
   365  		mark.run.values[id] = results[0].Interface()
   366  	}
   367  }
   368  
   369  // actionMarkerFunc returns a wrapper around a function that allows it to be
   370  // called during the processing of action markers (e.g. @action("abc", 123))
   371  // with marker arguments converted to function parameters. The provided
   372  // function's first parameter must be of type 'marker', and it must not return
   373  // any values.
   374  //
   375  // The provided fn should not mutate the test environment.
   376  func actionMarkerFunc(fn any) func(marker) {
   377  	ftype := reflect.TypeOf(fn)
   378  	if ftype.NumIn() == 0 || ftype.In(0) != markerType {
   379  		panic(fmt.Sprintf("action marker function %#v must accept marker as its first argument", ftype))
   380  	}
   381  	if ftype.NumOut() != 0 {
   382  		panic(fmt.Sprintf("action marker function %#v cannot have results", ftype))
   383  	}
   384  
   385  	return func(mark marker) {
   386  		args := append([]any{mark}, mark.note.Args...)
   387  		argValues, err := convertArgs(mark, ftype, args)
   388  		if err != nil {
   389  			mark.errorf("converting args: %v", err)
   390  			return
   391  		}
   392  		reflect.ValueOf(fn).Call(argValues)
   393  	}
   394  }
   395  
   396  func convertArgs(mark marker, ftype reflect.Type, args []any) ([]reflect.Value, error) {
   397  	var (
   398  		argValues []reflect.Value
   399  		pnext     int          // next param index
   400  		p         reflect.Type // current param
   401  	)
   402  	for i, arg := range args {
   403  		if i < ftype.NumIn() {
   404  			p = ftype.In(pnext)
   405  			pnext++
   406  		} else if p == nil || !ftype.IsVariadic() {
   407  			// The actual number of arguments expected by the mark varies, depending
   408  			// on whether this is a value marker or an action marker.
   409  			//
   410  			// Since this error indicates a bug, probably OK to have an imprecise
   411  			// error message here.
   412  			return nil, fmt.Errorf("too many arguments to %s", mark.note.Name)
   413  		}
   414  		elemType := p
   415  		if ftype.IsVariadic() && pnext == ftype.NumIn() {
   416  			elemType = p.Elem()
   417  		}
   418  		var v reflect.Value
   419  		if id, ok := arg.(expect.Identifier); ok && id == "_" {
   420  			v = reflect.Zero(elemType)
   421  		} else {
   422  			a, err := convert(mark, arg, elemType)
   423  			if err != nil {
   424  				return nil, err
   425  			}
   426  			v = reflect.ValueOf(a)
   427  		}
   428  		argValues = append(argValues, v)
   429  	}
   430  	// Check that we have sufficient arguments. If the function is variadic, we
   431  	// do not need arguments for the final parameter.
   432  	if pnext < ftype.NumIn()-1 || pnext == ftype.NumIn()-1 && !ftype.IsVariadic() {
   433  		// Same comment as above: OK to be vague here.
   434  		return nil, fmt.Errorf("not enough arguments to %s", mark.note.Name)
   435  	}
   436  	return argValues, nil
   437  }
   438  
   439  // is reports whether arg is a T.
   440  func is[T any](arg any) bool {
   441  	_, ok := arg.(T)
   442  	return ok
   443  }
   444  
   445  // Supported value marker functions. See [valueMarkerFunc] for more details.
   446  var valueMarkerFuncs = map[string]func(marker){
   447  	"loc":  valueMarkerFunc(locMarker),
   448  	"item": valueMarkerFunc(completionItemMarker),
   449  }
   450  
   451  // Supported action marker functions. See [actionMarkerFunc] for more details.
   452  var actionMarkerFuncs = map[string]func(marker){
   453  	"acceptcompletion": actionMarkerFunc(acceptCompletionMarker),
   454  	"codeaction":       actionMarkerFunc(codeActionMarker),
   455  	"codeactionedit":   actionMarkerFunc(codeActionEditMarker),
   456  	"codeactionerr":    actionMarkerFunc(codeActionErrMarker),
   457  	"codelenses":       actionMarkerFunc(codeLensesMarker),
   458  	"complete":         actionMarkerFunc(completeMarker),
   459  	"def":              actionMarkerFunc(defMarker),
   460  	"diag":             actionMarkerFunc(diagMarker),
   461  	"documentlink":     actionMarkerFunc(documentLinkMarker),
   462  	"foldingrange":     actionMarkerFunc(foldingRangeMarker),
   463  	"format":           actionMarkerFunc(formatMarker),
   464  	"highlight":        actionMarkerFunc(highlightMarker),
   465  	"hover":            actionMarkerFunc(hoverMarker),
   466  	"hovererr":         actionMarkerFunc(hoverErrMarker),
   467  	"implementation":   actionMarkerFunc(implementationMarker),
   468  	"incomingcalls":    actionMarkerFunc(incomingCallsMarker),
   469  	"inlayhints":       actionMarkerFunc(inlayhintsMarker),
   470  	"outgoingcalls":    actionMarkerFunc(outgoingCallsMarker),
   471  	"preparerename":    actionMarkerFunc(prepareRenameMarker),
   472  	"rank":             actionMarkerFunc(rankMarker),
   473  	"rankl":            actionMarkerFunc(ranklMarker),
   474  	"refs":             actionMarkerFunc(refsMarker),
   475  	"rename":           actionMarkerFunc(renameMarker),
   476  	"renameerr":        actionMarkerFunc(renameErrMarker),
   477  	"selectionrange":   actionMarkerFunc(selectionRangeMarker),
   478  	"signature":        actionMarkerFunc(signatureMarker),
   479  	"snippet":          actionMarkerFunc(snippetMarker),
   480  	"suggestedfix":     actionMarkerFunc(suggestedfixMarker),
   481  	"suggestedfixerr":  actionMarkerFunc(suggestedfixErrMarker),
   482  	"symbol":           actionMarkerFunc(symbolMarker),
   483  	"token":            actionMarkerFunc(tokenMarker),
   484  	"typedef":          actionMarkerFunc(typedefMarker),
   485  	"workspacesymbol":  actionMarkerFunc(workspaceSymbolMarker),
   486  }
   487  
   488  // markerTest holds all the test data extracted from a test txtar archive.
   489  //
   490  // See the documentation for RunMarkerTests for more information on the archive
   491  // format.
   492  type markerTest struct {
   493  	name         string                        // relative path to the txtar file in the testdata dir
   494  	fset         *token.FileSet                // fileset used for parsing notes
   495  	content      []byte                        // raw test content
   496  	archive      *txtar.Archive                // original test archive
   497  	settings     map[string]any                // gopls settings
   498  	capabilities []byte                        // content of capabilities.json file
   499  	env          map[string]string             // editor environment
   500  	proxyFiles   map[string][]byte             // proxy content
   501  	files        map[string][]byte             // data files from the archive (excluding special files)
   502  	notes        []*expect.Note                // extracted notes from data files
   503  	golden       map[expect.Identifier]*Golden // extracted golden content, by identifier name
   504  
   505  	skipReason string   // the skip reason extracted from the "skip" archive file
   506  	flags      []string // flags extracted from the special "flags" archive file.
   507  
   508  	// Parsed flags values.
   509  	minGoVersion     string
   510  	maxGoVersion     string
   511  	cgo              bool
   512  	writeGoSum       []string // comma separated dirs to write go sum for
   513  	skipGOOS         []string // comma separated GOOS values to skip
   514  	ignoreExtraDiags bool
   515  	filterBuiltins   bool
   516  	filterKeywords   bool
   517  }
   518  
   519  // flagSet returns the flagset used for parsing the special "flags" file in the
   520  // test archive.
   521  func (t *markerTest) flagSet() *flag.FlagSet {
   522  	flags := flag.NewFlagSet(t.name, flag.ContinueOnError)
   523  	flags.StringVar(&t.minGoVersion, "min_go", "", "if set, the minimum go1.X version required for this test")
   524  	flags.StringVar(&t.maxGoVersion, "max_go", "", "if set, the maximum go1.X version required for this test")
   525  	flags.BoolVar(&t.cgo, "cgo", false, "if set, requires cgo (both the cgo tool and CGO_ENABLED=1)")
   526  	flags.Var((*stringListValue)(&t.writeGoSum), "write_sumfile", "if set, write the sumfile for these directories")
   527  	flags.Var((*stringListValue)(&t.skipGOOS), "skip_goos", "if set, skip this test on these GOOS values")
   528  	flags.BoolVar(&t.ignoreExtraDiags, "ignore_extra_diags", false, "if set, suppress errors for unmatched diagnostics")
   529  	flags.BoolVar(&t.filterBuiltins, "filter_builtins", true, "if set, filter builtins from completion results")
   530  	flags.BoolVar(&t.filterKeywords, "filter_keywords", true, "if set, filter keywords from completion results")
   531  	return flags
   532  }
   533  
   534  // stringListValue implements flag.Value.
   535  type stringListValue []string
   536  
   537  func (l *stringListValue) Set(s string) error {
   538  	if s != "" {
   539  		for _, d := range strings.Split(s, ",") {
   540  			*l = append(*l, strings.TrimSpace(d))
   541  		}
   542  	}
   543  	return nil
   544  }
   545  
   546  func (l stringListValue) String() string {
   547  	return strings.Join([]string(l), ",")
   548  }
   549  
   550  func (t *markerTest) getGolden(id expect.Identifier) *Golden {
   551  	golden, ok := t.golden[id]
   552  	// If there was no golden content for this identifier, we must create one
   553  	// to handle the case where -update is set: we need a place to store
   554  	// the updated content.
   555  	if !ok {
   556  		golden = &Golden{id: id}
   557  
   558  		// TODO(adonovan): the separation of markerTest (the
   559  		// static aspects) from markerTestRun (the dynamic
   560  		// ones) is evidently bogus because here we modify
   561  		// markerTest during execution. Let's merge the two.
   562  		t.golden[id] = golden
   563  	}
   564  	return golden
   565  }
   566  
   567  // Golden holds extracted golden content for a single @<name> prefix.
   568  //
   569  // When -update is set, golden captures the updated golden contents for later
   570  // writing.
   571  type Golden struct {
   572  	id      expect.Identifier
   573  	data    map[string][]byte // key "" => @id itself
   574  	updated map[string][]byte
   575  }
   576  
   577  // Get returns golden content for the given name, which corresponds to the
   578  // relative path following the golden prefix @<name>/. For example, to access
   579  // the content of @foo/path/to/result.json from the Golden associated with
   580  // @foo, name should be "path/to/result.json".
   581  //
   582  // If -update is set, the given update function will be called to get the
   583  // updated golden content that should be written back to testdata.
   584  //
   585  // Marker functions must use this method instead of accessing data entries
   586  // directly otherwise the -update operation will delete those entries.
   587  //
   588  // TODO(rfindley): rethink the logic here. We may want to separate Get and Set,
   589  // and not delete golden content that isn't set.
   590  func (g *Golden) Get(t testing.TB, name string, updated []byte) ([]byte, bool) {
   591  	if existing, ok := g.updated[name]; ok {
   592  		// Multiple tests may reference the same golden data, but if they do they
   593  		// must agree about its expected content.
   594  		if diff := compare.NamedText("existing", "updated", string(existing), string(updated)); diff != "" {
   595  			t.Errorf("conflicting updates for golden data %s/%s:\n%s", g.id, name, diff)
   596  		}
   597  	}
   598  	if g.updated == nil {
   599  		g.updated = make(map[string][]byte)
   600  	}
   601  	g.updated[name] = updated
   602  	if *update {
   603  		return updated, true
   604  	}
   605  
   606  	res, ok := g.data[name]
   607  	return res, ok
   608  }
   609  
   610  // loadMarkerTests walks the given dir looking for .txt files, which it
   611  // interprets as a txtar archive.
   612  //
   613  // See the documentation for RunMarkerTests for more details on the test data
   614  // archive.
   615  func loadMarkerTests(dir string) ([]*markerTest, error) {
   616  	var tests []*markerTest
   617  	err := filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error {
   618  		if strings.HasSuffix(path, ".txt") {
   619  			content, err := os.ReadFile(path)
   620  			if err != nil {
   621  				return err
   622  			}
   623  
   624  			name := strings.TrimPrefix(path, dir+string(filepath.Separator))
   625  			test, err := loadMarkerTest(name, content)
   626  			if err != nil {
   627  				return fmt.Errorf("%s: %v", path, err)
   628  			}
   629  			tests = append(tests, test)
   630  		}
   631  		return err
   632  	})
   633  	return tests, err
   634  }
   635  
   636  func loadMarkerTest(name string, content []byte) (*markerTest, error) {
   637  	archive := txtar.Parse(content)
   638  	if len(archive.Files) == 0 {
   639  		return nil, fmt.Errorf("txtar file has no '-- filename --' sections")
   640  	}
   641  	if bytes.Contains(archive.Comment, []byte("\n-- ")) {
   642  		// This check is conservative, but the comment is only a comment.
   643  		return nil, fmt.Errorf("ill-formed '-- filename --' header in comment")
   644  	}
   645  	test := &markerTest{
   646  		name:    name,
   647  		fset:    token.NewFileSet(),
   648  		content: content,
   649  		archive: archive,
   650  		files:   make(map[string][]byte),
   651  		golden:  make(map[expect.Identifier]*Golden),
   652  	}
   653  	for _, file := range archive.Files {
   654  		switch {
   655  		case file.Name == "skip":
   656  			reason := strings.ReplaceAll(string(file.Data), "\n", " ")
   657  			reason = strings.TrimSpace(reason)
   658  			test.skipReason = reason
   659  
   660  		case file.Name == "flags":
   661  			test.flags = strings.Fields(string(file.Data))
   662  
   663  		case file.Name == "settings.json":
   664  			if err := json.Unmarshal(file.Data, &test.settings); err != nil {
   665  				return nil, err
   666  			}
   667  
   668  		case file.Name == "capabilities.json":
   669  			test.capabilities = file.Data // lazily unmarshalled by the editor
   670  
   671  		case file.Name == "env":
   672  			test.env = make(map[string]string)
   673  			fields := strings.Fields(string(file.Data))
   674  			for _, field := range fields {
   675  				key, value, ok := strings.Cut(field, "=")
   676  				if !ok {
   677  					return nil, fmt.Errorf("env vars must be formatted as var=value, got %q", field)
   678  				}
   679  				test.env[key] = value
   680  			}
   681  
   682  		case strings.HasPrefix(file.Name, "@"): // golden content
   683  			idstring, name, _ := strings.Cut(file.Name[len("@"):], "/")
   684  			id := expect.Identifier(idstring)
   685  			// Note that a file.Name of just "@id" gives (id, name) = ("id", "").
   686  			if _, ok := test.golden[id]; !ok {
   687  				test.golden[id] = &Golden{
   688  					id:   id,
   689  					data: make(map[string][]byte),
   690  				}
   691  			}
   692  			test.golden[id].data[name] = file.Data
   693  
   694  		case strings.HasPrefix(file.Name, "proxy/"):
   695  			name := file.Name[len("proxy/"):]
   696  			if test.proxyFiles == nil {
   697  				test.proxyFiles = make(map[string][]byte)
   698  			}
   699  			test.proxyFiles[name] = file.Data
   700  
   701  		default: // ordinary file content
   702  			notes, err := expect.Parse(test.fset, file.Name, file.Data)
   703  			if err != nil {
   704  				return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err)
   705  			}
   706  
   707  			// Reject common misspelling: "// @mark".
   708  			// TODO(adonovan): permit "// @" within a string. Detect multiple spaces.
   709  			if i := bytes.Index(file.Data, []byte("// @")); i >= 0 {
   710  				line := 1 + bytes.Count(file.Data[:i], []byte("\n"))
   711  				return nil, fmt.Errorf("%s:%d: unwanted space before marker (// @)", file.Name, line)
   712  			}
   713  
   714  			// The 'go list' command doesn't work correct with modules named
   715  			// testdata", so don't allow it as a module name (golang/go#65406).
   716  			// (Otherwise files within it will end up in an ad hoc
   717  			// package, "command-line-arguments/$TMPDIR/...".)
   718  			if filepath.Base(file.Name) == "go.mod" &&
   719  				bytes.Contains(file.Data, []byte("module testdata")) {
   720  				return nil, fmt.Errorf("'testdata' is not a valid module name")
   721  			}
   722  
   723  			test.notes = append(test.notes, notes...)
   724  			test.files[file.Name] = file.Data
   725  		}
   726  
   727  		// Print a warning if we see what looks like "-- filename --"
   728  		// without the second "--". It's not necessarily wrong,
   729  		// but it should almost never appear in our test inputs.
   730  		if bytes.Contains(file.Data, []byte("\n-- ")) {
   731  			log.Printf("ill-formed '-- filename --' header in %s?", file.Name)
   732  		}
   733  	}
   734  
   735  	// Parse flags after loading files, as they may have been set by the "flags"
   736  	// file.
   737  	if err := test.flagSet().Parse(test.flags); err != nil {
   738  		return nil, fmt.Errorf("parsing flags: %v", err)
   739  	}
   740  
   741  	return test, nil
   742  }
   743  
   744  // formatTest formats the test as a txtar archive.
   745  func formatTest(test *markerTest) ([]byte, error) {
   746  	arch := &txtar.Archive{
   747  		Comment: test.archive.Comment,
   748  	}
   749  
   750  	updatedGolden := make(map[string][]byte)
   751  	for id, g := range test.golden {
   752  		for name, data := range g.updated {
   753  			filename := "@" + path.Join(string(id), name) // name may be ""
   754  			updatedGolden[filename] = data
   755  		}
   756  	}
   757  
   758  	// Preserve the original ordering of archive files.
   759  	for _, file := range test.archive.Files {
   760  		switch file.Name {
   761  		// Preserve configuration files exactly as they were. They must have parsed
   762  		// if we got this far.
   763  		case "skip", "flags", "settings.json", "capabilities.json", "env":
   764  			arch.Files = append(arch.Files, file)
   765  		default:
   766  			if _, ok := test.files[file.Name]; ok { // ordinary file
   767  				arch.Files = append(arch.Files, file)
   768  			} else if strings.HasPrefix(file.Name, "proxy/") { // proxy file
   769  				arch.Files = append(arch.Files, file)
   770  			} else if data, ok := updatedGolden[file.Name]; ok { // golden file
   771  				arch.Files = append(arch.Files, txtar.File{Name: file.Name, Data: data})
   772  				delete(updatedGolden, file.Name)
   773  			}
   774  		}
   775  	}
   776  
   777  	// ...followed by any new golden files.
   778  	var newGoldenFiles []txtar.File
   779  	for filename, data := range updatedGolden {
   780  		// TODO(rfindley): it looks like this implicitly removes trailing newlines
   781  		// from golden content. Is there any way to fix that? Perhaps we should
   782  		// just make the diff tolerant of missing newlines?
   783  		newGoldenFiles = append(newGoldenFiles, txtar.File{Name: filename, Data: data})
   784  	}
   785  	// Sort new golden files lexically.
   786  	sort.Slice(newGoldenFiles, func(i, j int) bool {
   787  		return newGoldenFiles[i].Name < newGoldenFiles[j].Name
   788  	})
   789  	arch.Files = append(arch.Files, newGoldenFiles...)
   790  
   791  	return txtar.Format(arch), nil
   792  }
   793  
   794  // newEnv creates a new environment for a marker test.
   795  //
   796  // TODO(rfindley): simplify and refactor the construction of testing
   797  // environments across integration tests, marker tests, and benchmarks.
   798  func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byte, writeGoSum []string, config fake.EditorConfig) *integration.Env {
   799  	sandbox, err := fake.NewSandbox(&fake.SandboxConfig{
   800  		RootDir:    t.TempDir(),
   801  		Files:      files,
   802  		ProxyFiles: proxyFiles,
   803  	})
   804  	if err != nil {
   805  		t.Fatal(err)
   806  	}
   807  
   808  	for _, dir := range writeGoSum {
   809  		if err := sandbox.RunGoCommand(context.Background(), dir, "list", []string{"-mod=mod", "..."}, []string{"GOWORK=off"}, true); err != nil {
   810  			t.Fatal(err)
   811  		}
   812  	}
   813  
   814  	// Put a debug instance in the context to prevent logging to stderr.
   815  	// See associated TODO in runner.go: we should revisit this pattern.
   816  	ctx := context.Background()
   817  	ctx = debug.WithInstance(ctx, "off")
   818  
   819  	awaiter := integration.NewAwaiter(sandbox.Workdir)
   820  	ss := lsprpc.NewStreamServer(cache, false, hooks.Options)
   821  	server := servertest.NewPipeServer(ss, jsonrpc2.NewRawStream)
   822  	const skipApplyEdits = true // capture edits but don't apply them
   823  	editor, err := fake.NewEditor(sandbox, config).Connect(ctx, server, awaiter.Hooks(), skipApplyEdits)
   824  	if err != nil {
   825  		sandbox.Close() // ignore error
   826  		t.Fatal(err)
   827  	}
   828  	if err := awaiter.Await(ctx, integration.InitialWorkspaceLoad); err != nil {
   829  		sandbox.Close() // ignore error
   830  		t.Fatal(err)
   831  	}
   832  	return &integration.Env{
   833  		T:       t,
   834  		Ctx:     ctx,
   835  		Editor:  editor,
   836  		Sandbox: sandbox,
   837  		Awaiter: awaiter,
   838  	}
   839  }
   840  
   841  // A markerTestRun holds the state of one run of a marker test archive.
   842  type markerTestRun struct {
   843  	test     *markerTest
   844  	env      *integration.Env
   845  	settings map[string]any
   846  
   847  	// Collected information.
   848  	// Each @diag/@suggestedfix marker eliminates an entry from diags.
   849  	values map[expect.Identifier]any
   850  	diags  map[protocol.Location][]protocol.Diagnostic // diagnostics by position; location end == start
   851  
   852  	// Notes that weren't associated with a top-level marker func. They may be
   853  	// consumed by another marker (e.g. @codelenses collects @codelens markers).
   854  	// Any notes that aren't consumed are flagged as an error.
   855  	extraNotes map[protocol.DocumentURI]map[string][]*expect.Note
   856  }
   857  
   858  // sprintf returns a formatted string after applying pre-processing to
   859  // arguments of the following types:
   860  //   - token.Pos: formatted using (*markerTestRun).fmtPos
   861  //   - protocol.Location: formatted using (*markerTestRun).fmtLoc
   862  func (c *marker) sprintf(format string, args ...any) string {
   863  	if false {
   864  		_ = fmt.Sprintf(format, args...) // enable vet printf checker
   865  	}
   866  	var args2 []any
   867  	for _, arg := range args {
   868  		switch arg := arg.(type) {
   869  		case token.Pos:
   870  			args2 = append(args2, c.run.fmtPos(arg))
   871  		case protocol.Location:
   872  			args2 = append(args2, c.run.fmtLoc(arg))
   873  		default:
   874  			args2 = append(args2, arg)
   875  		}
   876  	}
   877  	return fmt.Sprintf(format, args2...)
   878  }
   879  
   880  // fmtLoc formats the given pos in the context of the test, using
   881  // archive-relative paths for files and including the line number in the full
   882  // archive file.
   883  func (run *markerTestRun) fmtPos(pos token.Pos) string {
   884  	file := run.test.fset.File(pos)
   885  	if file == nil {
   886  		run.env.T.Errorf("position %d not in test fileset", pos)
   887  		return "<invalid location>"
   888  	}
   889  	m, err := run.env.Editor.Mapper(file.Name())
   890  	if err != nil {
   891  		run.env.T.Errorf("%s", err)
   892  		return "<invalid location>"
   893  	}
   894  	loc, err := m.PosLocation(file, pos, pos)
   895  	if err != nil {
   896  		run.env.T.Errorf("Mapper(%s).PosLocation failed: %v", file.Name(), err)
   897  	}
   898  	return run.fmtLoc(loc)
   899  }
   900  
   901  // fmtLoc formats the given location in the context of the test, using
   902  // archive-relative paths for files and including the line number in the full
   903  // archive file.
   904  func (run *markerTestRun) fmtLoc(loc protocol.Location) string {
   905  	formatted := run.fmtLocDetails(loc, true)
   906  	if formatted == "" {
   907  		run.env.T.Errorf("unable to find %s in test archive", loc)
   908  		return "<invalid location>"
   909  	}
   910  	return formatted
   911  }
   912  
   913  // See fmtLoc. If includeTxtPos is not set, the position in the full archive
   914  // file is omitted.
   915  //
   916  // If the location cannot be found within the archive, fmtLocDetails returns "".
   917  func (run *markerTestRun) fmtLocDetails(loc protocol.Location, includeTxtPos bool) string {
   918  	if loc == (protocol.Location{}) {
   919  		return ""
   920  	}
   921  	lines := bytes.Count(run.test.archive.Comment, []byte("\n"))
   922  	var name string
   923  	for _, f := range run.test.archive.Files {
   924  		lines++ // -- separator --
   925  		uri := run.env.Sandbox.Workdir.URI(f.Name)
   926  		if uri == loc.URI {
   927  			name = f.Name
   928  			break
   929  		}
   930  		lines += bytes.Count(f.Data, []byte("\n"))
   931  	}
   932  	if name == "" {
   933  		return ""
   934  	}
   935  	m, err := run.env.Editor.Mapper(name)
   936  	if err != nil {
   937  		run.env.T.Errorf("internal error: %v", err)
   938  		return "<invalid location>"
   939  	}
   940  	start, end, err := m.RangeOffsets(loc.Range)
   941  	if err != nil {
   942  		run.env.T.Errorf("error formatting location %s: %v", loc, err)
   943  		return "<invalid location>"
   944  	}
   945  	var (
   946  		startLine, startCol8 = m.OffsetLineCol8(start)
   947  		endLine, endCol8     = m.OffsetLineCol8(end)
   948  	)
   949  	innerSpan := fmt.Sprintf("%d:%d", startLine, startCol8)       // relative to the embedded file
   950  	outerSpan := fmt.Sprintf("%d:%d", lines+startLine, startCol8) // relative to the archive file
   951  	if start != end {
   952  		if endLine == startLine {
   953  			innerSpan += fmt.Sprintf("-%d", endCol8)
   954  			outerSpan += fmt.Sprintf("-%d", endCol8)
   955  		} else {
   956  			innerSpan += fmt.Sprintf("-%d:%d", endLine, endCol8)
   957  			outerSpan += fmt.Sprintf("-%d:%d", lines+endLine, endCol8)
   958  		}
   959  	}
   960  
   961  	if includeTxtPos {
   962  		return fmt.Sprintf("%s:%s (%s:%s)", name, innerSpan, run.test.name, outerSpan)
   963  	} else {
   964  		return fmt.Sprintf("%s:%s", name, innerSpan)
   965  	}
   966  }
   967  
   968  // ---- converters ----
   969  
   970  // converter is the signature of argument converters.
   971  // A converter should return an error rather than calling marker.errorf().
   972  //
   973  // type converter func(marker, any) (any, error)
   974  
   975  // Types with special conversions.
   976  var (
   977  	goldenType        = reflect.TypeOf(&Golden{})
   978  	locationType      = reflect.TypeOf(protocol.Location{})
   979  	markerType        = reflect.TypeOf(marker{})
   980  	stringMatcherType = reflect.TypeOf(stringMatcher{})
   981  )
   982  
   983  func convert(mark marker, arg any, paramType reflect.Type) (any, error) {
   984  	// Handle stringMatcher and golden parameters before resolving identifiers,
   985  	// because golden content lives in a separate namespace from other
   986  	// identifiers.
   987  	switch paramType {
   988  	case stringMatcherType:
   989  		return convertStringMatcher(mark, arg)
   990  	case goldenType:
   991  		id, ok := arg.(expect.Identifier)
   992  		if !ok {
   993  			return nil, fmt.Errorf("invalid input type %T: golden key must be an identifier", arg)
   994  		}
   995  		return mark.run.test.getGolden(id), nil
   996  	}
   997  	if id, ok := arg.(expect.Identifier); ok {
   998  		if arg, ok := mark.run.values[id]; ok {
   999  			if !reflect.TypeOf(arg).AssignableTo(paramType) {
  1000  				return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType)
  1001  			}
  1002  			return arg, nil
  1003  		}
  1004  	}
  1005  	if paramType == locationType {
  1006  		return convertLocation(mark, arg)
  1007  	}
  1008  	if reflect.TypeOf(arg).AssignableTo(paramType) {
  1009  		return arg, nil // no conversion required
  1010  	}
  1011  	return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType)
  1012  }
  1013  
  1014  // convertLocation converts a string or regexp argument into the protocol
  1015  // location corresponding to the first position of the string (or first match
  1016  // of the regexp) in the line preceding the note.
  1017  func convertLocation(mark marker, arg any) (protocol.Location, error) {
  1018  	switch arg := arg.(type) {
  1019  	case string:
  1020  		startOff, preceding, m, err := linePreceding(mark.run, mark.note.Pos)
  1021  		if err != nil {
  1022  			return protocol.Location{}, err
  1023  		}
  1024  		idx := bytes.Index(preceding, []byte(arg))
  1025  		if idx < 0 {
  1026  			return protocol.Location{}, fmt.Errorf("substring %q not found in %q", arg, preceding)
  1027  		}
  1028  		off := startOff + idx
  1029  		return m.OffsetLocation(off, off+len(arg))
  1030  	case *regexp.Regexp:
  1031  		return findRegexpInLine(mark.run, mark.note.Pos, arg)
  1032  	default:
  1033  		return protocol.Location{}, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg)
  1034  	}
  1035  }
  1036  
  1037  // findRegexpInLine searches the partial line preceding pos for a match for the
  1038  // regular expression re, returning a location spanning the first match. If re
  1039  // contains exactly one subgroup, the position of this subgroup match is
  1040  // returned rather than the position of the full match.
  1041  func findRegexpInLine(run *markerTestRun, pos token.Pos, re *regexp.Regexp) (protocol.Location, error) {
  1042  	startOff, preceding, m, err := linePreceding(run, pos)
  1043  	if err != nil {
  1044  		return protocol.Location{}, err
  1045  	}
  1046  
  1047  	matches := re.FindSubmatchIndex(preceding)
  1048  	if len(matches) == 0 {
  1049  		return protocol.Location{}, fmt.Errorf("no match for regexp %q found in %q", re, string(preceding))
  1050  	}
  1051  	var start, end int
  1052  	switch len(matches) {
  1053  	case 2:
  1054  		// no subgroups: return the range of the regexp expression
  1055  		start, end = matches[0], matches[1]
  1056  	case 4:
  1057  		// one subgroup: return its range
  1058  		start, end = matches[2], matches[3]
  1059  	default:
  1060  		return protocol.Location{}, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", re, len(matches)/2-1)
  1061  	}
  1062  
  1063  	return m.OffsetLocation(start+startOff, end+startOff)
  1064  }
  1065  
  1066  func linePreceding(run *markerTestRun, pos token.Pos) (int, []byte, *protocol.Mapper, error) {
  1067  	file := run.test.fset.File(pos)
  1068  	posn := safetoken.Position(file, pos)
  1069  	lineStart := file.LineStart(posn.Line)
  1070  	startOff, endOff, err := safetoken.Offsets(file, lineStart, pos)
  1071  	if err != nil {
  1072  		return 0, nil, nil, err
  1073  	}
  1074  	m, err := run.env.Editor.Mapper(file.Name())
  1075  	if err != nil {
  1076  		return 0, nil, nil, err
  1077  	}
  1078  	return startOff, m.Content[startOff:endOff], m, nil
  1079  }
  1080  
  1081  // convertStringMatcher converts a string, regexp, or identifier
  1082  // argument into a stringMatcher. The string is a substring of the
  1083  // expected error, the regexp is a pattern than matches the expected
  1084  // error, and the identifier is a golden file containing the expected
  1085  // error.
  1086  func convertStringMatcher(mark marker, arg any) (stringMatcher, error) {
  1087  	switch arg := arg.(type) {
  1088  	case string:
  1089  		return stringMatcher{substr: arg}, nil
  1090  	case *regexp.Regexp:
  1091  		return stringMatcher{pattern: arg}, nil
  1092  	case expect.Identifier:
  1093  		golden := mark.run.test.getGolden(arg)
  1094  		return stringMatcher{golden: golden}, nil
  1095  	default:
  1096  		return stringMatcher{}, fmt.Errorf("cannot convert %T to wantError (want: string, regexp, or identifier)", arg)
  1097  	}
  1098  }
  1099  
  1100  // A stringMatcher represents an expectation of a specific string value.
  1101  //
  1102  // It may be indicated in one of three ways, in 'expect' notation:
  1103  //   - an identifier 'foo', to compare (exactly) with the contents of the golden
  1104  //     section @foo;
  1105  //   - a pattern expression re"ab.*c", to match against a regular expression;
  1106  //   - a string literal "abc", to check for a substring.
  1107  type stringMatcher struct {
  1108  	golden  *Golden
  1109  	pattern *regexp.Regexp
  1110  	substr  string
  1111  }
  1112  
  1113  func (sc stringMatcher) String() string {
  1114  	if sc.golden != nil {
  1115  		return fmt.Sprintf("content from @%s entry", sc.golden.id)
  1116  	} else if sc.pattern != nil {
  1117  		return fmt.Sprintf("content matching %#q", sc.pattern)
  1118  	} else {
  1119  		return fmt.Sprintf("content with substring %q", sc.substr)
  1120  	}
  1121  }
  1122  
  1123  // checkErr asserts that the given error matches the stringMatcher's expectations.
  1124  func (sc stringMatcher) checkErr(mark marker, err error) {
  1125  	if err == nil {
  1126  		mark.errorf("@%s succeeded unexpectedly, want %v", mark.note.Name, sc)
  1127  		return
  1128  	}
  1129  	sc.check(mark, err.Error())
  1130  }
  1131  
  1132  // check asserts that the given content matches the stringMatcher's expectations.
  1133  func (sc stringMatcher) check(mark marker, got string) {
  1134  	if sc.golden != nil {
  1135  		compareGolden(mark, []byte(got), sc.golden)
  1136  	} else if sc.pattern != nil {
  1137  		// Content must match the regular expression pattern.
  1138  		if !sc.pattern.MatchString(got) {
  1139  			mark.errorf("got %q, does not match pattern %#q", got, sc.pattern)
  1140  		}
  1141  
  1142  	} else if !strings.Contains(got, sc.substr) {
  1143  		// Content must contain the expected substring.
  1144  		mark.errorf("got %q, want substring %q", got, sc.substr)
  1145  	}
  1146  }
  1147  
  1148  // checkChangedFiles compares the files changed by an operation with their expected (golden) state.
  1149  func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) {
  1150  	// Check changed files match expectations.
  1151  	for filename, got := range changed {
  1152  		if want, ok := golden.Get(mark.T(), filename, got); !ok {
  1153  			mark.errorf("%s: unexpected change to file %s; got:\n%s",
  1154  				mark.note.Name, filename, got)
  1155  
  1156  		} else if string(got) != string(want) {
  1157  			mark.errorf("%s: wrong file content for %s: got:\n%s\nwant:\n%s\ndiff:\n%s",
  1158  				mark.note.Name, filename, got, want,
  1159  				compare.Bytes(want, got))
  1160  		}
  1161  	}
  1162  
  1163  	// Report unmet expectations.
  1164  	for filename := range golden.data {
  1165  		if _, ok := changed[filename]; !ok {
  1166  			want, _ := golden.Get(mark.T(), filename, nil)
  1167  			mark.errorf("%s: missing change to file %s; want:\n%s",
  1168  				mark.note.Name, filename, want)
  1169  		}
  1170  	}
  1171  }
  1172  
  1173  // checkDiffs computes unified diffs for each changed file, and compares with
  1174  // the diff content stored in the given golden directory.
  1175  func checkDiffs(mark marker, changed map[string][]byte, golden *Golden) {
  1176  	diffs := make(map[string]string)
  1177  	for name, after := range changed {
  1178  		before := mark.run.env.FileContent(name)
  1179  		// TODO(golang/go#64023): switch back to diff.Strings.
  1180  		// The attached issue is only one obstacle to switching.
  1181  		// Another is that different diff algorithms produce
  1182  		// different results, so if we commit diffs in test
  1183  		// expectations, then we need to either (1) state
  1184  		// which diff implementation they use and never change
  1185  		// it, or (2) don't compare diffs, but instead apply
  1186  		// the "want" diff and check that it produces the
  1187  		// "got" output. Option 2 is more robust, as it allows
  1188  		// the test expectation to use any valid diff.
  1189  		edits := myers.ComputeEdits(before, string(after))
  1190  		d, err := diff.ToUnified("before", "after", before, edits, 0)
  1191  		if err != nil {
  1192  			// Can't happen: edits are consistent.
  1193  			log.Fatalf("internal error in diff.ToUnified: %v", err)
  1194  		}
  1195  		// Trim the unified header from diffs, as it is unnecessary and repetitive.
  1196  		difflines := strings.Split(d, "\n")
  1197  		if len(difflines) >= 2 && strings.HasPrefix(difflines[1], "+++") {
  1198  			diffs[name] = strings.Join(difflines[2:], "\n")
  1199  		} else {
  1200  			diffs[name] = d
  1201  		}
  1202  	}
  1203  	// Check changed files match expectations.
  1204  	for filename, got := range diffs {
  1205  		if want, ok := golden.Get(mark.T(), filename, []byte(got)); !ok {
  1206  			mark.errorf("%s: unexpected change to file %s; got diff:\n%s",
  1207  				mark.note.Name, filename, got)
  1208  
  1209  		} else if got != string(want) {
  1210  			mark.errorf("%s: wrong diff for %s:\n\ngot:\n%s\n\nwant:\n%s\n",
  1211  				mark.note.Name, filename, got, want)
  1212  		}
  1213  	}
  1214  	// Report unmet expectations.
  1215  	for filename := range golden.data {
  1216  		if _, ok := changed[filename]; !ok {
  1217  			want, _ := golden.Get(mark.T(), filename, nil)
  1218  			mark.errorf("%s: missing change to file %s; want:\n%s",
  1219  				mark.note.Name, filename, want)
  1220  		}
  1221  	}
  1222  }
  1223  
  1224  // ---- marker functions ----
  1225  
  1226  // TODO(rfindley): consolidate documentation of these markers. They are already
  1227  // documented above, so much of the documentation here is redundant.
  1228  
  1229  // completionItem is a simplified summary of a completion item.
  1230  type completionItem struct {
  1231  	Label, Detail, Kind, Documentation string
  1232  }
  1233  
  1234  func completionItemMarker(mark marker, label string, other ...string) completionItem {
  1235  	if len(other) > 3 {
  1236  		mark.errorf("too many arguments to @item: expect at most 4")
  1237  	}
  1238  	item := completionItem{
  1239  		Label: label,
  1240  	}
  1241  	if len(other) > 0 {
  1242  		item.Detail = other[0]
  1243  	}
  1244  	if len(other) > 1 {
  1245  		item.Kind = other[1]
  1246  	}
  1247  	if len(other) > 2 {
  1248  		item.Documentation = other[2]
  1249  	}
  1250  	return item
  1251  }
  1252  
  1253  func rankMarker(mark marker, src protocol.Location, items ...completionItem) {
  1254  	list := mark.run.env.Completion(src)
  1255  	var got []string
  1256  	// Collect results that are present in items, preserving their order.
  1257  	for _, g := range list.Items {
  1258  		for _, w := range items {
  1259  			if g.Label == w.Label {
  1260  				got = append(got, g.Label)
  1261  				break
  1262  			}
  1263  		}
  1264  	}
  1265  	var want []string
  1266  	for _, w := range items {
  1267  		want = append(want, w.Label)
  1268  	}
  1269  	if diff := cmp.Diff(want, got); diff != "" {
  1270  		mark.errorf("completion rankings do not match (-want +got):\n%s", diff)
  1271  	}
  1272  }
  1273  
  1274  func ranklMarker(mark marker, src protocol.Location, labels ...string) {
  1275  	list := mark.run.env.Completion(src)
  1276  	var got []string
  1277  	// Collect results that are present in items, preserving their order.
  1278  	for _, g := range list.Items {
  1279  		for _, label := range labels {
  1280  			if g.Label == label {
  1281  				got = append(got, g.Label)
  1282  				break
  1283  			}
  1284  		}
  1285  	}
  1286  	if diff := cmp.Diff(labels, got); diff != "" {
  1287  		mark.errorf("completion rankings do not match (-want +got):\n%s", diff)
  1288  	}
  1289  }
  1290  
  1291  func snippetMarker(mark marker, src protocol.Location, item completionItem, want string) {
  1292  	list := mark.run.env.Completion(src)
  1293  	var (
  1294  		found bool
  1295  		got   string
  1296  		all   []string // for errors
  1297  	)
  1298  	items := filterBuiltinsAndKeywords(mark, list.Items)
  1299  	for _, i := range items {
  1300  		all = append(all, i.Label)
  1301  		if i.Label == item.Label {
  1302  			found = true
  1303  			if i.TextEdit != nil {
  1304  				got = i.TextEdit.NewText
  1305  			}
  1306  			break
  1307  		}
  1308  	}
  1309  	if !found {
  1310  		mark.errorf("no completion item found matching %s (got: %v)", item.Label, all)
  1311  		return
  1312  	}
  1313  	if got != want {
  1314  		mark.errorf("snippets do not match: got %q, want %q", got, want)
  1315  	}
  1316  }
  1317  
  1318  // completeMarker implements the @complete marker, running
  1319  // textDocument/completion at the given src location and asserting that the
  1320  // results match the expected results.
  1321  func completeMarker(mark marker, src protocol.Location, want ...completionItem) {
  1322  	list := mark.run.env.Completion(src)
  1323  	items := filterBuiltinsAndKeywords(mark, list.Items)
  1324  	var got []completionItem
  1325  	for i, item := range items {
  1326  		simplified := completionItem{
  1327  			Label:  item.Label,
  1328  			Detail: item.Detail,
  1329  			Kind:   fmt.Sprint(item.Kind),
  1330  		}
  1331  		if item.Documentation != nil {
  1332  			switch v := item.Documentation.Value.(type) {
  1333  			case string:
  1334  				simplified.Documentation = v
  1335  			case protocol.MarkupContent:
  1336  				simplified.Documentation = strings.TrimSpace(v.Value) // trim newlines
  1337  			}
  1338  		}
  1339  		// Support short-hand notation: if Detail, Kind, or Documentation are omitted from the
  1340  		// item, don't match them.
  1341  		if i < len(want) {
  1342  			if want[i].Detail == "" {
  1343  				simplified.Detail = ""
  1344  			}
  1345  			if want[i].Kind == "" {
  1346  				simplified.Kind = ""
  1347  			}
  1348  			if want[i].Documentation == "" {
  1349  				simplified.Documentation = ""
  1350  			}
  1351  		}
  1352  		got = append(got, simplified)
  1353  	}
  1354  	if len(want) == 0 {
  1355  		want = nil // got is nil if empty
  1356  	}
  1357  	if diff := cmp.Diff(want, got); diff != "" {
  1358  		mark.errorf("Completion(...) returned unexpect results (-want +got):\n%s", diff)
  1359  	}
  1360  }
  1361  
  1362  // filterBuiltinsAndKeywords filters out builtins and keywords from completion
  1363  // results.
  1364  //
  1365  // It over-approximates, and does not detect if builtins are shadowed.
  1366  func filterBuiltinsAndKeywords(mark marker, items []protocol.CompletionItem) []protocol.CompletionItem {
  1367  	keep := 0
  1368  	for _, item := range items {
  1369  		if mark.run.test.filterKeywords && item.Kind == protocol.KeywordCompletion {
  1370  			continue
  1371  		}
  1372  		if mark.run.test.filterBuiltins && types.Universe.Lookup(item.Label) != nil {
  1373  			continue
  1374  		}
  1375  		items[keep] = item
  1376  		keep++
  1377  	}
  1378  	return items[:keep]
  1379  }
  1380  
  1381  // acceptCompletionMarker implements the @acceptCompletion marker, running
  1382  // textDocument/completion at the given src location and accepting the
  1383  // candidate with the given label. The resulting source must match the provided
  1384  // golden content.
  1385  func acceptCompletionMarker(mark marker, src protocol.Location, label string, golden *Golden) {
  1386  	list := mark.run.env.Completion(src)
  1387  	var selected *protocol.CompletionItem
  1388  	for _, item := range list.Items {
  1389  		if item.Label == label {
  1390  			selected = &item
  1391  			break
  1392  		}
  1393  	}
  1394  	if selected == nil {
  1395  		mark.errorf("Completion(...) did not return an item labeled %q", label)
  1396  		return
  1397  	}
  1398  	filename := mark.path()
  1399  	mapper := mark.mapper()
  1400  	patched, _, err := protocol.ApplyEdits(mapper, append([]protocol.TextEdit{*selected.TextEdit}, selected.AdditionalTextEdits...))
  1401  
  1402  	if err != nil {
  1403  		mark.errorf("ApplyProtocolEdits failed: %v", err)
  1404  		return
  1405  	}
  1406  	changes := map[string][]byte{filename: patched}
  1407  	// Check the file state.
  1408  	checkChangedFiles(mark, changes, golden)
  1409  }
  1410  
  1411  // defMarker implements the @def marker, running textDocument/definition at
  1412  // the given src location and asserting that there is exactly one resulting
  1413  // location, matching dst.
  1414  //
  1415  // TODO(rfindley): support a variadic destination set.
  1416  func defMarker(mark marker, src, dst protocol.Location) {
  1417  	got := mark.run.env.GoToDefinition(src)
  1418  	if got != dst {
  1419  		mark.errorf("definition location does not match:\n\tgot: %s\n\twant %s",
  1420  			mark.run.fmtLoc(got), mark.run.fmtLoc(dst))
  1421  	}
  1422  }
  1423  
  1424  func typedefMarker(mark marker, src, dst protocol.Location) {
  1425  	got := mark.run.env.TypeDefinition(src)
  1426  	if got != dst {
  1427  		mark.errorf("type definition location does not match:\n\tgot: %s\n\twant %s",
  1428  			mark.run.fmtLoc(got), mark.run.fmtLoc(dst))
  1429  	}
  1430  }
  1431  
  1432  func foldingRangeMarker(mark marker, g *Golden) {
  1433  	env := mark.run.env
  1434  	ranges, err := mark.server().FoldingRange(env.Ctx, &protocol.FoldingRangeParams{
  1435  		TextDocument: mark.document(),
  1436  	})
  1437  	if err != nil {
  1438  		mark.errorf("foldingRange failed: %v", err)
  1439  		return
  1440  	}
  1441  	var edits []protocol.TextEdit
  1442  	insert := func(line, char uint32, text string) {
  1443  		pos := protocol.Position{Line: line, Character: char}
  1444  		edits = append(edits, protocol.TextEdit{
  1445  			Range: protocol.Range{
  1446  				Start: pos,
  1447  				End:   pos,
  1448  			},
  1449  			NewText: text,
  1450  		})
  1451  	}
  1452  	for i, rng := range ranges {
  1453  		insert(rng.StartLine, rng.StartCharacter, fmt.Sprintf("<%d kind=%q>", i, rng.Kind))
  1454  		insert(rng.EndLine, rng.EndCharacter, fmt.Sprintf("</%d>", i))
  1455  	}
  1456  	filename := mark.path()
  1457  	mapper, err := env.Editor.Mapper(filename)
  1458  	if err != nil {
  1459  		mark.errorf("Editor.Mapper(%s) failed: %v", filename, err)
  1460  		return
  1461  	}
  1462  	got, _, err := protocol.ApplyEdits(mapper, edits)
  1463  	if err != nil {
  1464  		mark.errorf("ApplyProtocolEdits failed: %v", err)
  1465  		return
  1466  	}
  1467  	want, _ := g.Get(mark.T(), "", got)
  1468  	if diff := compare.Bytes(want, got); diff != "" {
  1469  		mark.errorf("foldingRange mismatch:\n%s", diff)
  1470  	}
  1471  }
  1472  
  1473  // formatMarker implements the @format marker.
  1474  func formatMarker(mark marker, golden *Golden) {
  1475  	edits, err := mark.server().Formatting(mark.ctx(), &protocol.DocumentFormattingParams{
  1476  		TextDocument: mark.document(),
  1477  	})
  1478  	var got []byte
  1479  	if err != nil {
  1480  		got = []byte(err.Error() + "\n") // all golden content is newline terminated
  1481  	} else {
  1482  		env := mark.run.env
  1483  		filename := mark.path()
  1484  		mapper, err := env.Editor.Mapper(filename)
  1485  		if err != nil {
  1486  			mark.errorf("Editor.Mapper(%s) failed: %v", filename, err)
  1487  		}
  1488  
  1489  		got, _, err = protocol.ApplyEdits(mapper, edits)
  1490  		if err != nil {
  1491  			mark.errorf("ApplyProtocolEdits failed: %v", err)
  1492  			return
  1493  		}
  1494  	}
  1495  
  1496  	compareGolden(mark, got, golden)
  1497  }
  1498  
  1499  func highlightMarker(mark marker, src protocol.Location, dsts ...protocol.Location) {
  1500  	highlights := mark.run.env.DocumentHighlight(src)
  1501  	var got []protocol.Range
  1502  	for _, h := range highlights {
  1503  		got = append(got, h.Range)
  1504  	}
  1505  
  1506  	var want []protocol.Range
  1507  	for _, d := range dsts {
  1508  		want = append(want, d.Range)
  1509  	}
  1510  
  1511  	sortRanges := func(s []protocol.Range) {
  1512  		sort.Slice(s, func(i, j int) bool {
  1513  			return protocol.CompareRange(s[i], s[j]) < 0
  1514  		})
  1515  	}
  1516  
  1517  	sortRanges(got)
  1518  	sortRanges(want)
  1519  
  1520  	if diff := cmp.Diff(want, got); diff != "" {
  1521  		mark.errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", src, diff)
  1522  	}
  1523  }
  1524  
  1525  func hoverMarker(mark marker, src, dst protocol.Location, sc stringMatcher) {
  1526  	content, gotDst := mark.run.env.Hover(src)
  1527  	if gotDst != dst {
  1528  		mark.errorf("hover location does not match:\n\tgot: %s\n\twant %s)", mark.run.fmtLoc(gotDst), mark.run.fmtLoc(dst))
  1529  	}
  1530  	gotMD := ""
  1531  	if content != nil {
  1532  		gotMD = content.Value
  1533  	}
  1534  	sc.check(mark, gotMD)
  1535  }
  1536  
  1537  func hoverErrMarker(mark marker, src protocol.Location, em stringMatcher) {
  1538  	_, _, err := mark.editor().Hover(mark.ctx(), src)
  1539  	em.checkErr(mark, err)
  1540  }
  1541  
  1542  // locMarker implements the @loc marker. It is executed before other
  1543  // markers, so that locations are available.
  1544  func locMarker(mark marker, loc protocol.Location) protocol.Location { return loc }
  1545  
  1546  // diagMarker implements the @diag marker. It eliminates diagnostics from
  1547  // the observed set in mark.test.
  1548  func diagMarker(mark marker, loc protocol.Location, re *regexp.Regexp) {
  1549  	if _, ok := removeDiagnostic(mark, loc, re); !ok {
  1550  		mark.errorf("no diagnostic at %v matches %q", loc, re)
  1551  	}
  1552  }
  1553  
  1554  // removeDiagnostic looks for a diagnostic matching loc at the given position.
  1555  //
  1556  // If found, it returns (diag, true), and eliminates the matched diagnostic
  1557  // from the unmatched set.
  1558  //
  1559  // If not found, it returns (protocol.Diagnostic{}, false).
  1560  func removeDiagnostic(mark marker, loc protocol.Location, re *regexp.Regexp) (protocol.Diagnostic, bool) {
  1561  	loc.Range.End = loc.Range.Start // diagnostics ignore end position.
  1562  	diags := mark.run.diags[loc]
  1563  	for i, diag := range diags {
  1564  		if re.MatchString(diag.Message) {
  1565  			mark.run.diags[loc] = append(diags[:i], diags[i+1:]...)
  1566  			return diag, true
  1567  		}
  1568  	}
  1569  	return protocol.Diagnostic{}, false
  1570  }
  1571  
  1572  // renameMarker implements the @rename(location, new, golden) marker.
  1573  func renameMarker(mark marker, loc protocol.Location, newName string, golden *Golden) {
  1574  	changed, err := rename(mark.run.env, loc, newName)
  1575  	if err != nil {
  1576  		mark.errorf("rename failed: %v. (Use @renameerr for expected errors.)", err)
  1577  		return
  1578  	}
  1579  	checkDiffs(mark, changed, golden)
  1580  }
  1581  
  1582  // renameErrMarker implements the @renamererr(location, new, error) marker.
  1583  func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr stringMatcher) {
  1584  	_, err := rename(mark.run.env, loc, newName)
  1585  	wantErr.checkErr(mark, err)
  1586  }
  1587  
  1588  func selectionRangeMarker(mark marker, loc protocol.Location, g *Golden) {
  1589  	ranges, err := mark.server().SelectionRange(mark.ctx(), &protocol.SelectionRangeParams{
  1590  		TextDocument: mark.document(),
  1591  		Positions:    []protocol.Position{loc.Range.Start},
  1592  	})
  1593  	if err != nil {
  1594  		mark.errorf("SelectionRange failed: %v", err)
  1595  		return
  1596  	}
  1597  	var buf bytes.Buffer
  1598  	m := mark.mapper()
  1599  	for i, path := range ranges {
  1600  		fmt.Fprintf(&buf, "Ranges %d:", i)
  1601  		rng := path
  1602  		for {
  1603  			s, e, err := m.RangeOffsets(rng.Range)
  1604  			if err != nil {
  1605  				mark.errorf("RangeOffsets failed: %v", err)
  1606  				return
  1607  			}
  1608  
  1609  			var snippet string
  1610  			if e-s < 30 {
  1611  				snippet = string(m.Content[s:e])
  1612  			} else {
  1613  				snippet = string(m.Content[s:s+15]) + "..." + string(m.Content[e-15:e])
  1614  			}
  1615  
  1616  			fmt.Fprintf(&buf, "\n\t%v %q", rng.Range, strings.ReplaceAll(snippet, "\n", "\\n"))
  1617  
  1618  			if rng.Parent == nil {
  1619  				break
  1620  			}
  1621  			rng = *rng.Parent
  1622  		}
  1623  		buf.WriteRune('\n')
  1624  	}
  1625  	compareGolden(mark, buf.Bytes(), g)
  1626  }
  1627  
  1628  func tokenMarker(mark marker, loc protocol.Location, tokenType, mod string) {
  1629  	tokens := mark.run.env.SemanticTokensRange(loc)
  1630  	if len(tokens) != 1 {
  1631  		mark.errorf("got %d tokens, want 1", len(tokens))
  1632  		return
  1633  	}
  1634  	tok := tokens[0]
  1635  	if tok.TokenType != tokenType {
  1636  		mark.errorf("token type = %q, want %q", tok.TokenType, tokenType)
  1637  	}
  1638  	if tok.Mod != mod {
  1639  		mark.errorf("token mod = %q, want %q", tok.Mod, mod)
  1640  	}
  1641  }
  1642  
  1643  func signatureMarker(mark marker, src protocol.Location, label string, active int64) {
  1644  	got := mark.run.env.SignatureHelp(src)
  1645  	if label == "" {
  1646  		// A null result is expected.
  1647  		// (There's no point having a @signatureerr marker
  1648  		// because the server handler suppresses all errors.)
  1649  		if got != nil && len(got.Signatures) > 0 {
  1650  			mark.errorf("signatureHelp = %v, want 0 signatures", got)
  1651  		}
  1652  		return
  1653  	}
  1654  	if got == nil || len(got.Signatures) != 1 {
  1655  		mark.errorf("signatureHelp = %v, want exactly 1 signature", got)
  1656  		return
  1657  	}
  1658  	if got := got.Signatures[0].Label; got != label {
  1659  		mark.errorf("signatureHelp: got label %q, want %q", got, label)
  1660  	}
  1661  	if got := int64(got.ActiveParameter); got != active {
  1662  		mark.errorf("signatureHelp: got active parameter %d, want %d", got, active)
  1663  	}
  1664  }
  1665  
  1666  // rename returns the new contents of the files that would be modified
  1667  // by renaming the identifier at loc to newName.
  1668  func rename(env *integration.Env, loc protocol.Location, newName string) (map[string][]byte, error) {
  1669  	// We call Server.Rename directly, instead of
  1670  	//   env.Editor.Rename(env.Ctx, loc, newName)
  1671  	// to isolate Rename from PrepareRename, and because we don't
  1672  	// want to modify the file system in a scenario with multiple
  1673  	// @rename markers.
  1674  
  1675  	editMap, err := env.Editor.Server.Rename(env.Ctx, &protocol.RenameParams{
  1676  		TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
  1677  		Position:     loc.Range.Start,
  1678  		NewName:      newName,
  1679  	})
  1680  	if err != nil {
  1681  		return nil, err
  1682  	}
  1683  
  1684  	fileChanges := make(map[string][]byte)
  1685  	if err := applyDocumentChanges(env, editMap.DocumentChanges, fileChanges); err != nil {
  1686  		return nil, fmt.Errorf("applying document changes: %v", err)
  1687  	}
  1688  	return fileChanges, nil
  1689  }
  1690  
  1691  // applyDocumentChanges applies the given document changes to the editor buffer
  1692  // content, recording the resulting contents in the fileChanges map. It is an
  1693  // error for a change to an edit a file that is already present in the
  1694  // fileChanges map.
  1695  func applyDocumentChanges(env *integration.Env, changes []protocol.DocumentChanges, fileChanges map[string][]byte) error {
  1696  	getMapper := func(path string) (*protocol.Mapper, error) {
  1697  		if _, ok := fileChanges[path]; ok {
  1698  			return nil, fmt.Errorf("internal error: %s is already edited", path)
  1699  		}
  1700  		return env.Editor.Mapper(path)
  1701  	}
  1702  
  1703  	for _, change := range changes {
  1704  		if change.RenameFile != nil {
  1705  			// rename
  1706  			oldFile := env.Sandbox.Workdir.URIToPath(change.RenameFile.OldURI)
  1707  			mapper, err := getMapper(oldFile)
  1708  			if err != nil {
  1709  				return err
  1710  			}
  1711  			newFile := env.Sandbox.Workdir.URIToPath(change.RenameFile.NewURI)
  1712  			fileChanges[newFile] = mapper.Content
  1713  		} else {
  1714  			// edit
  1715  			filename := env.Sandbox.Workdir.URIToPath(change.TextDocumentEdit.TextDocument.URI)
  1716  			mapper, err := getMapper(filename)
  1717  			if err != nil {
  1718  				return err
  1719  			}
  1720  			patched, _, err := protocol.ApplyEdits(mapper, protocol.AsTextEdits(change.TextDocumentEdit.Edits))
  1721  			if err != nil {
  1722  				return err
  1723  			}
  1724  			fileChanges[filename] = patched
  1725  		}
  1726  	}
  1727  
  1728  	return nil
  1729  }
  1730  
  1731  func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden, titles ...string) {
  1732  	// Request the range from start.Start to end.End.
  1733  	loc := start
  1734  	loc.Range.End = end.Range.End
  1735  
  1736  	// Apply the fix it suggests.
  1737  	changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles)
  1738  	if err != nil {
  1739  		mark.errorf("codeAction failed: %v", err)
  1740  		return
  1741  	}
  1742  
  1743  	// Check the file state.
  1744  	checkChangedFiles(mark, changed, g)
  1745  }
  1746  
  1747  func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden, titles ...string) {
  1748  	changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles)
  1749  	if err != nil {
  1750  		mark.errorf("codeAction failed: %v", err)
  1751  		return
  1752  	}
  1753  
  1754  	checkDiffs(mark, changed, g)
  1755  }
  1756  
  1757  func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr stringMatcher) {
  1758  	loc := start
  1759  	loc.Range.End = end.Range.End
  1760  	_, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, nil)
  1761  	wantErr.checkErr(mark, err)
  1762  }
  1763  
  1764  // codeLensesMarker runs the @codelenses() marker, collecting @codelens marks
  1765  // in the current file and comparing with the result of the
  1766  // textDocument/codeLens RPC.
  1767  func codeLensesMarker(mark marker) {
  1768  	type codeLens struct {
  1769  		Range protocol.Range
  1770  		Title string
  1771  	}
  1772  
  1773  	lenses := mark.run.env.CodeLens(mark.path())
  1774  	var got []codeLens
  1775  	for _, lens := range lenses {
  1776  		title := ""
  1777  		if lens.Command != nil {
  1778  			title = lens.Command.Title
  1779  		}
  1780  		got = append(got, codeLens{lens.Range, title})
  1781  	}
  1782  
  1783  	var want []codeLens
  1784  	mark.consumeExtraNotes("codelens", actionMarkerFunc(func(_ marker, loc protocol.Location, title string) {
  1785  		want = append(want, codeLens{loc.Range, title})
  1786  	}))
  1787  
  1788  	for _, s := range [][]codeLens{got, want} {
  1789  		sort.Slice(s, func(i, j int) bool {
  1790  			li, lj := s[i], s[j]
  1791  			if c := protocol.CompareRange(li.Range, lj.Range); c != 0 {
  1792  				return c < 0
  1793  			}
  1794  			return li.Title < lj.Title
  1795  		})
  1796  	}
  1797  
  1798  	if diff := cmp.Diff(want, got); diff != "" {
  1799  		mark.errorf("codelenses: unexpected diff (-want +got):\n%s", diff)
  1800  	}
  1801  }
  1802  
  1803  func documentLinkMarker(mark marker, g *Golden) {
  1804  	var b bytes.Buffer
  1805  	links := mark.run.env.DocumentLink(mark.path())
  1806  	for _, l := range links {
  1807  		if l.Target == nil {
  1808  			mark.errorf("%s: nil link target", l.Range)
  1809  			continue
  1810  		}
  1811  		loc := protocol.Location{URI: mark.uri(), Range: l.Range}
  1812  		fmt.Fprintln(&b, mark.run.fmtLocDetails(loc, false), *l.Target)
  1813  	}
  1814  
  1815  	compareGolden(mark, b.Bytes(), g)
  1816  }
  1817  
  1818  // consumeExtraNotes runs the provided func for each extra note with the given
  1819  // name, and deletes all matching notes.
  1820  func (mark marker) consumeExtraNotes(name string, f func(marker)) {
  1821  	uri := mark.uri()
  1822  	notes := mark.run.extraNotes[uri][name]
  1823  	delete(mark.run.extraNotes[uri], name)
  1824  
  1825  	for _, note := range notes {
  1826  		f(marker{run: mark.run, note: note})
  1827  	}
  1828  }
  1829  
  1830  // suggestedfixMarker implements the @suggestedfix(location, regexp,
  1831  // kind, golden) marker. It acts like @diag(location, regexp), to set
  1832  // the expectation of a diagnostic, but then it applies the first code
  1833  // action of the specified kind suggested by the matched diagnostic.
  1834  func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) {
  1835  	loc.Range.End = loc.Range.Start // diagnostics ignore end position.
  1836  	// Find and remove the matching diagnostic.
  1837  	diag, ok := removeDiagnostic(mark, loc, re)
  1838  	if !ok {
  1839  		mark.errorf("no diagnostic at %v matches %q", loc, re)
  1840  		return
  1841  	}
  1842  
  1843  	// Apply the fix it suggests.
  1844  	changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil)
  1845  	if err != nil {
  1846  		mark.errorf("suggestedfix failed: %v. (Use @suggestedfixerr for expected errors.)", err)
  1847  		return
  1848  	}
  1849  
  1850  	// Check the file state.
  1851  	checkDiffs(mark, changed, golden)
  1852  }
  1853  
  1854  func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp, wantErr stringMatcher) {
  1855  	loc.Range.End = loc.Range.Start // diagnostics ignore end position.
  1856  	// Find and remove the matching diagnostic.
  1857  	diag, ok := removeDiagnostic(mark, loc, re)
  1858  	if !ok {
  1859  		mark.errorf("no diagnostic at %v matches %q", loc, re)
  1860  		return
  1861  	}
  1862  
  1863  	// Apply the fix it suggests.
  1864  	_, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil)
  1865  	wantErr.checkErr(mark, err)
  1866  }
  1867  
  1868  // codeAction executes a textDocument/codeAction request for the specified
  1869  // location and kind. If diag is non-nil, it is used as the code action
  1870  // context.
  1871  //
  1872  // The resulting map contains resulting file contents after the code action is
  1873  // applied. Currently, this function does not support code actions that return
  1874  // edits directly; it only supports code action commands.
  1875  func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) (map[string][]byte, error) {
  1876  	changes, err := codeActionChanges(env, uri, rng, actionKind, diag, titles)
  1877  	if err != nil {
  1878  		return nil, err
  1879  	}
  1880  	fileChanges := make(map[string][]byte)
  1881  	if err := applyDocumentChanges(env, changes, fileChanges); err != nil {
  1882  		return nil, fmt.Errorf("applying document changes: %v", err)
  1883  	}
  1884  	return fileChanges, nil
  1885  }
  1886  
  1887  // codeActionChanges executes a textDocument/codeAction request for the
  1888  // specified location and kind, and captures the resulting document changes.
  1889  // If diag is non-nil, it is used as the code action context.
  1890  // If titles is non-empty, the code action title must be present among the provided titles.
  1891  func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) ([]protocol.DocumentChanges, error) {
  1892  	// Request all code actions that apply to the diagnostic.
  1893  	// (The protocol supports filtering using Context.Only={actionKind}
  1894  	// but we can give a better error if we don't filter.)
  1895  	params := &protocol.CodeActionParams{
  1896  		TextDocument: protocol.TextDocumentIdentifier{URI: uri},
  1897  		Range:        rng,
  1898  		Context: protocol.CodeActionContext{
  1899  			Only: nil, // => all kinds
  1900  		},
  1901  	}
  1902  	if diag != nil {
  1903  		params.Context.Diagnostics = []protocol.Diagnostic{*diag}
  1904  	}
  1905  
  1906  	actions, err := env.Editor.Server.CodeAction(env.Ctx, params)
  1907  	if err != nil {
  1908  		return nil, err
  1909  	}
  1910  
  1911  	// Find the sole candidates CodeAction of the specified kind (e.g. refactor.rewrite).
  1912  	var candidates []protocol.CodeAction
  1913  	for _, act := range actions {
  1914  		if act.Kind == protocol.CodeActionKind(actionKind) {
  1915  			if len(titles) > 0 {
  1916  				for _, f := range titles {
  1917  					if act.Title == f {
  1918  						candidates = append(candidates, act)
  1919  						break
  1920  					}
  1921  				}
  1922  			} else {
  1923  				candidates = append(candidates, act)
  1924  			}
  1925  		}
  1926  	}
  1927  	if len(candidates) != 1 {
  1928  		for _, act := range actions {
  1929  			env.T.Logf("found CodeAction Kind=%s Title=%q", act.Kind, act.Title)
  1930  		}
  1931  		return nil, fmt.Errorf("found %d CodeActions of kind %s matching filters %v for this diagnostic, want 1", len(candidates), actionKind, titles)
  1932  	}
  1933  	action := candidates[0]
  1934  
  1935  	// Apply the codeAction.
  1936  	//
  1937  	// Spec:
  1938  	//  "If a code action provides an edit and a command, first the edit is
  1939  	//  executed and then the command."
  1940  	// An action may specify an edit and/or a command, to be
  1941  	// applied in that order. But since applyDocumentChanges(env,
  1942  	// action.Edit.DocumentChanges) doesn't compose, for now we
  1943  	// assert that actions return one or the other.
  1944  
  1945  	// Resolve code action edits first if the client has resolve support
  1946  	// and the code action has no edits.
  1947  	if action.Edit == nil {
  1948  		editSupport, err := env.Editor.EditResolveSupport()
  1949  		if err != nil {
  1950  			return nil, err
  1951  		}
  1952  		if editSupport {
  1953  			resolved, err := env.Editor.Server.ResolveCodeAction(env.Ctx, &action)
  1954  			if err != nil {
  1955  				return nil, err
  1956  			}
  1957  			action.Edit = resolved.Edit
  1958  		}
  1959  	}
  1960  
  1961  	if action.Edit != nil {
  1962  		if action.Edit.Changes != nil {
  1963  			env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title)
  1964  		}
  1965  		if action.Edit.DocumentChanges != nil {
  1966  			if action.Command != nil {
  1967  				env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Command", action.Kind, action.Title)
  1968  			}
  1969  			return action.Edit.DocumentChanges, nil
  1970  		}
  1971  	}
  1972  
  1973  	if action.Command != nil {
  1974  		// This is a typical CodeAction command:
  1975  		//
  1976  		//   Title:     "Implement error"
  1977  		//   Command:   gopls.apply_fix
  1978  		//   Arguments: [{"Fix":"stub_methods","URI":".../a.go","Range":...}}]
  1979  		//
  1980  		// The client makes an ExecuteCommand RPC to the server,
  1981  		// which dispatches it to the ApplyFix handler.
  1982  		// ApplyFix dispatches to the "stub_methods" suggestedfix hook (the meat).
  1983  		// The server then makes an ApplyEdit RPC to the client,
  1984  		// whose Awaiter hook gathers the edits instead of applying them.
  1985  
  1986  		_ = env.Awaiter.TakeDocumentChanges() // reset (assuming Env is confined to this thread)
  1987  
  1988  		if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{
  1989  			Command:   action.Command.Command,
  1990  			Arguments: action.Command.Arguments,
  1991  		}); err != nil {
  1992  			return nil, err
  1993  		}
  1994  		return env.Awaiter.TakeDocumentChanges(), nil
  1995  	}
  1996  
  1997  	return nil, nil
  1998  }
  1999  
  2000  // refsMarker implements the @refs marker.
  2001  func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) {
  2002  	refs := func(includeDeclaration bool, want []protocol.Location) error {
  2003  		got, err := mark.server().References(mark.ctx(), &protocol.ReferenceParams{
  2004  			TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src),
  2005  			Context: protocol.ReferenceContext{
  2006  				IncludeDeclaration: includeDeclaration,
  2007  			},
  2008  		})
  2009  		if err != nil {
  2010  			return err
  2011  		}
  2012  
  2013  		return compareLocations(mark, got, want)
  2014  	}
  2015  
  2016  	for _, includeDeclaration := range []bool{false, true} {
  2017  		// Ignore first 'want' location if we didn't request the declaration.
  2018  		// TODO(adonovan): don't assume a single declaration:
  2019  		// there may be >1 if corresponding methods are considered.
  2020  		want := want
  2021  		if !includeDeclaration && len(want) > 0 {
  2022  			want = want[1:]
  2023  		}
  2024  		if err := refs(includeDeclaration, want); err != nil {
  2025  			mark.errorf("refs(includeDeclaration=%t) failed: %v",
  2026  				includeDeclaration, err)
  2027  		}
  2028  	}
  2029  }
  2030  
  2031  // implementationMarker implements the @implementation marker.
  2032  func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) {
  2033  	got, err := mark.server().Implementation(mark.ctx(), &protocol.ImplementationParams{
  2034  		TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src),
  2035  	})
  2036  	if err != nil {
  2037  		mark.errorf("implementation at %s failed: %v", src, err)
  2038  		return
  2039  	}
  2040  	if err := compareLocations(mark, got, want); err != nil {
  2041  		mark.errorf("implementation: %v", err)
  2042  	}
  2043  }
  2044  
  2045  func itemLocation(item protocol.CallHierarchyItem) protocol.Location {
  2046  	return protocol.Location{
  2047  		URI:   item.URI,
  2048  		Range: item.Range,
  2049  	}
  2050  }
  2051  
  2052  func incomingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) {
  2053  	getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) {
  2054  		calls, err := mark.server().IncomingCalls(mark.ctx(), &protocol.CallHierarchyIncomingCallsParams{Item: item})
  2055  		if err != nil {
  2056  			return nil, err
  2057  		}
  2058  		var locs []protocol.Location
  2059  		for _, call := range calls {
  2060  			locs = append(locs, itemLocation(call.From))
  2061  		}
  2062  		return locs, nil
  2063  	}
  2064  	callHierarchy(mark, src, getCalls, want)
  2065  }
  2066  
  2067  func outgoingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) {
  2068  	getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) {
  2069  		calls, err := mark.server().OutgoingCalls(mark.ctx(), &protocol.CallHierarchyOutgoingCallsParams{Item: item})
  2070  		if err != nil {
  2071  			return nil, err
  2072  		}
  2073  		var locs []protocol.Location
  2074  		for _, call := range calls {
  2075  			locs = append(locs, itemLocation(call.To))
  2076  		}
  2077  		return locs, nil
  2078  	}
  2079  	callHierarchy(mark, src, getCalls, want)
  2080  }
  2081  
  2082  type callHierarchyFunc = func(protocol.CallHierarchyItem) ([]protocol.Location, error)
  2083  
  2084  func callHierarchy(mark marker, src protocol.Location, getCalls callHierarchyFunc, want []protocol.Location) {
  2085  	items, err := mark.server().PrepareCallHierarchy(mark.ctx(), &protocol.CallHierarchyPrepareParams{
  2086  		TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src),
  2087  	})
  2088  	if err != nil {
  2089  		mark.errorf("PrepareCallHierarchy failed: %v", err)
  2090  		return
  2091  	}
  2092  	if nitems := len(items); nitems != 1 {
  2093  		mark.errorf("PrepareCallHierarchy returned %d items, want exactly 1", nitems)
  2094  		return
  2095  	}
  2096  	if loc := itemLocation(items[0]); loc != src {
  2097  		mark.errorf("PrepareCallHierarchy found call %v, want %v", loc, src)
  2098  		return
  2099  	}
  2100  	calls, err := getCalls(items[0])
  2101  	if err != nil {
  2102  		mark.errorf("call hierarchy failed: %v", err)
  2103  		return
  2104  	}
  2105  	if calls == nil {
  2106  		calls = []protocol.Location{}
  2107  	}
  2108  	// TODO(rfindley): why aren't call hierarchy results stable?
  2109  	sortLocs := func(locs []protocol.Location) {
  2110  		sort.Slice(locs, func(i, j int) bool {
  2111  			return protocol.CompareLocation(locs[i], locs[j]) < 0
  2112  		})
  2113  	}
  2114  	sortLocs(want)
  2115  	sortLocs(calls)
  2116  	if d := cmp.Diff(want, calls); d != "" {
  2117  		mark.errorf("call hierarchy: unexpected results (-want +got):\n%s", d)
  2118  	}
  2119  }
  2120  
  2121  func inlayhintsMarker(mark marker, g *Golden) {
  2122  	hints := mark.run.env.InlayHints(mark.path())
  2123  
  2124  	// Map inlay hints to text edits.
  2125  	edits := make([]protocol.TextEdit, len(hints))
  2126  	for i, hint := range hints {
  2127  		var paddingLeft, paddingRight string
  2128  		if hint.PaddingLeft {
  2129  			paddingLeft = " "
  2130  		}
  2131  		if hint.PaddingRight {
  2132  			paddingRight = " "
  2133  		}
  2134  		edits[i] = protocol.TextEdit{
  2135  			Range:   protocol.Range{Start: hint.Position, End: hint.Position},
  2136  			NewText: fmt.Sprintf("<%s%s%s>", paddingLeft, hint.Label[0].Value, paddingRight),
  2137  		}
  2138  	}
  2139  
  2140  	m := mark.mapper()
  2141  	got, _, err := protocol.ApplyEdits(m, edits)
  2142  	if err != nil {
  2143  		mark.errorf("ApplyProtocolEdits: %v", err)
  2144  		return
  2145  	}
  2146  
  2147  	compareGolden(mark, got, g)
  2148  }
  2149  
  2150  func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder string) {
  2151  	params := &protocol.PrepareRenameParams{
  2152  		TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src),
  2153  	}
  2154  	got, err := mark.server().PrepareRename(mark.ctx(), params)
  2155  	if err != nil {
  2156  		mark.T().Fatal(err)
  2157  	}
  2158  	if placeholder == "" {
  2159  		if got != nil {
  2160  			mark.errorf("PrepareRename(...) = %v, want nil", got)
  2161  		}
  2162  		return
  2163  	}
  2164  	want := &protocol.PrepareRenameResult{Range: spn.Range, Placeholder: placeholder}
  2165  	if diff := cmp.Diff(want, got); diff != "" {
  2166  		mark.errorf("mismatching PrepareRename result:\n%s", diff)
  2167  	}
  2168  }
  2169  
  2170  // symbolMarker implements the @symbol marker.
  2171  func symbolMarker(mark marker, golden *Golden) {
  2172  	// Retrieve information about all symbols in this file.
  2173  	symbols, err := mark.server().DocumentSymbol(mark.ctx(), &protocol.DocumentSymbolParams{
  2174  		TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()},
  2175  	})
  2176  	if err != nil {
  2177  		mark.errorf("DocumentSymbol request failed: %v", err)
  2178  		return
  2179  	}
  2180  
  2181  	// Format symbols one per line, sorted (in effect) by first column, a dotted name.
  2182  	var lines []string
  2183  	for _, symbol := range symbols {
  2184  		// Each result element is a union of (legacy)
  2185  		// SymbolInformation and (new) DocumentSymbol,
  2186  		// so we ascertain which one and then transcode.
  2187  		data, err := json.Marshal(symbol)
  2188  		if err != nil {
  2189  			mark.T().Fatal(err)
  2190  		}
  2191  		if _, ok := symbol.(map[string]any)["location"]; ok {
  2192  			// This case is not reached because Editor initialization
  2193  			// enables HierarchicalDocumentSymbolSupport.
  2194  			// TODO(adonovan): test this too.
  2195  			var sym protocol.SymbolInformation
  2196  			if err := json.Unmarshal(data, &sym); err != nil {
  2197  				mark.T().Fatal(err)
  2198  			}
  2199  			mark.errorf("fake Editor doesn't support SymbolInformation")
  2200  
  2201  		} else {
  2202  			var sym protocol.DocumentSymbol // new hierarchical hotness
  2203  			if err := json.Unmarshal(data, &sym); err != nil {
  2204  				mark.T().Fatal(err)
  2205  			}
  2206  
  2207  			// Print each symbol in the response tree.
  2208  			var visit func(sym protocol.DocumentSymbol, prefix []string)
  2209  			visit = func(sym protocol.DocumentSymbol, prefix []string) {
  2210  				var out strings.Builder
  2211  				out.WriteString(strings.Join(prefix, "."))
  2212  				fmt.Fprintf(&out, " %q", sym.Detail)
  2213  				if delta := sym.Range.End.Line - sym.Range.Start.Line; delta > 0 {
  2214  					fmt.Fprintf(&out, " +%d lines", delta)
  2215  				}
  2216  				lines = append(lines, out.String())
  2217  
  2218  				for _, child := range sym.Children {
  2219  					visit(child, append(prefix, child.Name))
  2220  				}
  2221  			}
  2222  			visit(sym, []string{sym.Name})
  2223  		}
  2224  	}
  2225  	sort.Strings(lines)
  2226  	lines = append(lines, "") // match trailing newline in .txtar file
  2227  	got := []byte(strings.Join(lines, "\n"))
  2228  
  2229  	// Compare with golden.
  2230  	want, ok := golden.Get(mark.T(), "", got)
  2231  	if !ok {
  2232  		mark.errorf("%s: missing golden file @%s", mark.note.Name, golden.id)
  2233  	} else if diff := cmp.Diff(string(got), string(want)); diff != "" {
  2234  		mark.errorf("%s: unexpected output: got:\n%s\nwant:\n%s\ndiff:\n%s",
  2235  			mark.note.Name, got, want, diff)
  2236  	}
  2237  }
  2238  
  2239  // compareLocations returns an error message if got and want are not
  2240  // the same set of locations. The marker is used only for fmtLoc.
  2241  func compareLocations(mark marker, got, want []protocol.Location) error {
  2242  	toStrings := func(locs []protocol.Location) []string {
  2243  		strs := make([]string, len(locs))
  2244  		for i, loc := range locs {
  2245  			strs[i] = mark.run.fmtLoc(loc)
  2246  		}
  2247  		sort.Strings(strs)
  2248  		return strs
  2249  	}
  2250  	if diff := cmp.Diff(toStrings(want), toStrings(got)); diff != "" {
  2251  		return fmt.Errorf("incorrect result locations: (got %d, want %d):\n%s",
  2252  			len(got), len(want), diff)
  2253  	}
  2254  	return nil
  2255  }
  2256  
  2257  func workspaceSymbolMarker(mark marker, query string, golden *Golden) {
  2258  	params := &protocol.WorkspaceSymbolParams{
  2259  		Query: query,
  2260  	}
  2261  
  2262  	gotSymbols, err := mark.server().Symbol(mark.ctx(), params)
  2263  	if err != nil {
  2264  		mark.errorf("Symbol(%q) failed: %v", query, err)
  2265  		return
  2266  	}
  2267  	var got bytes.Buffer
  2268  	for _, s := range gotSymbols {
  2269  		// Omit the txtar position of the symbol location; otherwise edits to the
  2270  		// txtar archive lead to unexpected failures.
  2271  		loc := mark.run.fmtLocDetails(s.Location, false)
  2272  		// TODO(rfindley): can we do better here, by detecting if the location is
  2273  		// relative to GOROOT?
  2274  		if loc == "" {
  2275  			loc = "<unknown>"
  2276  		}
  2277  		fmt.Fprintf(&got, "%s %s %s\n", loc, s.Name, s.Kind)
  2278  	}
  2279  
  2280  	compareGolden(mark, got.Bytes(), golden)
  2281  }
  2282  
  2283  // compareGolden compares the content of got with that of g.Get(""), reporting
  2284  // errors on any mismatch.
  2285  //
  2286  // TODO(rfindley): use this helper in more places.
  2287  func compareGolden(mark marker, got []byte, g *Golden) {
  2288  	want, ok := g.Get(mark.T(), "", got)
  2289  	if !ok {
  2290  		mark.errorf("missing golden file @%s", g.id)
  2291  		return
  2292  	}
  2293  	// Normalize newline termination: archive files (i.e. Golden content) can't
  2294  	// contain non-newline terminated files, except in the special case where the
  2295  	// file is completely empty.
  2296  	//
  2297  	// Note that txtar partitions a contiguous byte slice, so we must copy before
  2298  	// appending.
  2299  	normalize := func(s []byte) []byte {
  2300  		if n := len(s); n > 0 && s[n-1] != '\n' {
  2301  			s = append(s[:n:n], '\n') // don't mutate array
  2302  		}
  2303  		return s
  2304  	}
  2305  	got = normalize(got)
  2306  	want = normalize(want)
  2307  	if diff := compare.Bytes(want, got); diff != "" {
  2308  		mark.errorf("%s does not match @%s:\n%s", mark.note.Name, g.id, diff)
  2309  	}
  2310  }