github.com/solo-io/cue@v0.4.7/internal/cuetxtar/txtar.go (about) 1 // 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 15 package cuetxtar 16 17 import ( 18 "bufio" 19 "bytes" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "os" 24 "path" 25 "path/filepath" 26 "strings" 27 "testing" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/rogpeppe/go-internal/txtar" 31 "github.com/solo-io/cue/cue/ast" 32 "github.com/solo-io/cue/cue/build" 33 "github.com/solo-io/cue/cue/errors" 34 "github.com/solo-io/cue/cue/format" 35 "github.com/solo-io/cue/cue/load" 36 "github.com/solo-io/cue/internal/cuetest" 37 ) 38 39 // A TxTarTest represents a test run that process all CUE tests in the txtar 40 // format rooted in a given directory. 41 type TxTarTest struct { 42 // Run TxTarTest on this directory. 43 Root string 44 45 // Name is a unique name for this test. The golden file for this test is 46 // derived from the out/<name> file in the .txtar file. 47 // 48 // TODO: by default derive from the current base directory name. 49 Name string 50 51 // If Update is true, TestTxTar will update the out/Name file if it differs 52 // from the original input. The user must set the output in Gold for this 53 // to be detected. 54 Update bool 55 56 // Skip is a map of tests to skip to their skip message. 57 Skip map[string]string 58 59 // ToDo is a map of tests that should be skipped now, but should be fixed. 60 ToDo map[string]string 61 } 62 63 // A Test represents a single test based on a .txtar file. 64 // 65 // A Test embeds *testing.T and should be used to report errors. 66 // 67 // A Test also embeds a *bytes.Buffer which is used to report test results, 68 // which are compared against the golden file for the test in the TxTar archive. 69 // If the test fails and the update flag is set to true, the Archive will be 70 // updated and written to disk. 71 type Test struct { 72 // Allow Test to be used as a T. 73 *testing.T 74 75 prefix string 76 buf *bytes.Buffer // the default buffer 77 outFiles []file 78 79 Archive *txtar.Archive 80 81 // The absolute path of the current test directory. 82 Dir string 83 84 hasGold bool 85 } 86 87 func (t *Test) Write(b []byte) (n int, err error) { 88 if t.buf == nil { 89 t.buf = &bytes.Buffer{} 90 t.outFiles = append(t.outFiles, file{t.prefix, t.buf}) 91 } 92 return t.buf.Write(b) 93 } 94 95 type file struct { 96 name string 97 buf *bytes.Buffer 98 } 99 100 func (t *Test) HasTag(key string) bool { 101 prefix := []byte("#" + key) 102 s := bufio.NewScanner(bytes.NewReader(t.Archive.Comment)) 103 for s.Scan() { 104 b := s.Bytes() 105 if bytes.Equal(bytes.TrimSpace(b), prefix) { 106 return true 107 } 108 } 109 return false 110 } 111 112 func (t *Test) Value(key string) (value string, ok bool) { 113 prefix := []byte("#" + key + ":") 114 s := bufio.NewScanner(bytes.NewReader(t.Archive.Comment)) 115 for s.Scan() { 116 b := s.Bytes() 117 if bytes.HasPrefix(b, prefix) { 118 return string(bytes.TrimSpace(b[len(prefix):])), true 119 } 120 } 121 return "", false 122 } 123 124 // Bool searches for a line starting with #key: value in the comment and 125 // returns true if the key exists and the value is true. 126 func (t *Test) Bool(key string) bool { 127 s, ok := t.Value(key) 128 return ok && s == "true" 129 } 130 131 // Rel converts filename to a normalized form so that it will given the same 132 // output across different runs and OSes. 133 func (t *Test) Rel(filename string) string { 134 rel, err := filepath.Rel(t.Dir, filename) 135 if err != nil { 136 return filepath.Base(filename) 137 } 138 return filepath.ToSlash(rel) 139 } 140 141 // WriteErrors writes strings and 142 func (t *Test) WriteErrors(err errors.Error) { 143 if err != nil { 144 errors.Print(t, err, &errors.Config{ 145 Cwd: t.Dir, 146 ToSlash: true, 147 }) 148 } 149 } 150 151 // Write file in a directory. 152 func (t *Test) WriteFile(f *ast.File) { 153 // TODO: use FileWriter instead in separate CL. 154 fmt.Fprintln(t, "==", filepath.Base(f.Filename)) 155 _, _ = t.Write(formatNode(t.T, f)) 156 } 157 158 // Writer returns a Writer with the given name. 159 func (t *Test) Writer(name string) io.Writer { 160 switch name { 161 case "": 162 name = t.prefix 163 default: 164 name = path.Join(t.prefix, name) 165 } 166 167 for _, f := range t.outFiles { 168 if f.name == name { 169 return f.buf 170 } 171 } 172 173 w := &bytes.Buffer{} 174 t.outFiles = append(t.outFiles, file{name, w}) 175 176 if name == t.prefix { 177 t.buf = w 178 } 179 180 return w 181 } 182 183 func formatNode(t *testing.T, n ast.Node) []byte { 184 t.Helper() 185 186 b, err := format.Node(n) 187 if err != nil { 188 t.Fatal(err) 189 } 190 return b 191 } 192 193 // ValidInstances returns the valid instances for this .txtar file or skips the 194 // test if there is an error loading the instances. 195 func (t *Test) ValidInstances(args ...string) []*build.Instance { 196 a := t.RawInstances(args...) 197 for _, i := range a { 198 if i.Err != nil { 199 if t.hasGold { 200 t.Fatal("Parse error: ", i.Err) 201 } 202 t.Skip("Parse error: ", i.Err) 203 } 204 } 205 return a 206 } 207 208 // RawInstances returns the intstances represented by this .txtar file. The 209 // returned instances are not checked for errors. 210 func (t *Test) RawInstances(args ...string) []*build.Instance { 211 return Load(t.Archive, t.Dir, args...) 212 } 213 214 // Load loads the intstances of a txtar file. By default, it only loads 215 // files in the root directory. Relative files in the archive are given an 216 // absolution location by prefixing it with dir. 217 func Load(a *txtar.Archive, dir string, args ...string) []*build.Instance { 218 auto := len(args) == 0 219 overlay := map[string]load.Source{} 220 for _, f := range a.Files { 221 if auto && !strings.Contains(f.Name, "/") { 222 args = append(args, f.Name) 223 } 224 overlay[filepath.Join(dir, f.Name)] = load.FromBytes(f.Data) 225 } 226 227 cfg := &load.Config{ 228 Dir: dir, 229 Overlay: overlay, 230 } 231 232 return load.Instances(args, cfg) 233 } 234 235 // Run runs tests defined in txtar files in root or its subdirectories. 236 // Only tests for which an `old/name` test output file exists are run. 237 func (x *TxTarTest) Run(t *testing.T, f func(tc *Test)) { 238 dir, err := os.Getwd() 239 if err != nil { 240 t.Fatal(err) 241 } 242 243 root := x.Root 244 245 err = filepath.Walk(root, func(fullpath string, info os.FileInfo, err error) error { 246 if err != nil { 247 t.Fatal(err) 248 } 249 250 if info.IsDir() || filepath.Ext(fullpath) != ".txtar" { 251 return nil 252 } 253 254 str := filepath.ToSlash(fullpath) 255 p := strings.Index(str, "/testdata/") 256 testName := str[p+len("/testdata/") : len(str)-len(".txtar")] 257 258 t.Run(testName, func(t *testing.T) { 259 a, err := txtar.ParseFile(fullpath) 260 if err != nil { 261 t.Fatalf("error parsing txtar file: %v", err) 262 } 263 264 tc := &Test{ 265 T: t, 266 Archive: a, 267 Dir: filepath.Dir(filepath.Join(dir, fullpath)), 268 269 prefix: path.Join("out", x.Name), 270 } 271 272 for _, f := range a.Files { 273 // TODO: not entirely correct. 274 if strings.HasPrefix(f.Name, tc.prefix) { 275 tc.hasGold = true 276 } 277 } 278 279 if tc.HasTag("skip") { 280 t.Skip() 281 } 282 283 if msg, ok := x.Skip[testName]; ok { 284 t.Skip(msg) 285 } 286 if msg, ok := x.ToDo[testName]; ok { 287 t.Skip(msg) 288 } 289 290 f(tc) 291 292 update := false 293 for _, sub := range tc.outFiles { 294 var gold *txtar.File 295 for i, f := range a.Files { 296 if f.Name == sub.name { 297 gold = &a.Files[i] 298 } 299 } 300 301 result := sub.buf.Bytes() 302 303 switch { 304 case gold == nil: 305 a.Files = append(a.Files, txtar.File{Name: sub.name}) 306 gold = &a.Files[len(a.Files)-1] 307 308 case bytes.Equal(gold.Data, result): 309 continue 310 } 311 312 if x.Update || cuetest.UpdateGoldenFiles { 313 update = true 314 gold.Data = result 315 continue 316 } 317 318 t.Errorf("result for %s differs:\n%s", 319 sub.name, 320 cmp.Diff(string(gold.Data), string(result))) 321 } 322 323 if update { 324 err = ioutil.WriteFile(fullpath, txtar.Format(a), 0644) 325 if err != nil { 326 t.Fatal(err) 327 } 328 } 329 }) 330 331 return nil 332 }) 333 334 if err != nil { 335 t.Fatal(err) 336 } 337 }