-
Notifications
You must be signed in to change notification settings - Fork 12
/
examples_test.go
316 lines (277 loc) · 7.79 KB
/
examples_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// These end-to-end tests run Goose over complete packages and test the Coq
// output.
//
// The examples are packages in internal/examples/.
// Tests in package pkg have a Ast pkg.gold.v with the expected Coq output.
// The Ast is generated by freezing the output of goose and then some manual
// auditing. They serve especially well as regression tests when making
// changes that are expected to have no impact on existing working code,
// and also conveniently are continuously-checked examples of goose output.
//
// There are also negative examples in testdata/ that goose rejects due to
// unsupported Go code. These are each run as a standalone package.
package goose_test
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path"
"regexp"
"strings"
"testing"
"github.com/goose-lang/goose"
"github.com/stretchr/testify/assert"
)
var updateGold = flag.Bool("update-gold",
false,
"update *.gold.v files in internal/examples/ with current output")
type test struct {
name string
path string
}
func loadTests(dir string) []test {
f, err := os.Open(dir)
if err != nil {
panic(err)
}
defer f.Close()
names, err := f.Readdirnames(0)
if err != nil {
panic(err)
}
var tests []test
for _, n := range names {
tests = append(tests, test{name: n, path: path.Join(dir, n)})
}
return tests
}
func (t test) isDir() bool {
info, _ := os.Stat(t.path)
return info.IsDir()
}
// A positiveTest is a test organized as a directory with expected Coq output
//
// Each test is a single Go package in dir that has a Ast <dir>.gold.v
// with the expected Coq output.
type positiveTest struct {
test
}
// goldFile returns the path to the test's gold Coq output
func (t positiveTest) goldFile() string {
return path.Join(t.path, t.name+".gold.v")
}
// actualFile returns the path to the test's actual output
func (t positiveTest) actualFile() string {
return path.Join(t.path, t.name+".actual.v")
}
// gold returns the contents of the gold Ast as a string
func (t positiveTest) gold() string {
expected, err := os.ReadFile(t.goldFile())
if err != nil {
return ""
}
return string(expected)
}
// updateGold updates the gold output with real results
func (t positiveTest) updateGold(actual string) {
err := os.WriteFile(t.goldFile(), []byte(actual), 0644)
if err != nil {
panic(err)
}
}
// putActual updates the actual test output with the real results
func (t positiveTest) putActual(actual string) {
err := os.WriteFile(t.actualFile(), []byte(actual), 0644)
if err != nil {
panic(err)
}
}
// deleteActual deletes the actual test output, if it exists
func (t positiveTest) deleteActual() {
_ = os.Remove(t.actualFile())
}
func testGold(testingT *testing.T, dir string, tr goose.TranslationConfig) {
testingT.Parallel()
testingT.Helper()
assert := assert.New(testingT)
t := positiveTest{test{name: path.Base(dir), path: dir}}
if !t.isDir() {
assert.FailNowf("not a test directory",
"path: %s",
t.path)
}
// c.Logf("testing example %s/", t.Path)
files, errs, patternError := tr.TranslatePackages(t.path, ".")
if patternError != nil {
// shouldn't be possible since "." is valid
assert.FailNowf("loading failed", "load error: %v", patternError)
}
if !(len(files) == 1 && len(errs) == 1) {
assert.FailNowf("pattern matched unexpected number of packages",
"files: %v", files)
}
f, terr := files[0], errs[0]
if terr != nil {
fmt.Fprintln(os.Stderr, terr)
assert.FailNow("translation failed")
}
var b bytes.Buffer
f.Write(&b)
actual := b.String()
if *updateGold {
expected := t.gold()
if actual != expected {
fmt.Fprintf(os.Stderr, "updated %s\n", t.goldFile())
}
t.updateGold(actual)
t.deleteActual()
return
}
expected := t.gold()
if expected == "" {
assert.FailNowf("could not load gold output",
"gold file: %s",
t.goldFile())
}
if actual != expected {
t.putActual(actual)
assert.FailNowf("actual Coq output != gold output",
"see %s",
t.actualFile())
return
}
// when tests pass, clean up actual output
t.deleteActual()
}
func TestUnitTests(t *testing.T) {
testGold(t, "internal/examples/unittest", goose.TranslationConfig{})
}
func TestUnitTestGeneric(t *testing.T) {
testGold(t, "internal/examples/unittest/generic", goose.TranslationConfig{})
}
func TestSimpleDb(t *testing.T) {
testGold(t, "internal/examples/simpledb", goose.TranslationConfig{})
}
func TestWal(t *testing.T) {
testGold(t, "internal/examples/wal", goose.TranslationConfig{})
}
func TestAsync(t *testing.T) {
testGold(t, "internal/examples/async", goose.TranslationConfig{TypeCheck: false})
}
func TestLogging2(t *testing.T) {
testGold(t, "internal/examples/logging2", goose.TranslationConfig{})
}
func TestAppendLog(t *testing.T) {
testGold(t, "internal/examples/append_log", goose.TranslationConfig{})
}
func TestRfc1813(t *testing.T) {
testGold(t, "internal/examples/rfc1813", goose.TranslationConfig{})
}
func TestSemantics(t *testing.T) {
testGold(t, "internal/examples/semantics", goose.TranslationConfig{})
}
func TestComments(t *testing.T) {
testGold(t, "internal/examples/comments", goose.TranslationConfig{})
}
func TestTrustedImport(t *testing.T) {
testGold(t, "internal/examples/trust_import", goose.TranslationConfig{})
}
func TestBadGoose(t *testing.T) {
testGold(t, "testdata/badgoose", goose.TranslationConfig{})
}
type errorExpectation struct {
Line int
Error string
}
type errorTestResult struct {
Err *goose.ConversionError
ActualLine int
Expected errorExpectation
}
func getExpectedError(fset *token.FileSet,
comments []*ast.CommentGroup) *errorExpectation {
errRegex := regexp.MustCompile(`ERROR ?(.*)`)
for _, cg := range comments {
for _, c := range cg.List {
ms := errRegex.FindStringSubmatch(c.Text)
if ms == nil {
continue
}
expected := &errorExpectation{
Line: fset.Position(c.Pos()).Line,
}
// found a match
if len(ms) > 1 {
expected.Error = ms[1]
}
// only use the first ERROR
return expected
}
}
return nil
}
func translateErrorFile(assert *assert.Assertions, filePath string) *errorTestResult {
pkgName := "example"
ctx := goose.NewCtx(pkgName, goose.PkgConfig{})
fset := ctx.Fset
f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments|parser.SkipObjectResolution)
if err != nil {
fmt.Fprintln(os.Stderr, err)
assert.FailNowf("test code does not parse", "file: %s", filePath)
return nil
}
err = ctx.TypeCheck([]*ast.File{f})
if err != nil {
fmt.Fprintln(os.Stderr, err)
assert.FailNowf("test code does not type check", "file: %s", filePath)
return nil
}
_, _, errs := ctx.Decls(goose.NamedFile{Path: filePath, Ast: f})
if len(errs) == 0 {
assert.FailNowf("expected error", "file: %s", filePath)
return nil
}
cerr := errs[0].(*goose.ConversionError)
expectedErr := getExpectedError(fset, f.Comments)
if expectedErr == nil {
assert.FailNowf("test code does not have an error expectation",
"file: %s", filePath)
return nil
}
return &errorTestResult{
Err: cerr,
ActualLine: fset.Position(cerr.Pos).Line,
Expected: *expectedErr,
}
}
func TestNegativeExamples(testingT *testing.T) {
tests := loadTests("./testdata/negative-tests")
for _, t := range tests {
if t.isDir() {
continue
}
testingT.Run(t.name, func(testingT *testing.T) {
assert := assert.New(testingT)
tt := translateErrorFile(assert, t.path)
if tt == nil {
// this Ast has already failed
return
}
assert.Regexp(`(unsupported|future)`, tt.Err.Category)
if !strings.Contains(tt.Err.Message, tt.Expected.Error) {
assert.FailNowf("incorrect error message",
`%s: error message "%s" does not contain "%s"`,
t.name, tt.Err.Message, tt.Expected.Error)
}
if tt.ActualLine > 0 && tt.ActualLine != tt.Expected.Line {
assert.FailNowf("incorrect error message line",
"%s: error is incorrectly attributed to %s",
t.name, tt.Err.GoSrcFile)
}
})
}
}