...

Source file src/cuelang.org/go/doc/tutorial/kubernetes/tut_test.go

Documentation: cuelang.org/go/doc/tutorial/kubernetes

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

View as plain text