blob: ec2d126def5dbcf4cb4d5f87c62a8d708d23507e [file] [log] [blame]
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +02001// Copyright 2020 CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package cuetxtar
16
17import (
18 "bufio"
19 "bytes"
Marcel van Lohuizen50dac242020-07-19 18:33:03 +020020 "fmt"
Marcel van Lohuizendbfa73b2020-11-12 09:56:59 +010021 "io"
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +020022 "io/ioutil"
23 "os"
24 "path"
25 "path/filepath"
26 "strings"
27 "testing"
28
Marcel van Lohuizen50dac242020-07-19 18:33:03 +020029 "cuelang.org/go/cue/ast"
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +020030 "cuelang.org/go/cue/build"
31 "cuelang.org/go/cue/errors"
Marcel van Lohuizen50dac242020-07-19 18:33:03 +020032 "cuelang.org/go/cue/format"
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +020033 "cuelang.org/go/cue/load"
34 "github.com/google/go-cmp/cmp"
Koichi Shiraishi3e624502020-05-17 20:03:54 +090035 "github.com/rogpeppe/go-internal/txtar"
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +020036)
37
Marcel van Lohuizen97b215f2020-07-17 08:52:09 +020038var envUpdate = os.Getenv("CUE_UPDATE")
39
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +020040// A TxTarTest represents a test run that process all CUE tests in the txtar
41// format rooted in a given directory.
42type TxTarTest struct {
43 // Run TxTarTest on this directory.
44 Root string
45
46 // Name is a unique name for this test. The golden file for this test is
47 // derived from the out/<name> file in the .txtar file.
48 //
49 // TODO: by default derive from the current base directory name.
50 Name string
51
52 // If Update is true, TestTxTar will update the out/Name file if it differs
53 // from the original input. The user must set the output in Gold for this
54 // to be detected.
55 Update bool
56
57 // Skip is a map of tests to skip to their skip message.
58 Skip map[string]string
59
60 // ToDo is a map of tests that should be skipped now, but should be fixed.
61 ToDo map[string]string
62}
63
64// A Test represents a single test based on a .txtar file.
65//
66// A Test embeds *testing.T and should be used to report errors.
67//
68// A Test also embeds a *bytes.Buffer which is used to report test results,
69// which are compared against the golden file for the test in the TxTar archive.
70// If the test fails and the update flag is set to true, the Archive will be
71// updated and written to disk.
72type Test struct {
73 // Allow Test to be used as a T.
74 *testing.T
75
Marcel van Lohuizendbfa73b2020-11-12 09:56:59 +010076 prefix string
77 buf *bytes.Buffer // the default buffer
78 outFiles []file
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +020079
80 Archive *txtar.Archive
81
82 // The absolute path of the current test directory.
83 Dir string
84
85 hasGold bool
86}
87
Marcel van Lohuizendbfa73b2020-11-12 09:56:59 +010088func (t *Test) Write(b []byte) (n int, err error) {
89 if t.buf == nil {
90 t.buf = &bytes.Buffer{}
91 t.outFiles = append(t.outFiles, file{t.prefix, t.buf})
92 }
93 return t.buf.Write(b)
94}
95
96type file struct {
97 name string
98 buf *bytes.Buffer
99}
100
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200101func (t *Test) HasTag(key string) bool {
102 prefix := []byte("#" + key)
103 s := bufio.NewScanner(bytes.NewReader(t.Archive.Comment))
104 for s.Scan() {
105 b := s.Bytes()
106 if bytes.Equal(bytes.TrimSpace(b), prefix) {
107 return true
108 }
109 }
110 return false
111}
112
113func (t *Test) Value(key string) (value string, ok bool) {
114 prefix := []byte("#" + key + ":")
115 s := bufio.NewScanner(bytes.NewReader(t.Archive.Comment))
116 for s.Scan() {
117 b := s.Bytes()
118 if bytes.HasPrefix(b, prefix) {
119 return string(bytes.TrimSpace(b[len(prefix):])), true
120 }
121 }
122 return "", false
123}
124
Oleg Kovalovbdb45b32020-07-22 14:47:38 +0200125// Bool searches for a line starting with #key: value in the comment and
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200126// returns true if the key exists and the value is true.
127func (t *Test) Bool(key string) bool {
128 s, ok := t.Value(key)
129 return ok && s == "true"
130}
131
132// Rel converts filename to a normalized form so that it will given the same
133// output across different runs and OSes.
134func (t *Test) Rel(filename string) string {
135 rel, err := filepath.Rel(t.Dir, filename)
136 if err != nil {
137 return filepath.Base(filename)
138 }
Marcel van Lohuizen8f09dde2020-05-19 17:01:44 +0200139 return filepath.ToSlash(rel)
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200140}
141
142// WriteErrors writes strings and
143func (t *Test) WriteErrors(err errors.Error) {
144 if err != nil {
145 errors.Print(t, err, &errors.Config{
146 Cwd: t.Dir,
147 ToSlash: true,
148 })
149 }
150}
151
Marcel van Lohuizen50dac242020-07-19 18:33:03 +0200152// Write file in a directory.
153func (t *Test) WriteFile(f *ast.File) {
Marcel van Lohuizendbfa73b2020-11-12 09:56:59 +0100154 // TODO: use FileWriter instead in separate CL.
Marcel van Lohuizen50dac242020-07-19 18:33:03 +0200155 fmt.Fprintln(t, "==", filepath.Base(f.Filename))
Marcel van Lohuizendbfa73b2020-11-12 09:56:59 +0100156 _, _ = t.Write(formatNode(t.T, f))
157}
158
159// Writer returns a Writer with the given name.
160func (t *Test) Writer(name string) io.Writer {
161 switch name {
162 case "":
163 name = t.prefix
164 default:
165 name = path.Join(t.prefix, name)
166 }
167
168 for _, f := range t.outFiles {
169 if f.name == name {
170 return f.buf
171 }
172 }
173
174 w := &bytes.Buffer{}
175 t.outFiles = append(t.outFiles, file{name, w})
176
177 if name == t.prefix {
178 t.buf = w
179 }
180
181 return w
Marcel van Lohuizen50dac242020-07-19 18:33:03 +0200182}
183
184func formatNode(t *testing.T, n ast.Node) []byte {
185 t.Helper()
186
187 b, err := format.Node(n)
188 if err != nil {
189 t.Fatal(err)
190 }
191 return b
192}
193
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200194// ValidInstances returns the valid instances for this .txtar file or skips the
195// test if there is an error loading the instances.
196func (t *Test) ValidInstances(args ...string) []*build.Instance {
197 a := t.RawInstances(args...)
198 for _, i := range a {
199 if i.Err != nil {
200 if t.hasGold {
201 t.Fatal("Parse error: ", i.Err)
202 }
203 t.Skip("Parse error: ", i.Err)
204 }
205 }
206 return a
207}
208
209// RawInstances returns the intstances represented by this .txtar file. The
210// returned instances are not checked for errors.
211func (t *Test) RawInstances(args ...string) []*build.Instance {
Marcel van Lohuizen16a2b892020-06-29 14:51:18 +0200212 return Load(t.Archive, t.Dir, args...)
213}
214
215// Load loads the intstances of a txtar file. By default, it only loads
216// files in the root directory. Relative files in the archive are given an
217// absolution location by prefixing it with dir.
218func Load(a *txtar.Archive, dir string, args ...string) []*build.Instance {
219 auto := len(args) == 0
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200220 overlay := map[string]load.Source{}
Marcel van Lohuizen16a2b892020-06-29 14:51:18 +0200221 for _, f := range a.Files {
222 if auto && !strings.Contains(f.Name, "/") {
223 args = append(args, f.Name)
224 }
225 overlay[filepath.Join(dir, f.Name)] = load.FromBytes(f.Data)
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200226 }
227
228 cfg := &load.Config{
Marcel van Lohuizen16a2b892020-06-29 14:51:18 +0200229 Dir: dir,
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200230 Overlay: overlay,
231 }
232
233 return load.Instances(args, cfg)
234}
235
236// Run runs tests defined in txtar files in root or its subdirectories.
237// Only tests for which an `old/name` test output file exists are run.
238func (x *TxTarTest) Run(t *testing.T, f func(tc *Test)) {
239 dir, err := os.Getwd()
240 if err != nil {
241 t.Fatal(err)
242 }
243
244 root := x.Root
245
246 err = filepath.Walk(root, func(fullpath string, info os.FileInfo, err error) error {
247 if err != nil {
248 t.Fatal(err)
249 }
250
251 if info.IsDir() || filepath.Ext(fullpath) != ".txtar" {
252 return nil
253 }
254
Marcel van Lohuizen1f42c812020-05-01 16:00:06 +0200255 str := filepath.ToSlash(fullpath)
256 p := strings.Index(str, "/testdata/")
257 testName := str[p+len("/testdata/") : len(str)-len(".txtar")]
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200258
259 t.Run(testName, func(t *testing.T) {
Marcel van Lohuizen16a2b892020-06-29 14:51:18 +0200260 a, err := txtar.ParseFile(fullpath)
261 if err != nil {
262 t.Fatalf("error parsing txtar file: %v", err)
263 }
264
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200265 tc := &Test{
266 T: t,
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200267 Archive: a,
268 Dir: filepath.Dir(filepath.Join(dir, fullpath)),
269
Marcel van Lohuizendbfa73b2020-11-12 09:56:59 +0100270 prefix: path.Join("out", x.Name),
271 }
272
273 for _, f := range a.Files {
274 // TODO: not entirely correct.
275 if strings.HasPrefix(f.Name, tc.prefix) {
276 tc.hasGold = true
277 }
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200278 }
279
280 if tc.HasTag("skip") {
281 t.Skip()
282 }
283
284 if msg, ok := x.Skip[testName]; ok {
285 t.Skip(msg)
286 }
287 if msg, ok := x.ToDo[testName]; ok {
288 t.Skip(msg)
289 }
290
291 f(tc)
292
Marcel van Lohuizendbfa73b2020-11-12 09:56:59 +0100293 update := false
294 for _, sub := range tc.outFiles {
295 var gold *txtar.File
296 for i, f := range a.Files {
297 if f.Name == sub.name {
298 gold = &a.Files[i]
299 }
300 }
301
302 result := sub.buf.Bytes()
303
304 switch {
305 case gold == nil:
306 a.Files = append(a.Files, txtar.File{Name: sub.name})
307 gold = &a.Files[len(a.Files)-1]
308
309 case bytes.Equal(gold.Data, result):
310 continue
311 }
312
313 if x.Update || envUpdate != "" {
314 update = true
315 gold.Data = result
316 continue
317 }
318
319 t.Errorf("result for %s differs:\n%s",
320 sub.name,
321 cmp.Diff(string(gold.Data), string(result)))
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200322 }
323
Marcel van Lohuizendbfa73b2020-11-12 09:56:59 +0100324 if update {
325 err = ioutil.WriteFile(fullpath, txtar.Format(a), 0644)
326 if err != nil {
327 t.Fatal(err)
328 }
Marcel van Lohuizena846fcc2020-05-14 18:48:43 +0200329 }
330 })
331
332 return nil
333 })
334
335 if err != nil {
336 t.Fatal(err)
337 }
338}