var update … func TestMain(m *testing.M) { … } // Test runs the marker tests from the testdata directory. // // See package documentation for details on how marker tests work. // // These tests were inspired by (and in many places copied from) a previous // iteration of the marker tests built on top of the packagestest framework. // Key design decisions motivating this reimplementation are as follows: // - The old tests had a single global session, causing interaction at a // distance and several awkward workarounds. // - The old tests could not be safely parallelized, because certain tests // manipulated the server options // - Relatedly, the old tests did not have a logic grouping of assertions into // a single unit, resulting in clusters of files serving clusters of // entangled assertions. // - The old tests used locations in the source as test names and as the // identity of golden content, meaning that a single edit could change the // name of an arbitrary number of subtests, and making it difficult to // manually edit golden content. // - The old tests did not hew closely to LSP concepts, resulting in, for // example, each marker implementation doing its own position // transformations, and inventing its own mechanism for configuration. // - The old tests had an ad-hoc session initialization process. The integration // test environment has had more time devoted to its initialization, and has a // more convenient API. // - The old tests lacked documentation, and often had failures that were hard // to understand. By starting from scratch, we can revisit these aspects. func Test(t *testing.T) { … } type marker … // ctx returns the mark context. func (m marker) ctx() context.Context { … } // T returns the testing.TB for this mark. func (m marker) T() testing.TB { … } // server returns the LSP server for the marker test run. func (m marker) editor() *fake.Editor { … } // server returns the LSP server for the marker test run. func (m marker) server() protocol.Server { … } // uri returns the URI of the file containing the marker. func (mark marker) uri() protocol.DocumentURI { … } // document returns a protocol.TextDocumentIdentifier for the current file. func (mark marker) document() protocol.TextDocumentIdentifier { … } // path returns the relative path to the file containing the marker. func (mark marker) path() string { … } // mapper returns a *protocol.Mapper for the current file. func (mark marker) mapper() *protocol.Mapper { … } // errorf reports an error with a prefix indicating the position of the marker note. // // It formats the error message using mark.sprintf. func (mark marker) errorf(format string, args ...any) { … } // valueMarkerFunc returns a wrapper around a function that allows it to be // called during the processing of value markers (e.g. @value(v, 123)) with marker // arguments converted to function parameters. The provided function's first // parameter must be of type 'marker', and it must return a value. // // Unlike action markers, which are executed for actions such as test // assertions, value markers are all evaluated first, and each computes // a value that is recorded by its identifier, which is the marker's first // argument. These values may be referred to from an action marker by // this identifier, e.g. @action(... , v, ...). // // For example, given a fn with signature // // func(mark marker, label, details, kind string) CompletionItem // // The result of valueMarkerFunc can associated with @item notes, and invoked // as follows: // // //@item(FooCompletion, "Foo", "func() int", "func") // // The provided fn should not mutate the test environment. func valueMarkerFunc(fn any) func(marker) { … } // actionMarkerFunc returns a wrapper around a function that allows it to be // called during the processing of action markers (e.g. @action("abc", 123)) // with marker arguments converted to function parameters. The provided // function's first parameter must be of type 'marker', and it must not return // any values. // // The provided fn should not mutate the test environment. func actionMarkerFunc(fn any) func(marker) { … } func convertArgs(mark marker, ftype reflect.Type, args []any) ([]reflect.Value, error) { … } // is reports whether arg is a T. func is[T any](arg any) bool { … } var valueMarkerFuncs … var actionMarkerFuncs … type markerTest … // flagSet returns the flagset used for parsing the special "flags" file in the // test archive. func (t *markerTest) flagSet() *flag.FlagSet { … } type stringListValue … func (l *stringListValue) Set(s string) error { … } func (l stringListValue) String() string { … } func (t *markerTest) getGolden(id expect.Identifier) *Golden { … } type Golden … // Get returns golden content for the given name, which corresponds to the // relative path following the golden prefix @<name>/. For example, to access // the content of @foo/path/to/result.json from the Golden associated with // @foo, name should be "path/to/result.json". // // If -update is set, the given update function will be called to get the // updated golden content that should be written back to testdata. // // Marker functions must use this method instead of accessing data entries // directly otherwise the -update operation will delete those entries. // // TODO(rfindley): rethink the logic here. We may want to separate Get and Set, // and not delete golden content that isn't set. func (g *Golden) Get(t testing.TB, name string, updated []byte) ([]byte, bool) { … } // loadMarkerTests walks the given dir looking for .txt files, which it // interprets as a txtar archive. // // See the documentation for RunMarkerTests for more details on the test data // archive. func loadMarkerTests(dir string) ([]*markerTest, error) { … } func loadMarkerTest(name string, content []byte) (*markerTest, error) { … } // formatTest formats the test as a txtar archive. func formatTest(test *markerTest) ([]byte, error) { … } // newEnv creates a new environment for a marker test. // // TODO(rfindley): simplify and refactor the construction of testing // environments across integration tests, marker tests, and benchmarks. func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byte, writeGoSum []string, config fake.EditorConfig) *integration.Env { … } type markerTestRun … // sprintf returns a formatted string after applying pre-processing to // arguments of the following types: // - token.Pos: formatted using (*markerTestRun).fmtPos // - protocol.Location: formatted using (*markerTestRun).fmtLoc func (c *marker) sprintf(format string, args ...any) string { … } // fmtPos formats the given pos in the context of the test, using // archive-relative paths for files and including the line number in the full // archive file. func (run *markerTestRun) fmtPos(pos token.Pos) string { … } // fmtLoc formats the given location in the context of the test, using // archive-relative paths for files and including the line number in the full // archive file. func (run *markerTestRun) fmtLoc(loc protocol.Location) string { … } // See fmtLoc. If includeTxtPos is not set, the position in the full archive // file is omitted. // // If the location cannot be found within the archive, fmtLocDetails returns "". func (run *markerTestRun) fmtLocDetails(loc protocol.Location, includeTxtPos bool) string { … } var goldenType … var markerType … var stringMatcherType … var customConverters … // converter transforms a typed argument conversion function to an untyped // conversion function. func converter[T any](f func(marker, any) (T, error)) func(marker, any) (any, error) { … } func convert(mark marker, arg any, paramType reflect.Type) (any, error) { … } // convertLocation converts a string or regexp argument into the protocol // location corresponding to the first position of the string (or first match // of the regexp) in the line preceding the note. func convertLocation(mark marker, arg any) (protocol.Location, error) { … } type completionLabel … // convertCompletionLabel coerces an argument to a [completionLabel] parameter // type. // // If the arg is a string, it is trivially converted. If the arg is a // completionItem, its label is extracted. // // This allows us to stage a migration of the "snippet" marker to a simpler // model where the completion label can just be listed explicitly. func convertCompletionLabel(mark marker, arg any) (completionLabel, error) { … } // convertStringMatcher converts a string, regexp, or identifier // argument into a stringMatcher. The string is a substring of the // expected error, the regexp is a pattern than matches the expected // error, and the identifier is a golden file containing the expected // error. func convertStringMatcher(mark marker, arg any) (stringMatcher, error) { … } type stringMatcher … func (sc stringMatcher) String() string { … } // checkErr asserts that the given error matches the stringMatcher's expectations. func (sc stringMatcher) checkErr(mark marker, err error) { … } // check asserts that the given content matches the stringMatcher's expectations. func (sc stringMatcher) check(mark marker, got string) { … } // checkChangedFiles compares the files changed by an operation with their expected (golden) state. func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) { … } // checkDiffs computes unified diffs for each changed file, and compares with // the diff content stored in the given golden directory. func checkDiffs(mark marker, changed map[string][]byte, golden *Golden) { … } type completionItem … func completionItemMarker(mark marker, label string, other ...string) completionItem { … } func rankMarker(mark marker, src protocol.Location, items ...completionLabel) { … } func snippetMarker(mark marker, src protocol.Location, label completionLabel, want string) { … } // completeMarker implements the @complete marker, running // textDocument/completion at the given src location and asserting that the // results match the expected results. func completeMarker(mark marker, src protocol.Location, want ...completionItem) { … } // filterBuiltinsAndKeywords filters out builtins and keywords from completion // results. // // It over-approximates, and does not detect if builtins are shadowed. func filterBuiltinsAndKeywords(mark marker, items []protocol.CompletionItem) []protocol.CompletionItem { … } // acceptCompletionMarker implements the @acceptCompletion marker, running // textDocument/completion at the given src location and accepting the // candidate with the given label. The resulting source must match the provided // golden content. func acceptCompletionMarker(mark marker, src protocol.Location, label string, golden *Golden) { … } // defMarker implements the @def marker, running textDocument/definition at // the given src location and asserting that there is exactly one resulting // location, matching dst. // // TODO(rfindley): support a variadic destination set. func defMarker(mark marker, src, dst protocol.Location) { … } func typedefMarker(mark marker, src, dst protocol.Location) { … } func foldingRangeMarker(mark marker, g *Golden) { … } // formatMarker implements the @format marker. func formatMarker(mark marker, golden *Golden) { … } func highlightLocationMarker(mark marker, loc protocol.Location, kindName expect.Identifier) protocol.DocumentHighlight { … } func sortDocumentHighlights(s []protocol.DocumentHighlight) { … } // highlightAllMarker makes textDocument/highlight // requests at locations of equivalence classes. Given input // highlightall(X1, X2, ..., Xn), the marker checks // highlight(X1) = highlight(X2) = ... = highlight(Xn) = {X1, X2, ..., Xn}. // It is not the general rule for all highlighting, and use @highlight // for asymmetric cases. // // TODO(b/288111111): this is a bit of a hack. We should probably // have a more general way of testing that a function is idempotent. func highlightAllMarker(mark marker, all ...protocol.DocumentHighlight) { … } func highlightMarker(mark marker, src protocol.DocumentHighlight, dsts ...protocol.DocumentHighlight) { … } func hoverMarker(mark marker, src, dst protocol.Location, sc stringMatcher) { … } func hoverErrMarker(mark marker, src protocol.Location, em stringMatcher) { … } // locMarker implements the @loc marker. It is executed before other // markers, so that locations are available. func locMarker(mark marker, loc protocol.Location) protocol.Location { … } // diagMarker implements the @diag marker. It eliminates diagnostics from // the observed set in mark.test. func diagMarker(mark marker, loc protocol.Location, re *regexp.Regexp) { … } // removeDiagnostic looks for a diagnostic matching loc at the given position. // // If found, it returns (diag, true), and eliminates the matched diagnostic // from the unmatched set. // // If not found, it returns (protocol.Diagnostic{}, false). func removeDiagnostic(mark marker, loc protocol.Location, re *regexp.Regexp) (protocol.Diagnostic, bool) { … } // renameMarker implements the @rename(location, new, golden) marker. func renameMarker(mark marker, loc protocol.Location, newName string, golden *Golden) { … } // renameErrMarker implements the @renamererr(location, new, error) marker. func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr stringMatcher) { … } func selectionRangeMarker(mark marker, loc protocol.Location, g *Golden) { … } func tokenMarker(mark marker, loc protocol.Location, tokenType, mod string) { … } func signatureMarker(mark marker, src protocol.Location, label string, active int64) { … } // rename returns the new contents of the files that would be modified // by renaming the identifier at loc to newName. func rename(env *integration.Env, loc protocol.Location, newName string) (map[string][]byte, error) { … } // changedFiles applies the given sequence of document changes to the // editor buffer content, recording the final contents in the returned map. // The actual editor state is not changed. // Deleted files are indicated by a content of []byte(nil). // // See also: // - Editor.applyWorkspaceEdit ../integration/fake/editor.go for the // implementation of this operation used in normal testing. // - cmdClient.applyWorkspaceEdit in ../../../cmd/cmd.go for the // CLI variant. func changedFiles(env *integration.Env, changes []protocol.DocumentChange) (map[string][]byte, error) { … } func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden) { … } func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden) { … } func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr stringMatcher) { … } // codeLensesMarker runs the @codelenses() marker, collecting @codelens marks // in the current file and comparing with the result of the // textDocument/codeLens RPC. func codeLensesMarker(mark marker) { … } func documentLinkMarker(mark marker, g *Golden) { … } // consumeExtraNotes runs the provided func for each extra note with the given // name, and deletes all matching notes. func (mark marker) consumeExtraNotes(name string, f func(marker)) { … } // quickfixMarker implements the @quickfix(location, regexp, // kind, golden) marker. It acts like @diag(location, regexp), to set // the expectation of a diagnostic, but then it applies the "quickfix" // code action (which must be unique) suggested by the matched diagnostic. func quickfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) { … } func quickfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp, wantErr stringMatcher) { … } // codeAction executes a textDocument/codeAction request for the specified // location and kind. If diag is non-nil, it is used as the code action // context. // // The resulting map contains resulting file contents after the code action is // applied. Currently, this function does not support code actions that return // edits directly; it only supports code action commands. func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (map[string][]byte, error) { … } // codeActionChanges executes a textDocument/codeAction request for the // specified location and kind, and captures the resulting document changes. // If diag is non-nil, it is used as the code action context. func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) ([]protocol.DocumentChange, error) { … } // refsMarker implements the @refs marker. func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { … } // implementationMarker implements the @implementation marker. func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) { … } func itemLocation(item protocol.CallHierarchyItem) protocol.Location { … } func incomingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { … } func outgoingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { … } type callHierarchyFunc … func callHierarchy(mark marker, src protocol.Location, getCalls callHierarchyFunc, want []protocol.Location) { … } func inlayhintsMarker(mark marker, g *Golden) { … } func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder string) { … } // symbolMarker implements the @symbol marker. func symbolMarker(mark marker, golden *Golden) { … } // compareLocations returns an error message if got and want are not // the same set of locations. The marker is used only for fmtLoc. func compareLocations(mark marker, got, want []protocol.Location) error { … } func workspaceSymbolMarker(mark marker, query string, golden *Golden) { … } // compareGolden compares the content of got with that of g.Get(""), reporting // errors on any mismatch. // // TODO(rfindley): use this helper in more places. func compareGolden(mark marker, got []byte, g *Golden) { … }