@@ -13,6 +13,7 @@ import (
13
13
"fmt"
14
14
"go/token"
15
15
"io/fs"
16
+ "log"
16
17
"os"
17
18
"path"
18
19
"path/filepath"
@@ -45,6 +46,10 @@ var update = flag.Bool("update", false, "if set, update test data during marker
45
46
// RunMarkerTests runs "marker" tests in the given test data directory.
46
47
// (In practice: ../../regtest/marker/testdata)
47
48
//
49
+ // Use this command to run the tests:
50
+ //
51
+ // $ go test ./gopls/internal/regtest/marker [-update]
52
+ //
48
53
// A marker test uses the '//@' marker syntax of the x/tools/go/expect package
49
54
// to annotate source code with various information such as locations and
50
55
// arguments of LSP operations to be executed by the test. The syntax following
@@ -153,6 +158,18 @@ var update = flag.Bool("update", false, "if set, update test data during marker
153
158
// first location and asserts that the result is the set of 'want' locations.
154
159
// The first want location must be the declaration (assumedly unique).
155
160
//
161
+ // - symbol(golden): makes a textDocument/documentSymbol request
162
+ // for the enclosing file, formats the response with one symbol
163
+ // per line, sorts it, and compares against the named golden file.
164
+ // Each line is of the form:
165
+ //
166
+ // dotted.symbol.name kind "detail" +n lines
167
+ //
168
+ // where the "+n lines" part indicates that the declaration spans
169
+ // several lines. The test otherwise makes no attempt to check
170
+ // location information. There is no point to using more than one
171
+ // @symbol marker in a given file.
172
+ //
156
173
// # Argument conversion
157
174
//
158
175
// Marker arguments are first parsed by the go/expect package, which accepts
@@ -242,7 +259,7 @@ var update = flag.Bool("update", false, "if set, update test data during marker
242
259
// - reorganize regtest packages (and rename to just 'test'?)
243
260
// - Rename the files .txtar.
244
261
//
245
- // Existing marker tests to port:
262
+ // Existing marker tests (in ../testdata) to port:
246
263
// - CallHierarchy
247
264
// - CodeLens
248
265
// - Diagnostics
@@ -260,11 +277,9 @@ var update = flag.Bool("update", false, "if set, update test data during marker
260
277
// - SemanticTokens
261
278
// - FunctionExtractions
262
279
// - MethodExtractions
263
- // - Definitions
264
280
// - Highlights
265
281
// - Renames
266
282
// - PrepareRenames
267
- // - Symbols
268
283
// - InlayHints
269
284
// - WorkspaceSymbols
270
285
// - Signatures
@@ -476,6 +491,7 @@ var markerFuncs = map[string]markerFunc{
476
491
"rename" : makeMarkerFunc (renameMarker ),
477
492
"renameerr" : makeMarkerFunc (renameErrMarker ),
478
493
"suggestedfix" : makeMarkerFunc (suggestedfixMarker ),
494
+ "symbol" : makeMarkerFunc (symbolMarker ),
479
495
"refs" : makeMarkerFunc (refsMarker ),
480
496
}
481
497
@@ -598,6 +614,13 @@ func loadMarkerTests(dir string) ([]*markerTest, error) {
598
614
599
615
func loadMarkerTest (name string , content []byte ) (* markerTest , error ) {
600
616
archive := txtar .Parse (content )
617
+ if len (archive .Files ) == 0 {
618
+ return nil , fmt .Errorf ("txtar file has no '-- filename --' sections" )
619
+ }
620
+ if bytes .Contains (archive .Comment , []byte ("\n -- " )) {
621
+ // This check is conservative, but the comment is only a comment.
622
+ return nil , fmt .Errorf ("ill-formed '-- filename --' header in comment" )
623
+ }
601
624
test := & markerTest {
602
625
name : name ,
603
626
fset : token .NewFileSet (),
@@ -650,6 +673,13 @@ func loadMarkerTest(name string, content []byte) (*markerTest, error) {
650
673
test .notes = append (test .notes , notes ... )
651
674
test .files [file .Name ] = file .Data
652
675
}
676
+
677
+ // Print a warning if we see what looks like "-- filename --"
678
+ // without the second "--". It's not necessarily wrong,
679
+ // but it should almost never appear in our test inputs.
680
+ if bytes .Contains (file .Data , []byte ("\n -- " )) {
681
+ log .Printf ("ill-formed '-- filename --' header in %s?" , file .Name )
682
+ }
653
683
}
654
684
655
685
return test , nil
@@ -792,6 +822,11 @@ func (c *marker) sprintf(format string, args ...interface{}) string {
792
822
return fmt .Sprintf (format , args2 ... )
793
823
}
794
824
825
+ // uri returns the URI of the file containing the marker.
826
+ func (mark marker ) uri () protocol.DocumentURI {
827
+ return mark .run .env .Sandbox .Workdir .URI (mark .run .test .fset .File (mark .note .Pos ).Name ())
828
+ }
829
+
795
830
// fmtLoc formats the given pos in the context of the test, using
796
831
// archive-relative paths for files and including the line number in the full
797
832
// archive file.
@@ -1405,6 +1440,75 @@ func implementationMarker(mark marker, src protocol.Location, want ...protocol.L
1405
1440
}
1406
1441
}
1407
1442
1443
+ // symbolMarker implements the @symbol marker.
1444
+ func symbolMarker (mark marker , golden * Golden ) {
1445
+ // Retrieve information about all symbols in this file.
1446
+ symbols , err := mark .run .env .Editor .Server .DocumentSymbol (mark .run .env .Ctx , & protocol.DocumentSymbolParams {
1447
+ TextDocument : protocol.TextDocumentIdentifier {URI : mark .uri ()},
1448
+ })
1449
+ if err != nil {
1450
+ mark .errorf ("DocumentSymbol request failed: %v" , err )
1451
+ return
1452
+ }
1453
+
1454
+ // Format symbols one per line, sorted (in effect) by first column, a dotted name.
1455
+ var lines []string
1456
+ for _ , symbol := range symbols {
1457
+ // Each result element is a union of (legacy)
1458
+ // SymbolInformation and (new) DocumentSymbol,
1459
+ // so we ascertain which one and then transcode.
1460
+ data , err := json .Marshal (symbol )
1461
+ if err != nil {
1462
+ mark .run .env .T .Fatal (err )
1463
+ }
1464
+ if _ , ok := symbol .(map [string ]interface {})["location" ]; ok {
1465
+ // This case is not reached because Editor initialization
1466
+ // enables HierarchicalDocumentSymbolSupport.
1467
+ // TODO(adonovan): test this too.
1468
+ var sym protocol.SymbolInformation
1469
+ if err := json .Unmarshal (data , & sym ); err != nil {
1470
+ mark .run .env .T .Fatal (err )
1471
+ }
1472
+ mark .errorf ("fake Editor doesn't support SymbolInformation" )
1473
+
1474
+ } else {
1475
+ var sym protocol.DocumentSymbol // new hierarchical hotness
1476
+ if err := json .Unmarshal (data , & sym ); err != nil {
1477
+ mark .run .env .T .Fatal (err )
1478
+ }
1479
+
1480
+ // Print each symbol in the response tree.
1481
+ var visit func (sym protocol.DocumentSymbol , prefix []string )
1482
+ visit = func (sym protocol.DocumentSymbol , prefix []string ) {
1483
+ var out strings.Builder
1484
+ out .WriteString (strings .Join (prefix , "." ))
1485
+ fmt .Fprintf (& out , " %q" , sym .Detail )
1486
+ if delta := sym .Range .End .Line - sym .Range .Start .Line ; delta > 0 {
1487
+ fmt .Fprintf (& out , " +%d lines" , delta )
1488
+ }
1489
+ lines = append (lines , out .String ())
1490
+
1491
+ for _ , child := range sym .Children {
1492
+ visit (child , append (prefix , child .Name ))
1493
+ }
1494
+ }
1495
+ visit (sym , []string {sym .Name })
1496
+ }
1497
+ }
1498
+ sort .Strings (lines )
1499
+ lines = append (lines , "" ) // match trailing newline in .txtar file
1500
+ got := []byte (strings .Join (lines , "\n " ))
1501
+
1502
+ // Compare with golden.
1503
+ want , ok := golden .Get (mark .run .env .T , "" , got )
1504
+ if ! ok {
1505
+ mark .errorf ("%s: missing golden file @%s" , mark .note .Name , golden .id )
1506
+ } else if diff := cmp .Diff (string (got ), string (want )); diff != "" {
1507
+ mark .errorf ("%s: unexpected output: got:\n %s\n want:\n %s\n diff:\n %s" ,
1508
+ mark .note .Name , got , want , diff )
1509
+ }
1510
+ }
1511
+
1408
1512
// compareLocations returns an error message if got and want are not
1409
1513
// the same set of locations. The marker is used only for fmtLoc.
1410
1514
func compareLocations (mark marker , got , want []protocol.Location ) error {
0 commit comments