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  }