github.com/solo-io/cue@v0.4.7/doc/tutorial/kubernetes/tut_test.go (about) 1 // Copyright 2019 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 kubernetes 16 17 import ( 18 "bytes" 19 "context" 20 "flag" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "log" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "regexp" 29 "strings" 30 "testing" 31 32 "github.com/kylelemons/godebug/diff" 33 34 "github.com/solo-io/cue/cmd/cue/cmd" 35 "github.com/solo-io/cue/cue/load" 36 "github.com/solo-io/cue/internal/copy" 37 "github.com/solo-io/cue/internal/cuetest" 38 ) 39 40 var ( 41 cleanup = flag.Bool("cleanup", true, "clean up generated files") 42 ) 43 44 func TestTutorial(t *testing.T) { 45 if testing.Short() { 46 t.Skip() 47 } 48 49 cwd, err := os.Getwd() 50 if err != nil { 51 t.Fatal(err) 52 } 53 54 // Read the tutorial. 55 b, err := ioutil.ReadFile("README.md") 56 if err != nil { 57 t.Fatal(err) 58 } 59 60 // Copy test data and change the cwd to this directory. 61 dir, err := ioutil.TempDir("", "tutorial") 62 if err != nil { 63 log.Fatal(err) 64 } 65 if *cleanup { 66 defer os.RemoveAll(dir) 67 } else { 68 defer logf(t, "Temporary dir: %v", dir) 69 } 70 71 wd := filepath.Join(dir, "services") 72 if err := copy.Dir(filepath.Join("original", "services"), wd); err != nil { 73 t.Fatal(err) 74 } 75 76 run(t, dir, "cue mod init", &config{ 77 // Stdin: strings.NewReader(input), 78 }) 79 80 if cuetest.UpdateGoldenFiles { 81 // The test environment won't work in all environments. We create 82 // a fake go.mod so that Go will find the module root. By default 83 // we won't set it. 84 out := execute(t, dir, "go", "mod", "init", "cuelang.org/dummy") 85 logf(t, "%s", out) 86 } else { 87 // We only fetch new kubernetes files with when updating. 88 err := copy.Dir(load.GenPath("quick"), load.GenPath(dir)) 89 if err != nil { 90 t.Fatal(err) 91 } 92 } 93 94 if err := os.Chdir(wd); err != nil { 95 t.Fatal(err) 96 } 97 defer os.Chdir(cwd) 98 logf(t, "Changed to directory: %s", wd) 99 100 // Execute the tutorial. 101 for c := cuetest.NewChunker(t, b); c.Next("```", "```"); { 102 for c := cuetest.NewChunker(t, c.Bytes()); c.Next("$ ", "\n"); { 103 alt := c.Text() 104 cmd := strings.Replace(alt, "<<EOF", "", -1) 105 106 input := "" 107 if cmd != alt { 108 if !c.Next("", "EOF") { 109 t.Fatalf("non-terminated <<EOF") 110 } 111 input = c.Text() 112 } 113 114 redirect := "" 115 if p := strings.Index(cmd, " >"); p > 0 { 116 redirect = cmd[p+1:] 117 cmd = cmd[:p] 118 } 119 120 logf(t, "$ %s", cmd) 121 switch cmd = strings.TrimSpace(cmd); { 122 case strings.HasPrefix(cmd, "cat"): 123 if input == "" { 124 break 125 } 126 var r *os.File 127 var err error 128 if strings.HasPrefix(redirect, ">>") { 129 // Append input 130 r, err = os.OpenFile( 131 strings.TrimSpace(redirect[2:]), 132 os.O_APPEND|os.O_CREATE|os.O_WRONLY, 133 0666) 134 } else { // strings.HasPrefix(redirect, ">") 135 // Create new file with input 136 r, err = os.Create(strings.TrimSpace(redirect[1:])) 137 } 138 if err != nil { 139 t.Fatal(err) 140 } 141 _, err = io.WriteString(r, input) 142 if err := r.Close(); err != nil { 143 t.Fatal(err) 144 } 145 if err != nil { 146 t.Fatal(err) 147 } 148 149 case strings.HasPrefix(cmd, "cue "): 150 if strings.HasPrefix(cmd, "cue create") { 151 // Don't execute the kubernetes dry run. 152 break 153 } 154 if strings.HasPrefix(cmd, "cue mod init") { 155 // Already ran this at setup. 156 break 157 } 158 159 if !cuetest.UpdateGoldenFiles && strings.HasPrefix(cmd, "cue get") { 160 // Don't fetch stuff in normal mode. 161 break 162 } 163 164 run(t, wd, cmd, &config{ 165 Stdin: strings.NewReader(input), 166 Stdout: os.Stdout, 167 }) 168 169 case strings.HasPrefix(cmd, "sed "): 170 c := cuetest.NewChunker(t, []byte(cmd)) 171 c.Next("s/", "/") 172 re := regexp.MustCompile(c.Text()) 173 c.Next("", "/'") 174 repl := c.Bytes() 175 c.Next(" ", ".cue") 176 file := c.Text() + ".cue" 177 b, err := ioutil.ReadFile(file) 178 if err != nil { 179 t.Fatal(err) 180 } 181 b = re.ReplaceAll(b, repl) 182 err = ioutil.WriteFile(file, b, 0644) 183 if err != nil { 184 t.Fatal(err) 185 } 186 187 case strings.HasPrefix(cmd, "touch "): 188 logf(t, "$ %s", cmd) 189 file := strings.TrimSpace(cmd[len("touch "):]) 190 err := ioutil.WriteFile(file, []byte(""), 0644) 191 if err != nil { 192 t.Fatal(err) 193 } 194 case strings.HasPrefix(cmd, "go "): 195 if !cuetest.UpdateGoldenFiles && strings.HasPrefix(cmd, "go get") { 196 // Don't fetch stuff in normal mode. 197 break 198 } 199 200 out := execute(t, wd, splitArgs(t, cmd)...) 201 logf(t, "%s", out) 202 } 203 } 204 } 205 206 if err := os.Chdir(filepath.Join(cwd, "quick")); err != nil { 207 t.Fatal(err) 208 } 209 210 if cuetest.UpdateGoldenFiles { 211 // Remove all old cue files. 212 err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 213 if isCUE(path) { 214 if err := os.Remove(path); err != nil { 215 t.Fatal(err) 216 } 217 } 218 return err 219 }) 220 if err != nil { 221 t.Fatal(err) 222 } 223 224 err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 225 if isCUE(path) { 226 dst := path[len(dir)+1:] 227 err := os.MkdirAll(filepath.Dir(dst), 0755) 228 if err != nil { 229 return err 230 } 231 return copy.File(path, dst) 232 } 233 return err 234 }) 235 if err != nil { 236 t.Fatal(err) 237 } 238 return 239 } 240 241 // Compare the output in the temp directory with the quick output. 242 err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 243 if filepath.Ext(path) != ".cue" { 244 return nil 245 } 246 b1, err := ioutil.ReadFile(path) 247 if err != nil { 248 t.Fatal(err) 249 } 250 b2, err := ioutil.ReadFile(path[len(dir)+1:]) 251 if err != nil { 252 t.Fatal(err) 253 } 254 got, want := string(b1), string(b2) 255 if got != want { 256 t.Log(diff.Diff(got, want)) 257 return fmt.Errorf("file %q differs", path) 258 } 259 return nil 260 }) 261 if err != nil { 262 t.Error(err) 263 } 264 } 265 266 func isCUE(filename string) bool { 267 return filepath.Ext(filename) == ".cue" && !strings.Contains(filename, "_tool") 268 } 269 270 func TestEval(t *testing.T) { 271 for _, dir := range []string{"quick", "manual"} { 272 t.Run(dir, func(t *testing.T) { 273 buf := &bytes.Buffer{} 274 run(t, dir, "cue eval ./...", &config{ 275 Stdout: buf, 276 }) 277 278 cwd, _ := os.Getwd() 279 pattern := fmt.Sprintf("//.*%s.*", regexp.QuoteMeta(filepath.Join(cwd, dir))) 280 re, err := regexp.Compile(pattern) 281 if err != nil { 282 t.Fatal(err) 283 } 284 got := re.ReplaceAll(buf.Bytes(), []byte{}) 285 got = bytes.TrimSpace(got) 286 287 testfile := filepath.Join("testdata", dir+".out") 288 289 if cuetest.UpdateGoldenFiles { 290 err := ioutil.WriteFile(testfile, got, 0644) 291 if err != nil { 292 t.Fatal(err) 293 } 294 return 295 } 296 297 b, err := ioutil.ReadFile(testfile) 298 if err != nil { 299 t.Fatal(err) 300 } 301 302 if got, want := string(got), string(b); got != want { 303 t.Log(got) 304 t.Errorf("output differs for file %s in %s", testfile, cwd) 305 } 306 }) 307 } 308 } 309 310 type config struct { 311 Stdin io.Reader 312 Stdout io.Writer 313 Golden string 314 } 315 316 // execute executes the given command in the given directory 317 func execute(t *testing.T, dir string, args ...string) string { 318 old, err := os.Getwd() 319 if err != nil { 320 t.Fatal(err) 321 } 322 if err = os.Chdir(dir); err != nil { 323 t.Fatal(err) 324 } 325 defer func() { os.Chdir(old) }() 326 327 logf(t, "Executing command: %s", strings.Join(args, " ")) 328 329 cmd := exec.Command(args[0], args[1:]...) 330 out, err := cmd.CombinedOutput() 331 if err != nil { 332 t.Fatalf("failed to run [%v] in %s: %v\n%s", cmd, dir, err, out) 333 } 334 return string(out) 335 } 336 337 // run executes the given command in the given directory and reports any 338 // errors comparing it to the gold standard. 339 func run(t *testing.T, dir, command string, cfg *config) { 340 if cfg == nil { 341 cfg = &config{} 342 } 343 344 old, err := os.Getwd() 345 if err != nil { 346 t.Fatal(err) 347 } 348 if err = os.Chdir(dir); err != nil { 349 t.Fatal(err) 350 } 351 defer func() { os.Chdir(old) }() 352 353 logf(t, "Executing command: %s", command) 354 355 command = strings.TrimSpace(command[4:]) 356 args := splitArgs(t, command) 357 logf(t, "Args: %q", args) 358 359 buf := &bytes.Buffer{} 360 if cfg.Golden != "" { 361 if cfg.Stdout != nil { 362 t.Fatal("cannot set Golden and Stdout") 363 } 364 cfg.Stdout = buf 365 } 366 cmd, err := cmd.New(args) 367 if err != nil { 368 t.Fatal(err) 369 } 370 if cfg.Stdout != nil { 371 cmd.SetOutput(cfg.Stdout) 372 } else { 373 cmd.SetOutput(buf) 374 } 375 if cfg.Stdin != nil { 376 cmd.SetInput(cfg.Stdin) 377 } 378 if err = cmd.Run(context.Background()); err != nil { 379 if cfg.Stdout == nil { 380 logf(t, "Output:\n%s", buf.String()) 381 } 382 logf(t, "Execution failed: %v", err) 383 } 384 385 if cfg.Golden == "" { 386 return 387 } 388 389 pattern := fmt.Sprintf("//.*%s.*", regexp.QuoteMeta(dir)) 390 re, err := regexp.Compile(pattern) 391 if err != nil { 392 t.Fatal(err) 393 } 394 got := re.ReplaceAllString(buf.String(), "") 395 got = strings.TrimSpace(got) 396 397 want := strings.TrimSpace(cfg.Golden) 398 if got != want { 399 t.Errorf("files differ:\n%s", diff.Diff(got, want)) 400 } 401 } 402 403 func logf(t *testing.T, format string, args ...interface{}) { 404 t.Helper() 405 t.Logf(format, args...) 406 } 407 408 func splitArgs(t *testing.T, s string) (args []string) { 409 c := cuetest.NewChunker(t, []byte(s)) 410 for { 411 found := c.Find(" '") 412 args = append(args, strings.Split(c.Text(), " ")...) 413 if !found { 414 break 415 } 416 c.Next("", "' ") 417 args = append(args, c.Text()) 418 } 419 return args 420 }