github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/tests/util.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 tests 6 7 import ( 8 "bytes" 9 "fmt" 10 "go/token" 11 "path/filepath" 12 "sort" 13 "strconv" 14 "strings" 15 "testing" 16 17 "github.com/april1989/origin-go-tools/internal/lsp/diff" 18 "github.com/april1989/origin-go-tools/internal/lsp/diff/myers" 19 "github.com/april1989/origin-go-tools/internal/lsp/protocol" 20 "github.com/april1989/origin-go-tools/internal/lsp/source" 21 "github.com/april1989/origin-go-tools/internal/span" 22 ) 23 24 // DiffLinks takes the links we got and checks if they are located within the source or a Note. 25 // If the link is within a Note, the link is removed. 26 // Returns an diff comment if there are differences and empty string if no diffs. 27 func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string { 28 var notePositions []token.Position 29 links := make(map[span.Span]string, len(wantLinks)) 30 for _, link := range wantLinks { 31 links[link.Src] = link.Target 32 notePositions = append(notePositions, link.NotePosition) 33 } 34 for _, link := range gotLinks { 35 spn, err := mapper.RangeSpan(link.Range) 36 if err != nil { 37 return fmt.Sprintf("%v", err) 38 } 39 linkInNote := false 40 for _, notePosition := range notePositions { 41 // Drop the links found inside expectation notes arguments as this links are not collected by expect package. 42 if notePosition.Line == spn.Start().Line() && 43 notePosition.Column <= spn.Start().Column() { 44 delete(links, spn) 45 linkInNote = true 46 } 47 } 48 if linkInNote { 49 continue 50 } 51 if target, ok := links[spn]; ok { 52 delete(links, spn) 53 if target != link.Target { 54 return fmt.Sprintf("for %v want %v, got %v\n", spn, target, link.Target) 55 } 56 } else { 57 return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target) 58 } 59 } 60 for spn, target := range links { 61 return fmt.Sprintf("missing link %v:%v\n", spn, target) 62 } 63 return "" 64 } 65 66 // DiffSymbols prints the diff between expected and actual symbols test results. 67 func DiffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string { 68 sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name }) 69 sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name }) 70 if len(got) != len(want) { 71 return summarizeSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want)) 72 } 73 for i, w := range want { 74 g := got[i] 75 if w.Name != g.Name { 76 return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name) 77 } 78 if w.Kind != g.Kind { 79 return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind) 80 } 81 if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 { 82 return summarizeSymbols(i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange) 83 } 84 if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" { 85 return fmt.Sprintf("children of %s: %s", w.Name, msg) 86 } 87 } 88 return "" 89 } 90 91 func summarizeSymbols(i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string { 92 msg := &bytes.Buffer{} 93 fmt.Fprint(msg, "document symbols failed") 94 if i >= 0 { 95 fmt.Fprintf(msg, " at %d", i) 96 } 97 fmt.Fprint(msg, " because of ") 98 fmt.Fprintf(msg, reason, args...) 99 fmt.Fprint(msg, ":\nexpected:\n") 100 for _, s := range want { 101 fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange) 102 } 103 fmt.Fprintf(msg, "got:\n") 104 for _, s := range got { 105 fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange) 106 } 107 return msg.String() 108 } 109 110 // FilterWorkspaceSymbols filters to got contained in the given dirs. 111 func FilterWorkspaceSymbols(got []protocol.SymbolInformation, dirs map[string]struct{}) []protocol.SymbolInformation { 112 var result []protocol.SymbolInformation 113 for _, si := range got { 114 if _, ok := dirs[filepath.Dir(si.Location.URI.SpanURI().Filename())]; ok { 115 result = append(result, si) 116 } 117 } 118 return result 119 } 120 121 // DiffWorkspaceSymbols prints the diff between expected and actual workspace 122 // symbols test results. 123 func DiffWorkspaceSymbols(want, got []protocol.SymbolInformation) string { 124 sort.Slice(want, func(i, j int) bool { return fmt.Sprintf("%v", want[i]) < fmt.Sprintf("%v", want[j]) }) 125 sort.Slice(got, func(i, j int) bool { return fmt.Sprintf("%v", got[i]) < fmt.Sprintf("%v", got[j]) }) 126 if len(got) != len(want) { 127 return summarizeWorkspaceSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want)) 128 } 129 for i, w := range want { 130 g := got[i] 131 if w.Name != g.Name { 132 return summarizeWorkspaceSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name) 133 } 134 if w.Kind != g.Kind { 135 return summarizeWorkspaceSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind) 136 } 137 if w.Location.URI != g.Location.URI { 138 return summarizeWorkspaceSymbols(i, want, got, "incorrect uri got %v want %v", g.Location.URI, w.Location.URI) 139 } 140 if protocol.CompareRange(w.Location.Range, g.Location.Range) != 0 { 141 return summarizeWorkspaceSymbols(i, want, got, "incorrect range got %v want %v", g.Location.Range, w.Location.Range) 142 } 143 } 144 return "" 145 } 146 147 func summarizeWorkspaceSymbols(i int, want, got []protocol.SymbolInformation, reason string, args ...interface{}) string { 148 msg := &bytes.Buffer{} 149 fmt.Fprint(msg, "workspace symbols failed") 150 if i >= 0 { 151 fmt.Fprintf(msg, " at %d", i) 152 } 153 fmt.Fprint(msg, " because of ") 154 fmt.Fprintf(msg, reason, args...) 155 fmt.Fprint(msg, ":\nexpected:\n") 156 for _, s := range want { 157 fmt.Fprintf(msg, " %v %v %v:%v\n", s.Name, s.Kind, s.Location.URI, s.Location.Range) 158 } 159 fmt.Fprintf(msg, "got:\n") 160 for _, s := range got { 161 fmt.Fprintf(msg, " %v %v %v:%v\n", s.Name, s.Kind, s.Location.URI, s.Location.Range) 162 } 163 return msg.String() 164 } 165 166 // DiffDiagnostics prints the diff between expected and actual diagnostics test 167 // results. 168 func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string { 169 source.SortDiagnostics(want) 170 source.SortDiagnostics(got) 171 172 if len(got) != len(want) { 173 return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want)) 174 } 175 for i, w := range want { 176 g := got[i] 177 if w.Message != g.Message { 178 return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message) 179 } 180 if w.Severity != g.Severity { 181 return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity) 182 } 183 if w.Source != g.Source { 184 return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source) 185 } 186 // Don't check the range on the badimport test. 187 if strings.Contains(uri.Filename(), "badimport") { 188 continue 189 } 190 if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 { 191 return summarizeDiagnostics(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start) 192 } 193 if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range. 194 if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 { 195 return summarizeDiagnostics(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End) 196 } 197 } 198 } 199 return "" 200 } 201 202 func summarizeDiagnostics(i int, uri span.URI, want, got []*source.Diagnostic, reason string, args ...interface{}) string { 203 msg := &bytes.Buffer{} 204 fmt.Fprint(msg, "diagnostics failed") 205 if i >= 0 { 206 fmt.Fprintf(msg, " at %d", i) 207 } 208 fmt.Fprint(msg, " because of ") 209 fmt.Fprintf(msg, reason, args...) 210 fmt.Fprint(msg, ":\nexpected:\n") 211 for _, d := range want { 212 fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message) 213 } 214 fmt.Fprintf(msg, "got:\n") 215 for _, d := range got { 216 fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message) 217 } 218 return msg.String() 219 } 220 221 func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string { 222 sortCodeLens(want) 223 sortCodeLens(got) 224 225 if len(got) != len(want) { 226 return summarizeCodeLens(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want)) 227 } 228 for i, w := range want { 229 g := got[i] 230 if w.Command.Command != g.Command.Command { 231 return summarizeCodeLens(i, uri, want, got, "incorrect Command Name got %v want %v", g.Command.Command, w.Command.Command) 232 } 233 if w.Command.Title != g.Command.Title { 234 return summarizeCodeLens(i, uri, want, got, "incorrect Command Title got %v want %v", g.Command.Title, w.Command.Title) 235 } 236 if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 { 237 return summarizeCodeLens(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start) 238 } 239 if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the codelens returns a zero-length range. 240 if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 { 241 return summarizeCodeLens(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End) 242 } 243 } 244 } 245 return "" 246 } 247 248 func sortCodeLens(c []protocol.CodeLens) { 249 sort.Slice(c, func(i int, j int) bool { 250 if r := protocol.CompareRange(c[i].Range, c[j].Range); r != 0 { 251 return r < 0 252 } 253 if c[i].Command.Command < c[j].Command.Command { 254 return true 255 } else if c[i].Command.Command == c[j].Command.Command { 256 return c[i].Command.Title < c[j].Command.Title 257 } else { 258 return false 259 } 260 }) 261 } 262 263 func summarizeCodeLens(i int, uri span.URI, want, got []protocol.CodeLens, reason string, args ...interface{}) string { 264 msg := &bytes.Buffer{} 265 fmt.Fprint(msg, "codelens failed") 266 if i >= 0 { 267 fmt.Fprintf(msg, " at %d", i) 268 } 269 fmt.Fprint(msg, " because of ") 270 fmt.Fprintf(msg, reason, args...) 271 fmt.Fprint(msg, ":\nexpected:\n") 272 for _, d := range want { 273 fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title) 274 } 275 fmt.Fprintf(msg, "got:\n") 276 for _, d := range got { 277 fmt.Fprintf(msg, " %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title) 278 } 279 return msg.String() 280 } 281 282 func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) string { 283 decorate := func(f string, args ...interface{}) string { 284 return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...)) 285 } 286 if len(got.Signatures) != 1 { 287 return decorate("wanted 1 signature, got %d", len(got.Signatures)) 288 } 289 if got.ActiveSignature != 0 { 290 return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)) 291 } 292 if want.ActiveParameter != got.ActiveParameter { 293 return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)) 294 } 295 g := got.Signatures[0] 296 w := want.Signatures[0] 297 if w.Label != g.Label { 298 wLabel := w.Label + "\n" 299 d := myers.ComputeEdits("", wLabel, g.Label+"\n") 300 return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d)) 301 } 302 var paramParts []string 303 for _, p := range g.Parameters { 304 paramParts = append(paramParts, p.Label) 305 } 306 paramsStr := strings.Join(paramParts, ", ") 307 if !strings.Contains(g.Label, paramsStr) { 308 return decorate("expected signature %q to contain params %q", g.Label, paramsStr) 309 } 310 return "" 311 } 312 313 // DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies 314 func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string { 315 expected := make(map[protocol.Location]bool) 316 for _, call := range expectedCalls { 317 expected[protocol.Location{URI: call.URI, Range: call.Range}] = true 318 } 319 320 got := make(map[protocol.Location]bool) 321 for _, call := range gotCalls { 322 got[protocol.Location{URI: call.URI, Range: call.Range}] = true 323 } 324 if len(got) != len(expected) { 325 return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got)) 326 } 327 for spn := range got { 328 if !expected[spn] { 329 return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got) 330 } 331 } 332 return "" 333 } 334 335 func ToProtocolCompletionItems(items []source.CompletionItem) []protocol.CompletionItem { 336 var result []protocol.CompletionItem 337 for _, item := range items { 338 result = append(result, ToProtocolCompletionItem(item)) 339 } 340 return result 341 } 342 343 func ToProtocolCompletionItem(item source.CompletionItem) protocol.CompletionItem { 344 pItem := protocol.CompletionItem{ 345 Label: item.Label, 346 Kind: item.Kind, 347 Detail: item.Detail, 348 Documentation: item.Documentation, 349 InsertText: item.InsertText, 350 TextEdit: &protocol.TextEdit{ 351 NewText: item.Snippet(), 352 }, 353 // Negate score so best score has lowest sort text like real API. 354 SortText: fmt.Sprint(-item.Score), 355 } 356 if pItem.InsertText == "" { 357 pItem.InsertText = pItem.Label 358 } 359 return pItem 360 } 361 362 func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem { 363 var ( 364 got []protocol.CompletionItem 365 wantBuiltins = strings.Contains(string(src.URI()), "builtins") 366 wantKeywords = strings.Contains(string(src.URI()), "keywords") 367 ) 368 for _, item := range items { 369 if !wantBuiltins && isBuiltin(item.Label, item.Detail, item.Kind) { 370 continue 371 } 372 373 if !wantKeywords && token.Lookup(item.Label).IsKeyword() { 374 continue 375 } 376 377 got = append(got, item) 378 } 379 return got 380 } 381 382 func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool { 383 if detail == "" && kind == protocol.ClassCompletion { 384 return true 385 } 386 // Remaining builtin constants, variables, interfaces, and functions. 387 trimmed := label 388 if i := strings.Index(trimmed, "("); i >= 0 { 389 trimmed = trimmed[:i] 390 } 391 switch trimmed { 392 case "append", "cap", "close", "complex", "copy", "delete", 393 "error", "false", "imag", "iota", "len", "make", "new", 394 "nil", "panic", "print", "println", "real", "recover", "true": 395 return true 396 } 397 return false 398 } 399 400 func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string { 401 var ( 402 matchedIdxs []int 403 lastGotIdx int 404 lastGotSort float64 405 inOrder = true 406 errorMsg = "completions out of order" 407 ) 408 for _, w := range want { 409 var found bool 410 for i, g := range got { 411 if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind { 412 matchedIdxs = append(matchedIdxs, i) 413 found = true 414 415 if i < lastGotIdx { 416 inOrder = false 417 } 418 lastGotIdx = i 419 420 sort, _ := strconv.ParseFloat(g.SortText, 64) 421 if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort { 422 inOrder = false 423 errorMsg = "candidate scores not strictly decreasing" 424 } 425 lastGotSort = sort 426 427 break 428 } 429 } 430 if !found { 431 return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion") 432 } 433 } 434 435 sort.Ints(matchedIdxs) 436 matched := make([]protocol.CompletionItem, 0, len(matchedIdxs)) 437 for _, idx := range matchedIdxs { 438 matched = append(matched, got[idx]) 439 } 440 441 if !inOrder { 442 return summarizeCompletionItems(-1, want, matched, errorMsg) 443 } 444 445 return "" 446 } 447 448 func DiffSnippets(want string, got *protocol.CompletionItem) string { 449 if want == "" { 450 if got != nil { 451 x := got.TextEdit 452 return fmt.Sprintf("expected no snippet but got %s", x.NewText) 453 } 454 } else { 455 if got == nil { 456 return fmt.Sprintf("couldn't find completion matching %q", want) 457 } 458 x := got.TextEdit 459 if want != x.NewText { 460 return fmt.Sprintf("expected snippet %q, got %q", want, x.NewText) 461 } 462 } 463 return "" 464 } 465 466 func FindItem(list []protocol.CompletionItem, want source.CompletionItem) *protocol.CompletionItem { 467 for _, item := range list { 468 if item.Label == want.Label { 469 return &item 470 } 471 } 472 return nil 473 } 474 475 // DiffCompletionItems prints the diff between expected and actual completion 476 // test results. 477 func DiffCompletionItems(want, got []protocol.CompletionItem) string { 478 if len(got) != len(want) { 479 return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want)) 480 } 481 for i, w := range want { 482 g := got[i] 483 if w.Label != g.Label { 484 return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label) 485 } 486 if w.Detail != g.Detail { 487 return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail) 488 } 489 if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") { 490 if w.Documentation != g.Documentation { 491 return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation) 492 } 493 } 494 if w.Kind != g.Kind { 495 return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind) 496 } 497 } 498 return "" 499 } 500 501 func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string { 502 msg := &bytes.Buffer{} 503 fmt.Fprint(msg, "completion failed") 504 if i >= 0 { 505 fmt.Fprintf(msg, " at %d", i) 506 } 507 fmt.Fprint(msg, " because of ") 508 fmt.Fprintf(msg, reason, args...) 509 fmt.Fprint(msg, ":\nexpected:\n") 510 for _, d := range want { 511 fmt.Fprintf(msg, " %v\n", d) 512 } 513 fmt.Fprintf(msg, "got:\n") 514 for _, d := range got { 515 fmt.Fprintf(msg, " %v\n", d) 516 } 517 return msg.String() 518 } 519 520 func FormatFolderName(folder string) string { 521 if index := strings.Index(folder, "testdata"); index != -1 { 522 return folder[index:] 523 } 524 return folder 525 } 526 527 func EnableAllAnalyzers(view source.View, opts *source.Options) { 528 if opts.UserEnabledAnalyses == nil { 529 opts.UserEnabledAnalyses = make(map[string]bool) 530 } 531 for _, a := range opts.DefaultAnalyzers { 532 if !a.Enabled(view) { 533 opts.UserEnabledAnalyses[a.Analyzer.Name] = true 534 } 535 } 536 for _, a := range opts.TypeErrorAnalyzers { 537 if !a.Enabled(view) { 538 opts.UserEnabledAnalyses[a.Analyzer.Name] = true 539 } 540 } 541 for _, a := range opts.ConvenienceAnalyzers { 542 if !a.Enabled(view) { 543 opts.UserEnabledAnalyses[a.Analyzer.Name] = true 544 } 545 } 546 } 547 548 func Diff(want, got string) string { 549 if want == got { 550 return "" 551 } 552 // Add newlines to avoid newline messages in diff. 553 want += "\n" 554 got += "\n" 555 d := myers.ComputeEdits("", want, got) 556 return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d)) 557 }