github.com/v2fly/tools@v0.100.0/internal/lsp/regtest/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 regtest
     6  
     7  import (
     8  	"fmt"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/v2fly/tools/internal/lsp"
    13  	"github.com/v2fly/tools/internal/lsp/fake"
    14  	"github.com/v2fly/tools/internal/lsp/protocol"
    15  	"github.com/v2fly/tools/internal/testenv"
    16  )
    17  
    18  // An Expectation asserts that the state of the editor at a point in time
    19  // matches an expected condition. This is used for signaling in tests when
    20  // certain conditions in the editor are met.
    21  type Expectation interface {
    22  	// Check determines whether the state of the editor satisfies the
    23  	// expectation, returning the results that met the condition.
    24  	Check(State) Verdict
    25  	// Description is a human-readable description of the expectation.
    26  	Description() string
    27  }
    28  
    29  var (
    30  	// InitialWorkspaceLoad is an expectation that the workspace initial load has
    31  	// completed. It is verified via workdone reporting.
    32  	InitialWorkspaceLoad = CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1)
    33  )
    34  
    35  // A Verdict is the result of checking an expectation against the current
    36  // editor state.
    37  type Verdict int
    38  
    39  // Order matters for the following constants: verdicts are sorted in order of
    40  // decisiveness.
    41  const (
    42  	// Met indicates that an expectation is satisfied by the current state.
    43  	Met Verdict = iota
    44  	// Unmet indicates that an expectation is not currently met, but could be met
    45  	// in the future.
    46  	Unmet
    47  	// Unmeetable indicates that an expectation cannot be satisfied in the
    48  	// future.
    49  	Unmeetable
    50  )
    51  
    52  func (v Verdict) String() string {
    53  	switch v {
    54  	case Met:
    55  		return "Met"
    56  	case Unmet:
    57  		return "Unmet"
    58  	case Unmeetable:
    59  		return "Unmeetable"
    60  	}
    61  	return fmt.Sprintf("unrecognized verdict %d", v)
    62  }
    63  
    64  // SimpleExpectation holds an arbitrary check func, and implements the Expectation interface.
    65  type SimpleExpectation struct {
    66  	check       func(State) Verdict
    67  	description string
    68  }
    69  
    70  // Check invokes e.check.
    71  func (e SimpleExpectation) Check(s State) Verdict {
    72  	return e.check(s)
    73  }
    74  
    75  // Description returns e.descriptin.
    76  func (e SimpleExpectation) Description() string {
    77  	return e.description
    78  }
    79  
    80  // OnceMet returns an Expectation that, once the precondition is met, asserts
    81  // that mustMeet is met.
    82  func OnceMet(precondition Expectation, mustMeet Expectation) *SimpleExpectation {
    83  	check := func(s State) Verdict {
    84  		switch pre := precondition.Check(s); pre {
    85  		case Unmeetable:
    86  			return Unmeetable
    87  		case Met:
    88  			verdict := mustMeet.Check(s)
    89  			if verdict != Met {
    90  				return Unmeetable
    91  			}
    92  			return Met
    93  		default:
    94  			return Unmet
    95  		}
    96  	}
    97  	return &SimpleExpectation{
    98  		check:       check,
    99  		description: fmt.Sprintf("once %q is met, must have %q", precondition.Description(), mustMeet.Description()),
   100  	}
   101  }
   102  
   103  // ReadDiagnostics is an 'expectation' that is used to read diagnostics
   104  // atomically. It is intended to be used with 'OnceMet'.
   105  func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation {
   106  	check := func(s State) Verdict {
   107  		diags, ok := s.diagnostics[fileName]
   108  		if !ok {
   109  			return Unmeetable
   110  		}
   111  		*into = *diags
   112  		return Met
   113  	}
   114  	return &SimpleExpectation{
   115  		check:       check,
   116  		description: fmt.Sprintf("read diagnostics for %q", fileName),
   117  	}
   118  }
   119  
   120  // NoOutstandingWork asserts that there is no work initiated using the LSP
   121  // $/progress API that has not completed.
   122  func NoOutstandingWork() SimpleExpectation {
   123  	check := func(s State) Verdict {
   124  		if len(s.outstandingWork) == 0 {
   125  			return Met
   126  		}
   127  		return Unmet
   128  	}
   129  	return SimpleExpectation{
   130  		check:       check,
   131  		description: "no outstanding work",
   132  	}
   133  }
   134  
   135  // NoShowMessage asserts that the editor has not received a ShowMessage.
   136  func NoShowMessage() SimpleExpectation {
   137  	check := func(s State) Verdict {
   138  		if len(s.showMessage) == 0 {
   139  			return Met
   140  		}
   141  		return Unmeetable
   142  	}
   143  	return SimpleExpectation{
   144  		check:       check,
   145  		description: "no ShowMessage received",
   146  	}
   147  }
   148  
   149  // ShownMessage asserts that the editor has received a ShownMessage with the
   150  // given title.
   151  func ShownMessage(title string) SimpleExpectation {
   152  	check := func(s State) Verdict {
   153  		for _, m := range s.showMessage {
   154  			if strings.Contains(m.Message, title) {
   155  				return Met
   156  			}
   157  		}
   158  		return Unmet
   159  	}
   160  	return SimpleExpectation{
   161  		check:       check,
   162  		description: "received ShowMessage",
   163  	}
   164  }
   165  
   166  // ShowMessageRequest asserts that the editor has received a ShowMessageRequest
   167  // with an action item that has the given title.
   168  func ShowMessageRequest(title string) SimpleExpectation {
   169  	check := func(s State) Verdict {
   170  		if len(s.showMessageRequest) == 0 {
   171  			return Unmet
   172  		}
   173  		// Only check the most recent one.
   174  		m := s.showMessageRequest[len(s.showMessageRequest)-1]
   175  		if len(m.Actions) == 0 || len(m.Actions) > 1 {
   176  			return Unmet
   177  		}
   178  		if m.Actions[0].Title == title {
   179  			return Met
   180  		}
   181  		return Unmet
   182  	}
   183  	return SimpleExpectation{
   184  		check:       check,
   185  		description: "received ShowMessageRequest",
   186  	}
   187  }
   188  
   189  // DoneWithOpen expects all didOpen notifications currently sent by the editor
   190  // to be completely processed.
   191  func (e *Env) DoneWithOpen() Expectation {
   192  	opens := e.Editor.Stats().DidOpen
   193  	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens)
   194  }
   195  
   196  // DoneWithChange expects all didChange notifications currently sent by the
   197  // editor to be completely processed.
   198  func (e *Env) DoneWithChange() Expectation {
   199  	changes := e.Editor.Stats().DidChange
   200  	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes)
   201  }
   202  
   203  // DoneWithSave expects all didSave notifications currently sent by the editor
   204  // to be completely processed.
   205  func (e *Env) DoneWithSave() Expectation {
   206  	saves := e.Editor.Stats().DidSave
   207  	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves)
   208  }
   209  
   210  // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications
   211  // currently sent by the editor to be completely processed.
   212  func (e *Env) DoneWithChangeWatchedFiles() Expectation {
   213  	changes := e.Editor.Stats().DidChangeWatchedFiles
   214  	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes)
   215  }
   216  
   217  // DoneWithClose expects all didClose notifications currently sent by the
   218  // editor to be completely processed.
   219  func (e *Env) DoneWithClose() Expectation {
   220  	changes := e.Editor.Stats().DidClose
   221  	return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes)
   222  }
   223  
   224  // CompletedWork expects a work item to have been completed >= atLeast times.
   225  //
   226  // Since the Progress API doesn't include any hidden metadata, we must use the
   227  // progress notification title to identify the work we expect to be completed.
   228  func CompletedWork(title string, atLeast uint64) SimpleExpectation {
   229  	check := func(s State) Verdict {
   230  		if s.completedWork[title] >= atLeast {
   231  			return Met
   232  		}
   233  		return Unmet
   234  	}
   235  	return SimpleExpectation{
   236  		check:       check,
   237  		description: fmt.Sprintf("completed work %q at least %d time(s)", title, atLeast),
   238  	}
   239  }
   240  
   241  // OutstandingWork expects a work item to be outstanding. The given title must
   242  // be an exact match, whereas the given msg must only be contained in the work
   243  // item's message.
   244  func OutstandingWork(title, msg string) SimpleExpectation {
   245  	check := func(s State) Verdict {
   246  		for _, work := range s.outstandingWork {
   247  			if work.title == title && strings.Contains(work.msg, msg) {
   248  				return Met
   249  			}
   250  		}
   251  		return Unmet
   252  	}
   253  	return SimpleExpectation{
   254  		check:       check,
   255  		description: fmt.Sprintf("outstanding work: %s", title),
   256  	}
   257  }
   258  
   259  // LogExpectation is an expectation on the log messages received by the editor
   260  // from gopls.
   261  type LogExpectation struct {
   262  	check       func([]*protocol.LogMessageParams) Verdict
   263  	description string
   264  }
   265  
   266  // Check implements the Expectation interface.
   267  func (e LogExpectation) Check(s State) Verdict {
   268  	return e.check(s.logs)
   269  }
   270  
   271  // Description implements the Expectation interface.
   272  func (e LogExpectation) Description() string {
   273  	return e.description
   274  }
   275  
   276  // NoErrorLogs asserts that the client has not received any log messages of
   277  // error severity.
   278  func NoErrorLogs() LogExpectation {
   279  	return NoLogMatching(protocol.Error, "")
   280  }
   281  
   282  // LogMatching asserts that the client has received a log message
   283  // of type typ matching the regexp re.
   284  func LogMatching(typ protocol.MessageType, re string, count int) LogExpectation {
   285  	rec, err := regexp.Compile(re)
   286  	if err != nil {
   287  		panic(err)
   288  	}
   289  	check := func(msgs []*protocol.LogMessageParams) Verdict {
   290  		var found int
   291  		for _, msg := range msgs {
   292  			if msg.Type == typ && rec.Match([]byte(msg.Message)) {
   293  				found++
   294  			}
   295  		}
   296  		if found == count {
   297  			return Met
   298  		}
   299  		return Unmet
   300  	}
   301  	return LogExpectation{
   302  		check:       check,
   303  		description: fmt.Sprintf("log message matching %q", re),
   304  	}
   305  }
   306  
   307  // NoLogMatching asserts that the client has not received a log message
   308  // of type typ matching the regexp re. If re is an empty string, any log
   309  // message is considered a match.
   310  func NoLogMatching(typ protocol.MessageType, re string) LogExpectation {
   311  	var r *regexp.Regexp
   312  	if re != "" {
   313  		var err error
   314  		r, err = regexp.Compile(re)
   315  		if err != nil {
   316  			panic(err)
   317  		}
   318  	}
   319  	check := func(msgs []*protocol.LogMessageParams) Verdict {
   320  		for _, msg := range msgs {
   321  			if msg.Type != typ {
   322  				continue
   323  			}
   324  			if r == nil || r.Match([]byte(msg.Message)) {
   325  				return Unmeetable
   326  			}
   327  		}
   328  		return Met
   329  	}
   330  	return LogExpectation{
   331  		check:       check,
   332  		description: fmt.Sprintf("no log message matching %q", re),
   333  	}
   334  }
   335  
   336  // RegistrationExpectation is an expectation on the capability registrations
   337  // received by the editor from gopls.
   338  type RegistrationExpectation struct {
   339  	check       func([]*protocol.RegistrationParams) Verdict
   340  	description string
   341  }
   342  
   343  // Check implements the Expectation interface.
   344  func (e RegistrationExpectation) Check(s State) Verdict {
   345  	return e.check(s.registrations)
   346  }
   347  
   348  // Description implements the Expectation interface.
   349  func (e RegistrationExpectation) Description() string {
   350  	return e.description
   351  }
   352  
   353  // RegistrationMatching asserts that the client has received a capability
   354  // registration matching the given regexp.
   355  func RegistrationMatching(re string) RegistrationExpectation {
   356  	rec, err := regexp.Compile(re)
   357  	if err != nil {
   358  		panic(err)
   359  	}
   360  	check := func(params []*protocol.RegistrationParams) Verdict {
   361  		for _, p := range params {
   362  			for _, r := range p.Registrations {
   363  				if rec.Match([]byte(r.Method)) {
   364  					return Met
   365  				}
   366  			}
   367  		}
   368  		return Unmet
   369  	}
   370  	return RegistrationExpectation{
   371  		check:       check,
   372  		description: fmt.Sprintf("registration matching %q", re),
   373  	}
   374  }
   375  
   376  // UnregistrationExpectation is an expectation on the capability
   377  // unregistrations received by the editor from gopls.
   378  type UnregistrationExpectation struct {
   379  	check       func([]*protocol.UnregistrationParams) Verdict
   380  	description string
   381  }
   382  
   383  // Check implements the Expectation interface.
   384  func (e UnregistrationExpectation) Check(s State) Verdict {
   385  	return e.check(s.unregistrations)
   386  }
   387  
   388  // Description implements the Expectation interface.
   389  func (e UnregistrationExpectation) Description() string {
   390  	return e.description
   391  }
   392  
   393  // UnregistrationMatching asserts that the client has received an
   394  // unregistration whose ID matches the given regexp.
   395  func UnregistrationMatching(re string) UnregistrationExpectation {
   396  	rec, err := regexp.Compile(re)
   397  	if err != nil {
   398  		panic(err)
   399  	}
   400  	check := func(params []*protocol.UnregistrationParams) Verdict {
   401  		for _, p := range params {
   402  			for _, r := range p.Unregisterations {
   403  				if rec.Match([]byte(r.Method)) {
   404  					return Met
   405  				}
   406  			}
   407  		}
   408  		return Unmet
   409  	}
   410  	return UnregistrationExpectation{
   411  		check:       check,
   412  		description: fmt.Sprintf("unregistration matching %q", re),
   413  	}
   414  }
   415  
   416  // A DiagnosticExpectation is a condition that must be met by the current set
   417  // of diagnostics for a file.
   418  type DiagnosticExpectation struct {
   419  	// optionally, the position of the diagnostic and the regex used to calculate it.
   420  	pos *fake.Pos
   421  	re  string
   422  
   423  	// optionally, the message that the diagnostic should contain.
   424  	message string
   425  
   426  	// whether the expectation is that the diagnostic is present, or absent.
   427  	present bool
   428  
   429  	// path is the scratch workdir-relative path to the file being asserted on.
   430  	path string
   431  }
   432  
   433  // Check implements the Expectation interface.
   434  func (e DiagnosticExpectation) Check(s State) Verdict {
   435  	diags, ok := s.diagnostics[e.path]
   436  	if !ok {
   437  		if !e.present {
   438  			return Met
   439  		}
   440  		return Unmet
   441  	}
   442  
   443  	found := false
   444  	for _, d := range diags.Diagnostics {
   445  		if e.pos != nil {
   446  			if d.Range.Start.Line != uint32(e.pos.Line) || d.Range.Start.Character != uint32(e.pos.Column) {
   447  				continue
   448  			}
   449  		}
   450  		if e.message != "" {
   451  			if !strings.Contains(d.Message, e.message) {
   452  				continue
   453  			}
   454  		}
   455  		found = true
   456  		break
   457  	}
   458  
   459  	if found == e.present {
   460  		return Met
   461  	}
   462  	return Unmet
   463  }
   464  
   465  // Description implements the Expectation interface.
   466  func (e DiagnosticExpectation) Description() string {
   467  	desc := e.path + ":"
   468  	if !e.present {
   469  		desc += " no"
   470  	}
   471  	desc += " diagnostic"
   472  	if e.pos != nil {
   473  		desc += fmt.Sprintf(" at {line:%d, column:%d}", e.pos.Line, e.pos.Column)
   474  		if e.re != "" {
   475  			desc += fmt.Sprintf(" (location of %q)", e.re)
   476  		}
   477  	}
   478  	if e.message != "" {
   479  		desc += fmt.Sprintf(" with message %q", e.message)
   480  	}
   481  	return desc
   482  }
   483  
   484  // EmptyDiagnostics asserts that empty diagnostics are sent for the
   485  // workspace-relative path name.
   486  func EmptyDiagnostics(name string) Expectation {
   487  	check := func(s State) Verdict {
   488  		if diags := s.diagnostics[name]; diags != nil && len(diags.Diagnostics) == 0 {
   489  			return Met
   490  		}
   491  		return Unmet
   492  	}
   493  	return SimpleExpectation{
   494  		check:       check,
   495  		description: "empty diagnostics",
   496  	}
   497  }
   498  
   499  // NoDiagnostics asserts that no diagnostics are sent for the
   500  // workspace-relative path name. It should be used primarily in conjunction
   501  // with a OnceMet, as it has to check that all outstanding diagnostics have
   502  // already been delivered.
   503  func NoDiagnostics(name string) Expectation {
   504  	check := func(s State) Verdict {
   505  		if _, ok := s.diagnostics[name]; !ok {
   506  			return Met
   507  		}
   508  		return Unmet
   509  	}
   510  	return SimpleExpectation{
   511  		check:       check,
   512  		description: "no diagnostics",
   513  	}
   514  }
   515  
   516  // AnyDiagnosticAtCurrentVersion asserts that there is a diagnostic report for
   517  // the current edited version of the buffer corresponding to the given
   518  // workdir-relative pathname.
   519  func (e *Env) AnyDiagnosticAtCurrentVersion(name string) Expectation {
   520  	version := e.Editor.BufferVersion(name)
   521  	check := func(s State) Verdict {
   522  		diags, ok := s.diagnostics[name]
   523  		if ok && diags.Version == int32(version) {
   524  			return Met
   525  		}
   526  		return Unmet
   527  	}
   528  	return SimpleExpectation{
   529  		check:       check,
   530  		description: fmt.Sprintf("any diagnostics at version %d", version),
   531  	}
   532  }
   533  
   534  // DiagnosticAtRegexp expects that there is a diagnostic entry at the start
   535  // position matching the regexp search string re in the buffer specified by
   536  // name. Note that this currently ignores the end position.
   537  func (e *Env) DiagnosticAtRegexp(name, re string) DiagnosticExpectation {
   538  	e.T.Helper()
   539  	pos := e.RegexpSearch(name, re)
   540  	return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true}
   541  }
   542  
   543  // DiagnosticAtRegexpWithMessage is like DiagnosticAtRegexp, but it also
   544  // checks for the content of the diagnostic message,
   545  func (e *Env) DiagnosticAtRegexpWithMessage(name, re, msg string) DiagnosticExpectation {
   546  	e.T.Helper()
   547  	pos := e.RegexpSearch(name, re)
   548  	return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, message: msg}
   549  }
   550  
   551  // DiagnosticAt asserts that there is a diagnostic entry at the position
   552  // specified by line and col, for the workdir-relative path name.
   553  func DiagnosticAt(name string, line, col int) DiagnosticExpectation {
   554  	return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: true}
   555  }
   556  
   557  // NoDiagnosticAtRegexp expects that there is no diagnostic entry at the start
   558  // position matching the regexp search string re in the buffer specified by
   559  // name. Note that this currently ignores the end position.
   560  // This should only be used in combination with OnceMet for a given condition,
   561  // otherwise it may always succeed.
   562  func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation {
   563  	e.T.Helper()
   564  	pos := e.RegexpSearch(name, re)
   565  	return DiagnosticExpectation{path: name, pos: &pos, re: re, present: false}
   566  }
   567  
   568  // NoDiagnosticAt asserts that there is no diagnostic entry at the position
   569  // specified by line and col, for the workdir-relative path name.
   570  // This should only be used in combination with OnceMet for a given condition,
   571  // otherwise it may always succeed.
   572  func NoDiagnosticAt(name string, line, col int) DiagnosticExpectation {
   573  	return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: false}
   574  }
   575  
   576  // NoDiagnosticWithMessage asserts that there is no diagnostic entry with the
   577  // given message.
   578  //
   579  // This should only be used in combination with OnceMet for a given condition,
   580  // otherwise it may always succeed.
   581  func NoDiagnosticWithMessage(name, msg string) DiagnosticExpectation {
   582  	return DiagnosticExpectation{path: name, message: msg, present: false}
   583  }
   584  
   585  // GoSumDiagnostic asserts that a "go.sum is out of sync" diagnostic for the
   586  // given module (as formatted in a go.mod file, e.g. "example.com v1.0.0") is
   587  // present.
   588  func (e *Env) GoSumDiagnostic(name, module string) Expectation {
   589  	e.T.Helper()
   590  	// In 1.16, go.sum diagnostics should appear on the relevant module. Earlier
   591  	// errors have no information and appear on the module declaration.
   592  	if testenv.Go1Point() >= 16 {
   593  		return e.DiagnosticAtRegexpWithMessage(name, module, "go.sum is out of sync")
   594  	} else {
   595  		return e.DiagnosticAtRegexpWithMessage(name, `module`, "go.sum is out of sync")
   596  	}
   597  }