Skip to content

Commit b22f1ad

Browse files
adonovangopherbot
authored andcommitted
internal/expect: fork go/expect
Almost no-one outside x/tools uses it, so we'd like to evolve it for our needs, and tag and delete the public package. Updates golang/go#70229 Change-Id: I77c7923881efdf772a1ad53134483ad0078c941d Reviewed-on: https://go-review.googlesource.com/c/tools/+/625918 Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent f1ae722 commit b22f1ad

File tree

6 files changed

+703
-0
lines changed

6 files changed

+703
-0
lines changed

internal/expect/expect.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright 2018 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+
/*
6+
Package expect provides support for interpreting structured comments in Go
7+
source code (including go.mod and go.work files) as test expectations.
8+
9+
This is primarily intended for writing tests of things that process Go source
10+
files, although it does not directly depend on the testing package.
11+
12+
Collect notes with the Extract or Parse functions, and use the
13+
MatchBefore function to find matches within the lines the comments were on.
14+
15+
The interpretation of the notes depends on the application.
16+
For example, the test suite for a static checking tool might
17+
use a @diag note to indicate an expected diagnostic:
18+
19+
fmt.Printf("%s", 1) //@ diag("%s wants a string, got int")
20+
21+
By contrast, the test suite for a source code navigation tool
22+
might use notes to indicate the positions of features of
23+
interest, the actions to be performed by the test,
24+
and their expected outcomes:
25+
26+
var x = 1 //@ x_decl
27+
...
28+
print(x) //@ definition("x", x_decl)
29+
print(x) //@ typeof("x", "int")
30+
31+
# Note comment syntax
32+
33+
Note comments always start with the special marker @, which must be the
34+
very first character after the comment opening pair, so //@ or /*@ with no
35+
spaces.
36+
37+
This is followed by a comma separated list of notes.
38+
39+
A note always starts with an identifier, which is optionally followed by an
40+
argument list. The argument list is surrounded with parentheses and contains a
41+
comma-separated list of arguments.
42+
The empty parameter list and the missing parameter list are distinguishable if
43+
needed; they result in a nil or an empty list in the Args parameter respectively.
44+
45+
Arguments are either identifiers or literals.
46+
The literals supported are the basic value literals, of string, float, integer
47+
true, false or nil. All the literals match the standard go conventions, with
48+
all bases of integers, and both quote and backtick strings.
49+
There is one extra literal type, which is a string literal preceded by the
50+
identifier "re" which is compiled to a regular expression.
51+
*/
52+
package expect
53+
54+
import (
55+
"bytes"
56+
"fmt"
57+
"go/token"
58+
"regexp"
59+
)
60+
61+
// Note is a parsed note from an expect comment.
62+
// It knows the position of the start of the comment, and the name and
63+
// arguments that make up the note.
64+
type Note struct {
65+
Pos token.Pos // The position at which the note identifier appears
66+
Name string // the name associated with the note
67+
Args []interface{} // the arguments for the note
68+
}
69+
70+
// ReadFile is the type of a function that can provide file contents for a
71+
// given filename.
72+
// This is used in MatchBefore to look up the content of the file in order to
73+
// find the line to match the pattern against.
74+
type ReadFile func(filename string) ([]byte, error)
75+
76+
// MatchBefore attempts to match a pattern in the line before the supplied pos.
77+
// It uses the FileSet and the ReadFile to work out the contents of the line
78+
// that end is part of, and then matches the pattern against the content of the
79+
// start of that line up to the supplied position.
80+
// The pattern may be either a simple string, []byte or a *regexp.Regexp.
81+
// MatchBefore returns the range of the line that matched the pattern, and
82+
// invalid positions if there was no match, or an error if the line could not be
83+
// found.
84+
func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern interface{}) (token.Pos, token.Pos, error) {
85+
f := fset.File(end)
86+
content, err := readFile(f.Name())
87+
if err != nil {
88+
return token.NoPos, token.NoPos, fmt.Errorf("invalid file: %v", err)
89+
}
90+
position := f.Position(end)
91+
startOffset := f.Offset(f.LineStart(position.Line))
92+
endOffset := f.Offset(end)
93+
line := content[startOffset:endOffset]
94+
matchStart, matchEnd := -1, -1
95+
switch pattern := pattern.(type) {
96+
case string:
97+
bytePattern := []byte(pattern)
98+
matchStart = bytes.Index(line, bytePattern)
99+
if matchStart >= 0 {
100+
matchEnd = matchStart + len(bytePattern)
101+
}
102+
case []byte:
103+
matchStart = bytes.Index(line, pattern)
104+
if matchStart >= 0 {
105+
matchEnd = matchStart + len(pattern)
106+
}
107+
case *regexp.Regexp:
108+
match := pattern.FindIndex(line)
109+
if len(match) > 0 {
110+
matchStart = match[0]
111+
matchEnd = match[1]
112+
}
113+
}
114+
if matchStart < 0 {
115+
return token.NoPos, token.NoPos, nil
116+
}
117+
return f.Pos(startOffset + matchStart), f.Pos(startOffset + matchEnd), nil
118+
}
119+
120+
func lineEnd(f *token.File, line int) token.Pos {
121+
if line >= f.LineCount() {
122+
return token.Pos(f.Base() + f.Size())
123+
}
124+
return f.LineStart(line + 1)
125+
}

internal/expect/expect_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2018 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 expect_test
6+
7+
import (
8+
"bytes"
9+
"go/token"
10+
"os"
11+
"testing"
12+
13+
"golang.org/x/tools/internal/expect"
14+
)
15+
16+
func TestMarker(t *testing.T) {
17+
for _, tt := range []struct {
18+
filename string
19+
expectNotes int
20+
expectMarkers map[string]string
21+
expectChecks map[string][]interface{}
22+
}{
23+
{
24+
filename: "testdata/test.go",
25+
expectNotes: 13,
26+
expectMarkers: map[string]string{
27+
"αSimpleMarker": "α",
28+
"OffsetMarker": "β",
29+
"RegexMarker": "γ",
30+
"εMultiple": "ε",
31+
"ζMarkers": "ζ",
32+
"ηBlockMarker": "η",
33+
"Declared": "η",
34+
"Comment": "ι",
35+
"LineComment": "someFunc",
36+
"NonIdentifier": "+",
37+
"StringMarker": "\"hello\"",
38+
},
39+
expectChecks: map[string][]interface{}{
40+
"αSimpleMarker": nil,
41+
"StringAndInt": {"Number %d", int64(12)},
42+
"Bool": {true},
43+
},
44+
},
45+
{
46+
filename: "testdata/go.fake.mod",
47+
expectNotes: 2,
48+
expectMarkers: map[string]string{
49+
"αMarker": "αfake1α",
50+
"βMarker": "require golang.org/modfile v0.0.0",
51+
},
52+
},
53+
{
54+
filename: "testdata/go.fake.work",
55+
expectNotes: 2,
56+
expectMarkers: map[string]string{
57+
"αMarker": "1.23.0",
58+
"βMarker": "αβ",
59+
},
60+
},
61+
} {
62+
t.Run(tt.filename, func(t *testing.T) {
63+
content, err := os.ReadFile(tt.filename)
64+
if err != nil {
65+
t.Fatal(err)
66+
}
67+
readFile := func(string) ([]byte, error) { return content, nil }
68+
69+
markers := make(map[string]token.Pos)
70+
for name, tok := range tt.expectMarkers {
71+
offset := bytes.Index(content, []byte(tok))
72+
markers[name] = token.Pos(offset + 1)
73+
end := bytes.Index(content[offset:], []byte(tok))
74+
if end > 0 {
75+
markers[name+"@"] = token.Pos(offset + end + 2)
76+
}
77+
}
78+
79+
fset := token.NewFileSet()
80+
notes, err := expect.Parse(fset, tt.filename, content)
81+
if err != nil {
82+
t.Fatalf("Failed to extract notes: %v", err)
83+
}
84+
if len(notes) != tt.expectNotes {
85+
t.Errorf("Expected %v notes, got %v", tt.expectNotes, len(notes))
86+
}
87+
for _, n := range notes {
88+
switch {
89+
case n.Args == nil:
90+
// A //@foo note associates the name foo with the position of the
91+
// first match of "foo" on the current line.
92+
checkMarker(t, fset, readFile, markers, n.Pos, n.Name, n.Name)
93+
case n.Name == "mark":
94+
// A //@mark(name, "pattern") note associates the specified name
95+
// with the position on the first match of pattern on the current line.
96+
if len(n.Args) != 2 {
97+
t.Errorf("%v: expected 2 args to mark, got %v", fset.Position(n.Pos), len(n.Args))
98+
continue
99+
}
100+
ident, ok := n.Args[0].(expect.Identifier)
101+
if !ok {
102+
t.Errorf("%v: identifier, got %T", fset.Position(n.Pos), n.Args[0])
103+
continue
104+
}
105+
checkMarker(t, fset, readFile, markers, n.Pos, string(ident), n.Args[1])
106+
107+
case n.Name == "check":
108+
// A //@check(args, ...) note specifies some hypothetical action to
109+
// be taken by the test driver and its expected outcome.
110+
// In this test, the action is to compare the arguments
111+
// against expectChecks.
112+
if len(n.Args) < 1 {
113+
t.Errorf("%v: expected 1 args to check, got %v", fset.Position(n.Pos), len(n.Args))
114+
continue
115+
}
116+
ident, ok := n.Args[0].(expect.Identifier)
117+
if !ok {
118+
t.Errorf("%v: identifier, got %T", fset.Position(n.Pos), n.Args[0])
119+
continue
120+
}
121+
args, ok := tt.expectChecks[string(ident)]
122+
if !ok {
123+
t.Errorf("%v: unexpected check %v", fset.Position(n.Pos), ident)
124+
continue
125+
}
126+
if len(n.Args) != len(args)+1 {
127+
t.Errorf("%v: expected %v args to check, got %v", fset.Position(n.Pos), len(args)+1, len(n.Args))
128+
continue
129+
}
130+
for i, got := range n.Args[1:] {
131+
if args[i] != got {
132+
t.Errorf("%v: arg %d expected %v, got %v", fset.Position(n.Pos), i, args[i], got)
133+
}
134+
}
135+
default:
136+
t.Errorf("Unexpected note %v at %v", n.Name, fset.Position(n.Pos))
137+
}
138+
}
139+
})
140+
}
141+
}
142+
143+
func checkMarker(t *testing.T, fset *token.FileSet, readFile expect.ReadFile, markers map[string]token.Pos, pos token.Pos, name string, pattern interface{}) {
144+
start, end, err := expect.MatchBefore(fset, readFile, pos, pattern)
145+
if err != nil {
146+
t.Errorf("%v: MatchBefore failed: %v", fset.Position(pos), err)
147+
return
148+
}
149+
if start == token.NoPos {
150+
t.Errorf("%v: Pattern %v did not match", fset.Position(pos), pattern)
151+
return
152+
}
153+
expectStart, ok := markers[name]
154+
if !ok {
155+
t.Errorf("%v: unexpected marker %v", fset.Position(pos), name)
156+
return
157+
}
158+
if start != expectStart {
159+
t.Errorf("%v: Expected %v got %v", fset.Position(pos), fset.Position(expectStart), fset.Position(start))
160+
}
161+
if expectEnd, ok := markers[name+"@"]; ok && end != expectEnd {
162+
t.Errorf("%v: Expected end %v got %v", fset.Position(pos), fset.Position(expectEnd), fset.Position(end))
163+
}
164+
}

0 commit comments

Comments
 (0)