golang.org/x/tools/gopls@v0.15.3/internal/test/integration/expectation.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package integration
     6  
     7  import (
     8  	"fmt"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	"golang.org/x/tools/gopls/internal/protocol"
    15  	"golang.org/x/tools/gopls/internal/server"
    16  )
    17  
    18  var (
    19  	// InitialWorkspaceLoad is an expectation that the workspace initial load has
    20  	// completed. It is verified via workdone reporting.
    21  	InitialWorkspaceLoad = CompletedWork(server.DiagnosticWorkTitle(server.FromInitialWorkspaceLoad), 1, false)
    22  )
    23  
    24  // A Verdict is the result of checking an expectation against the current
    25  // editor state.
    26  type Verdict int
    27  
    28  // Order matters for the following constants: verdicts are sorted in order of
    29  // decisiveness.
    30  const (
    31  	// Met indicates that an expectation is satisfied by the current state.
    32  	Met Verdict = iota
    33  	// Unmet indicates that an expectation is not currently met, but could be met
    34  	// in the future.
    35  	Unmet
    36  	// Unmeetable indicates that an expectation cannot be satisfied in the
    37  	// future.
    38  	Unmeetable
    39  )
    40  
    41  func (v Verdict) String() string {
    42  	switch v {
    43  	case Met:
    44  		return "Met"
    45  	case Unmet:
    46  		return "Unmet"
    47  	case Unmeetable:
    48  		return "Unmeetable"
    49  	}
    50  	return fmt.Sprintf("unrecognized verdict %d", v)
    51  }
    52  
    53  // An Expectation is an expected property of the state of the LSP client.
    54  // The Check function reports whether the property is met.
    55  //
    56  // Expectations are combinators. By composing them, tests may express
    57  // complex expectations in terms of simpler ones.
    58  //
    59  // TODO(rfindley): as expectations are combined, it becomes harder to identify
    60  // why they failed. A better signature for Check would be
    61  //
    62  //	func(State) (Verdict, string)
    63  //
    64  // returning a reason for the verdict that can be composed similarly to
    65  // descriptions.
    66  type Expectation struct {
    67  	Check func(State) Verdict
    68  
    69  	// Description holds a noun-phrase identifying what the expectation checks.
    70  	//
    71  	// TODO(rfindley): revisit existing descriptions to ensure they compose nicely.
    72  	Description string
    73  }
    74  
    75  // OnceMet returns an Expectation that, once the precondition is met, asserts
    76  // that mustMeet is met.
    77  func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation {
    78  	check := func(s State) Verdict {
    79  		switch pre := precondition.Check(s); pre {
    80  		case Unmeetable:
    81  			return Unmeetable
    82  		case Met:
    83  			for _, mustMeet := range mustMeets {
    84  				verdict := mustMeet.Check(s)
    85  				if verdict != Met {
    86  					return Unmeetable
    87  				}
    88  			}
    89  			return Met
    90  		default:
    91  			return Unmet
    92  		}
    93  	}
    94  	description := describeExpectations(mustMeets...)
    95  	return Expectation{
    96  		Check:       check,
    97  		Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description),
    98  	}
    99  }
   100  
   101  func describeExpectations(expectations ...Expectation) string {
   102  	var descriptions []string
   103  	for _, e := range expectations {
   104  		descriptions = append(descriptions, e.Description)
   105  	}
   106  	return strings.Join(descriptions, "\n")
   107  }
   108  
   109  // Not inverts the sense of an expectation: a met expectation is unmet, and an
   110  // unmet expectation is met.
   111  func Not(e Expectation) Expectation {
   112  	check := func(s State) Verdict {
   113  		switch v := e.Check(s); v {
   114  		case Met:
   115  			return Unmet
   116  		case Unmet, Unmeetable:
   117  			return Met
   118  		default:
   119  			panic(fmt.Sprintf("unexpected verdict %v", v))
   120  		}
   121  	}
   122  	description := describeExpectations(e)
   123  	return Expectation{
   124  		Check:       check,
   125  		Description: fmt.Sprintf("not: %s", description),
   126  	}
   127  }
   128  
   129  // AnyOf returns an expectation that is satisfied when any of the given
   130  // expectations is met.
   131  func AnyOf(anyOf ...Expectation) Expectation {
   132  	check := func(s State) Verdict {
   133  		for _, e := range anyOf {
   134  			verdict := e.Check(s)
   135  			if verdict == Met {
   136  				return Met
   137  			}
   138  		}
   139  		return Unmet
   140  	}
   141  	description := describeExpectations(anyOf...)
   142  	return Expectation{
   143  		Check:       check,
   144  		Description: fmt.Sprintf("Any of:\n%s", description),
   145  	}
   146  }
   147  
   148  // AllOf expects that all given expectations are met.
   149  //
   150  // TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf
   151  // and AllOf) is that we lose the information of *why* they failed: the Awaiter
   152  // is not smart enough to look inside.
   153  //
   154  // Refactor the API such that the Check function is responsible for explaining
   155  // why an expectation failed. This should allow us to significantly improve
   156  // test output: we won't need to summarize state at all, as the verdict
   157  // explanation itself should describe clearly why the expectation not met.
   158  func AllOf(allOf ...Expectation) Expectation {
   159  	check := func(s State) Verdict {
   160  		verdict := Met
   161  		for _, e := range allOf {
   162  			if v := e.Check(s); v > verdict {
   163  				verdict = v
   164  			}
   165  		}
   166  		return verdict
   167  	}
   168  	description := describeExpectations(allOf...)
   169  	return Expectation{
   170  		Check:       check,
   171  		Description: fmt.Sprintf("All of:\n%s", description),
   172  	}
   173  }
   174  
   175  // ReadDiagnostics is an Expectation that stores the current diagnostics for
   176  // fileName in into, whenever it is evaluated.
   177  //
   178  // It can be used in combination with OnceMet or AfterChange to capture the
   179  // state of diagnostics when other expectations are satisfied.
   180  func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation {
   181  	check := func(s State) Verdict {
   182  		diags, ok := s.diagnostics[fileName]
   183  		if !ok {
   184  			return Unmeetable
   185  		}
   186  		*into = *diags
   187  		return Met
   188  	}
   189  	return Expectation{
   190  		Check:       check,
   191  		Description: fmt.Sprintf("read diagnostics for %q", fileName),
   192  	}
   193  }
   194  
   195  // ReadAllDiagnostics is an expectation that stores all published diagnostics
   196  // into the provided map, whenever it is evaluated.
   197  //
   198  // It can be used in combination with OnceMet or AfterChange to capture the
   199  // state of diagnostics when other expectations are satisfied.
   200  func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Expectation {
   201  	check := func(s State) Verdict {
   202  		allDiags := make(map[string]*protocol.PublishDiagnosticsParams)
   203  		for name, diags := range s.diagnostics {
   204  			allDiags[name] = diags
   205  		}
   206  		*into = allDiags
   207  		return Met
   208  	}
   209  	return Expectation{
   210  		Check:       check,
   211  		Description: "read all diagnostics",
   212  	}
   213  }
   214  
   215  // ShownDocument asserts that the client has received a
   216  // ShowDocumentRequest for the given URI.
   217  func ShownDocument(uri protocol.URI) Expectation {
   218  	check := func(s State) Verdict {
   219  		for _, params := range s.showDocument {
   220  			if params.URI == uri {
   221  				return Met
   222  			}
   223  		}
   224  		return Unmet
   225  	}
   226  	return Expectation{
   227  		Check:       check,
   228  		Description: fmt.Sprintf("received window/showDocument for URI %s", uri),
   229  	}
   230  }
   231  
   232  // NoShownMessage asserts that the editor has not received a ShowMessage.
   233  func NoShownMessage(subString string) Expectation {
   234  	check := func(s State) Verdict {
   235  		for _, m := range s.showMessage {
   236  			if strings.Contains(m.Message, subString) {
   237  				return Unmeetable
   238  			}
   239  		}
   240  		return Met
   241  	}
   242  	return Expectation{
   243  		Check:       check,
   244  		Description: fmt.Sprintf("no ShowMessage received containing %q", subString),
   245  	}
   246  }
   247  
   248  // ShownMessage asserts that the editor has received a ShowMessageRequest
   249  // containing the given substring.
   250  func ShownMessage(containing string) Expectation {
   251  	check := func(s State) Verdict {
   252  		for _, m := range s.showMessage {
   253  			if strings.Contains(m.Message, containing) {
   254  				return Met
   255  			}
   256  		}
   257  		return Unmet
   258  	}
   259  	return Expectation{
   260  		Check:       check,
   261  		Description: fmt.Sprintf("received window/showMessage containing %q", containing),
   262  	}
   263  }
   264  
   265  // ShownMessageRequest asserts that the editor has received a
   266  // ShowMessageRequest with message matching the given regular expression.
   267  func ShownMessageRequest(messageRegexp string) Expectation {
   268  	msgRE := regexp.MustCompile(messageRegexp)
   269  	check := func(s State) Verdict {
   270  		if len(s.showMessageRequest) == 0 {
   271  			return Unmet
   272  		}
   273  		for _, m := range s.showMessageRequest {
   274  			if msgRE.MatchString(m.Message) {
   275  				return Met
   276  			}
   277  		}
   278  		return Unmet
   279  	}
   280  	return Expectation{
   281  		Check:       check,
   282  		Description: fmt.Sprintf("ShowMessageRequest matching %q", messageRegexp),
   283  	}
   284  }
   285  
   286  // DoneDiagnosingChanges expects that diagnostics are complete from common
   287  // change notifications: didOpen, didChange, didSave, didChangeWatchedFiles,
   288  // and didClose.
   289  //
   290  // This can be used when multiple notifications may have been sent, such as
   291  // when a didChange is immediately followed by a didSave. It is insufficient to
   292  // simply await NoOutstandingWork, because the LSP client has no control over
   293  // when the server starts processing a notification. Therefore, we must keep
   294  // track of
   295  func (e *Env) DoneDiagnosingChanges() Expectation {
   296  	stats := e.Editor.Stats()
   297  	statsBySource := map[server.ModificationSource]uint64{
   298  		server.FromDidOpen:                stats.DidOpen,
   299  		server.FromDidChange:              stats.DidChange,
   300  		server.FromDidSave:                stats.DidSave,
   301  		server.FromDidChangeWatchedFiles:  stats.DidChangeWatchedFiles,
   302  		server.FromDidClose:               stats.DidClose,
   303  		server.FromDidChangeConfiguration: stats.DidChangeConfiguration,
   304  	}
   305  
   306  	var expected []server.ModificationSource
   307  	for k, v := range statsBySource {
   308  		if v > 0 {
   309  			expected = append(expected, k)
   310  		}
   311  	}
   312  
   313  	// Sort for stability.
   314  	sort.Slice(expected, func(i, j int) bool {
   315  		return expected[i] < expected[j]
   316  	})
   317  
   318  	var all []Expectation
   319  	for _, source := range expected {
   320  		all = append(all, CompletedWork(server.DiagnosticWorkTitle(source), statsBySource[source], true))
   321  	}
   322  
   323  	return AllOf(all...)
   324  }
   325  
   326  // AfterChange expects that the given expectations will be met after all
   327  // state-changing notifications have been processed by the server.
   328  //
   329  // It awaits the completion of all anticipated work before checking the given
   330  // expectations.
   331  func (e *Env) AfterChange(expectations ...Expectation) {
   332  	e.T.Helper()
   333  	e.OnceMet(
   334  		e.DoneDiagnosingChanges(),
   335  		expectations...,
   336  	)
   337  }
   338  
   339  // DoneWithOpen expects all didOpen notifications currently sent by the editor
   340  // to be completely processed.
   341  func (e *Env) DoneWithOpen() Expectation {
   342  	opens := e.Editor.Stats().DidOpen
   343  	return CompletedWork(server.DiagnosticWorkTitle(server.FromDidOpen), opens, true)
   344  }
   345  
   346  // StartedChange expects that the server has at least started processing all
   347  // didChange notifications sent from the client.
   348  func (e *Env) StartedChange() Expectation {
   349  	changes := e.Editor.Stats().DidChange
   350  	return StartedWork(server.DiagnosticWorkTitle(server.FromDidChange), changes)
   351  }
   352  
   353  // DoneWithChange expects all didChange notifications currently sent by the
   354  // editor to be completely processed.
   355  func (e *Env) DoneWithChange() Expectation {
   356  	changes := e.Editor.Stats().DidChange
   357  	return CompletedWork(server.DiagnosticWorkTitle(server.FromDidChange), changes, true)
   358  }
   359  
   360  // DoneWithSave expects all didSave notifications currently sent by the editor
   361  // to be completely processed.
   362  func (e *Env) DoneWithSave() Expectation {
   363  	saves := e.Editor.Stats().DidSave
   364  	return CompletedWork(server.DiagnosticWorkTitle(server.FromDidSave), saves, true)
   365  }
   366  
   367  // StartedChangeWatchedFiles expects that the server has at least started
   368  // processing all didChangeWatchedFiles notifications sent from the client.
   369  func (e *Env) StartedChangeWatchedFiles() Expectation {
   370  	changes := e.Editor.Stats().DidChangeWatchedFiles
   371  	return StartedWork(server.DiagnosticWorkTitle(server.FromDidChangeWatchedFiles), changes)
   372  }
   373  
   374  // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications
   375  // currently sent by the editor to be completely processed.
   376  func (e *Env) DoneWithChangeWatchedFiles() Expectation {
   377  	changes := e.Editor.Stats().DidChangeWatchedFiles
   378  	return CompletedWork(server.DiagnosticWorkTitle(server.FromDidChangeWatchedFiles), changes, true)
   379  }
   380  
   381  // DoneWithClose expects all didClose notifications currently sent by the
   382  // editor to be completely processed.
   383  func (e *Env) DoneWithClose() Expectation {
   384  	changes := e.Editor.Stats().DidClose
   385  	return CompletedWork(server.DiagnosticWorkTitle(server.FromDidClose), changes, true)
   386  }
   387  
   388  // StartedWork expect a work item to have been started >= atLeast times.
   389  //
   390  // See CompletedWork.
   391  func StartedWork(title string, atLeast uint64) Expectation {
   392  	check := func(s State) Verdict {
   393  		if s.startedWork()[title] >= atLeast {
   394  			return Met
   395  		}
   396  		return Unmet
   397  	}
   398  	return Expectation{
   399  		Check:       check,
   400  		Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast),
   401  	}
   402  }
   403  
   404  // CompletedWork expects a work item to have been completed >= atLeast times.
   405  //
   406  // Since the Progress API doesn't include any hidden metadata, we must use the
   407  // progress notification title to identify the work we expect to be completed.
   408  func CompletedWork(title string, count uint64, atLeast bool) Expectation {
   409  	check := func(s State) Verdict {
   410  		completed := s.completedWork()
   411  		if completed[title] == count || atLeast && completed[title] > count {
   412  			return Met
   413  		}
   414  		return Unmet
   415  	}
   416  	desc := fmt.Sprintf("completed work %q %v times", title, count)
   417  	if atLeast {
   418  		desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count)
   419  	}
   420  	return Expectation{
   421  		Check:       check,
   422  		Description: desc,
   423  	}
   424  }
   425  
   426  type WorkStatus struct {
   427  	// Last seen message from either `begin` or `report` progress.
   428  	Msg string
   429  	// Message sent with `end` progress message.
   430  	EndMsg string
   431  }
   432  
   433  // CompletedProgress expects that workDone progress is complete for the given
   434  // progress token. When non-nil WorkStatus is provided, it will be filled
   435  // when the expectation is met.
   436  //
   437  // If the token is not a progress token that the client has seen, this
   438  // expectation is Unmeetable.
   439  func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) Expectation {
   440  	check := func(s State) Verdict {
   441  		work, ok := s.work[token]
   442  		if !ok {
   443  			return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result
   444  		}
   445  		if work.complete {
   446  			if into != nil {
   447  				into.Msg = work.msg
   448  				into.EndMsg = work.endMsg
   449  			}
   450  			return Met
   451  		}
   452  		return Unmet
   453  	}
   454  	desc := fmt.Sprintf("completed work for token %v", token)
   455  	return Expectation{
   456  		Check:       check,
   457  		Description: desc,
   458  	}
   459  }
   460  
   461  // OutstandingWork expects a work item to be outstanding. The given title must
   462  // be an exact match, whereas the given msg must only be contained in the work
   463  // item's message.
   464  func OutstandingWork(title, msg string) Expectation {
   465  	check := func(s State) Verdict {
   466  		for _, work := range s.work {
   467  			if work.complete {
   468  				continue
   469  			}
   470  			if work.title == title && strings.Contains(work.msg, msg) {
   471  				return Met
   472  			}
   473  		}
   474  		return Unmet
   475  	}
   476  	return Expectation{
   477  		Check:       check,
   478  		Description: fmt.Sprintf("outstanding work: %q containing %q", title, msg),
   479  	}
   480  }
   481  
   482  // NoOutstandingWork asserts that there is no work initiated using the LSP
   483  // $/progress API that has not completed.
   484  //
   485  // If non-nil, the ignore func is used to ignore certain work items for the
   486  // purpose of this check.
   487  //
   488  // TODO(rfindley): consider refactoring to treat outstanding work the same way
   489  // we treat diagnostics: with an algebra of filters.
   490  func NoOutstandingWork(ignore func(title, msg string) bool) Expectation {
   491  	check := func(s State) Verdict {
   492  		for _, w := range s.work {
   493  			if w.complete {
   494  				continue
   495  			}
   496  			if w.title == "" {
   497  				// A token that has been created but not yet used.
   498  				//
   499  				// TODO(rfindley): this should be separated in the data model: until
   500  				// the "begin" notification, work should not be in progress.
   501  				continue
   502  			}
   503  			if ignore != nil && ignore(w.title, w.msg) {
   504  				continue
   505  			}
   506  			return Unmet
   507  		}
   508  		return Met
   509  	}
   510  	return Expectation{
   511  		Check:       check,
   512  		Description: "no outstanding work",
   513  	}
   514  }
   515  
   516  // IgnoreTelemetryPromptWork may be used in conjunction with NoOutStandingWork
   517  // to ignore the telemetry prompt.
   518  func IgnoreTelemetryPromptWork(title, msg string) bool {
   519  	return title == server.TelemetryPromptWorkTitle
   520  }
   521  
   522  // NoErrorLogs asserts that the client has not received any log messages of
   523  // error severity.
   524  func NoErrorLogs() Expectation {
   525  	return NoLogMatching(protocol.Error, "")
   526  }
   527  
   528  // LogMatching asserts that the client has received a log message
   529  // of type typ matching the regexp re a certain number of times.
   530  //
   531  // The count argument specifies the expected number of matching logs. If
   532  // atLeast is set, this is a lower bound, otherwise there must be exactly count
   533  // matching logs.
   534  //
   535  // Logs are asynchronous to other LSP messages, so this expectation should not
   536  // be used with combinators such as OnceMet or AfterChange that assert on
   537  // ordering with respect to other operations.
   538  func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) Expectation {
   539  	rec, err := regexp.Compile(re)
   540  	if err != nil {
   541  		panic(err)
   542  	}
   543  	check := func(state State) Verdict {
   544  		var found int
   545  		for _, msg := range state.logs {
   546  			if msg.Type == typ && rec.Match([]byte(msg.Message)) {
   547  				found++
   548  			}
   549  		}
   550  		// Check for an exact or "at least" match.
   551  		if found == count || (found >= count && atLeast) {
   552  			return Met
   553  		}
   554  		// If we require an exact count, and have received more than expected, the
   555  		// expectation can never be met.
   556  		if found > count && !atLeast {
   557  			return Unmeetable
   558  		}
   559  		return Unmet
   560  	}
   561  	desc := fmt.Sprintf("log message matching %q expected %v times", re, count)
   562  	if atLeast {
   563  		desc = fmt.Sprintf("log message matching %q expected at least %v times", re, count)
   564  	}
   565  	return Expectation{
   566  		Check:       check,
   567  		Description: desc,
   568  	}
   569  }
   570  
   571  // NoLogMatching asserts that the client has not received a log message
   572  // of type typ matching the regexp re. If re is an empty string, any log
   573  // message is considered a match.
   574  func NoLogMatching(typ protocol.MessageType, re string) Expectation {
   575  	var r *regexp.Regexp
   576  	if re != "" {
   577  		var err error
   578  		r, err = regexp.Compile(re)
   579  		if err != nil {
   580  			panic(err)
   581  		}
   582  	}
   583  	check := func(state State) Verdict {
   584  		for _, msg := range state.logs {
   585  			if msg.Type != typ {
   586  				continue
   587  			}
   588  			if r == nil || r.Match([]byte(msg.Message)) {
   589  				return Unmeetable
   590  			}
   591  		}
   592  		return Met
   593  	}
   594  	return Expectation{
   595  		Check:       check,
   596  		Description: fmt.Sprintf("no log message matching %q", re),
   597  	}
   598  }
   599  
   600  // FileWatchMatching expects that a file registration matches re.
   601  func FileWatchMatching(re string) Expectation {
   602  	return Expectation{
   603  		Check:       checkFileWatch(re, Met, Unmet),
   604  		Description: fmt.Sprintf("file watch matching %q", re),
   605  	}
   606  }
   607  
   608  // NoFileWatchMatching expects that no file registration matches re.
   609  func NoFileWatchMatching(re string) Expectation {
   610  	return Expectation{
   611  		Check:       checkFileWatch(re, Unmet, Met),
   612  		Description: fmt.Sprintf("no file watch matching %q", re),
   613  	}
   614  }
   615  
   616  func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict {
   617  	rec := regexp.MustCompile(re)
   618  	return func(s State) Verdict {
   619  		r := s.registeredCapabilities["workspace/didChangeWatchedFiles"]
   620  		watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{})
   621  		for _, watcher := range watchers {
   622  			pattern := jsonProperty(watcher, "globPattern").(string)
   623  			if rec.MatchString(pattern) {
   624  				return onMatch
   625  			}
   626  		}
   627  		return onNoMatch
   628  	}
   629  }
   630  
   631  // jsonProperty extracts a value from a path of JSON property names, assuming
   632  // the default encoding/json unmarshaling to the empty interface (i.e.: that
   633  // JSON objects are unmarshalled as map[string]interface{})
   634  //
   635  // For example, if obj is unmarshalled from the following json:
   636  //
   637  //	{
   638  //		"foo": { "bar": 3 }
   639  //	}
   640  //
   641  // Then jsonProperty(obj, "foo", "bar") will be 3.
   642  func jsonProperty(obj interface{}, path ...string) interface{} {
   643  	if len(path) == 0 || obj == nil {
   644  		return obj
   645  	}
   646  	m := obj.(map[string]interface{})
   647  	return jsonProperty(m[path[0]], path[1:]...)
   648  }
   649  
   650  // Diagnostics asserts that there is at least one diagnostic matching the given
   651  // filters.
   652  func Diagnostics(filters ...DiagnosticFilter) Expectation {
   653  	check := func(s State) Verdict {
   654  		diags := flattenDiagnostics(s)
   655  		for _, filter := range filters {
   656  			var filtered []flatDiagnostic
   657  			for _, d := range diags {
   658  				if filter.check(d.name, d.diag) {
   659  					filtered = append(filtered, d)
   660  				}
   661  			}
   662  			if len(filtered) == 0 {
   663  				// TODO(rfindley): if/when expectations describe their own failure, we
   664  				// can provide more useful information here as to which filter caused
   665  				// the failure.
   666  				return Unmet
   667  			}
   668  			diags = filtered
   669  		}
   670  		return Met
   671  	}
   672  	var descs []string
   673  	for _, filter := range filters {
   674  		descs = append(descs, filter.desc)
   675  	}
   676  	return Expectation{
   677  		Check:       check,
   678  		Description: "any diagnostics " + strings.Join(descs, ", "),
   679  	}
   680  }
   681  
   682  // NoDiagnostics asserts that there are no diagnostics matching the given
   683  // filters. Notably, if no filters are supplied this assertion checks that
   684  // there are no diagnostics at all, for any file.
   685  func NoDiagnostics(filters ...DiagnosticFilter) Expectation {
   686  	check := func(s State) Verdict {
   687  		diags := flattenDiagnostics(s)
   688  		for _, filter := range filters {
   689  			var filtered []flatDiagnostic
   690  			for _, d := range diags {
   691  				if filter.check(d.name, d.diag) {
   692  					filtered = append(filtered, d)
   693  				}
   694  			}
   695  			diags = filtered
   696  		}
   697  		if len(diags) > 0 {
   698  			return Unmet
   699  		}
   700  		return Met
   701  	}
   702  	var descs []string
   703  	for _, filter := range filters {
   704  		descs = append(descs, filter.desc)
   705  	}
   706  	return Expectation{
   707  		Check:       check,
   708  		Description: "no diagnostics " + strings.Join(descs, ", "),
   709  	}
   710  }
   711  
   712  type flatDiagnostic struct {
   713  	name string
   714  	diag protocol.Diagnostic
   715  }
   716  
   717  func flattenDiagnostics(state State) []flatDiagnostic {
   718  	var result []flatDiagnostic
   719  	for name, diags := range state.diagnostics {
   720  		for _, diag := range diags.Diagnostics {
   721  			result = append(result, flatDiagnostic{name, diag})
   722  		}
   723  	}
   724  	return result
   725  }
   726  
   727  // -- Diagnostic filters --
   728  
   729  // A DiagnosticFilter filters the set of diagnostics, for assertion with
   730  // Diagnostics or NoDiagnostics.
   731  type DiagnosticFilter struct {
   732  	desc  string
   733  	check func(name string, _ protocol.Diagnostic) bool
   734  }
   735  
   736  // ForFile filters to diagnostics matching the sandbox-relative file name.
   737  func ForFile(name string) DiagnosticFilter {
   738  	return DiagnosticFilter{
   739  		desc: fmt.Sprintf("for file %q", name),
   740  		check: func(diagName string, _ protocol.Diagnostic) bool {
   741  			return diagName == name
   742  		},
   743  	}
   744  }
   745  
   746  // FromSource filters to diagnostics matching the given diagnostics source.
   747  func FromSource(source string) DiagnosticFilter {
   748  	return DiagnosticFilter{
   749  		desc: fmt.Sprintf("with source %q", source),
   750  		check: func(_ string, d protocol.Diagnostic) bool {
   751  			return d.Source == source
   752  		},
   753  	}
   754  }
   755  
   756  // AtRegexp filters to diagnostics in the file with sandbox-relative path name,
   757  // at the first position matching the given regexp pattern.
   758  //
   759  // TODO(rfindley): pass in the editor to expectations, so that they may depend
   760  // on editor state and AtRegexp can be a function rather than a method.
   761  func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter {
   762  	loc := e.RegexpSearch(name, pattern)
   763  	return DiagnosticFilter{
   764  		desc: fmt.Sprintf("at the first position (%v) matching %#q in %q", loc.Range.Start, pattern, name),
   765  		check: func(diagName string, d protocol.Diagnostic) bool {
   766  			return diagName == name && d.Range.Start == loc.Range.Start
   767  		},
   768  	}
   769  }
   770  
   771  // AtPosition filters to diagnostics at location name:line:character, for a
   772  // sandbox-relative path name.
   773  //
   774  // Line and character are 0-based, and character measures UTF-16 codes.
   775  //
   776  // Note: prefer the more readable AtRegexp.
   777  func AtPosition(name string, line, character uint32) DiagnosticFilter {
   778  	pos := protocol.Position{Line: line, Character: character}
   779  	return DiagnosticFilter{
   780  		desc: fmt.Sprintf("at %s:%d:%d", name, line, character),
   781  		check: func(diagName string, d protocol.Diagnostic) bool {
   782  			return diagName == name && d.Range.Start == pos
   783  		},
   784  	}
   785  }
   786  
   787  // WithMessage filters to diagnostics whose message contains the given
   788  // substring.
   789  func WithMessage(substring string) DiagnosticFilter {
   790  	return DiagnosticFilter{
   791  		desc: fmt.Sprintf("with message containing %q", substring),
   792  		check: func(_ string, d protocol.Diagnostic) bool {
   793  			return strings.Contains(d.Message, substring)
   794  		},
   795  	}
   796  }
   797  
   798  // WithSeverityTags filters to diagnostics whose severity and tags match
   799  // the given expectation.
   800  func WithSeverityTags(diagName string, severity protocol.DiagnosticSeverity, tags []protocol.DiagnosticTag) DiagnosticFilter {
   801  	return DiagnosticFilter{
   802  		desc: fmt.Sprintf("with diagnostic %q with severity %q and tag %#q", diagName, severity, tags),
   803  		check: func(_ string, d protocol.Diagnostic) bool {
   804  			return d.Source == diagName && d.Severity == severity && cmp.Equal(d.Tags, tags)
   805  		},
   806  	}
   807  }