doc/tutorial/kubernetes: add tests for examples

Change-Id: Ia3987cc2694999c480dc43f481e4d26e14336d3d
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2180
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/add.go b/cmd/cue/cmd/add.go
index d4787ac..57b1fbc 100644
--- a/cmd/cue/cmd/add.go
+++ b/cmd/cue/cmd/add.go
@@ -203,7 +203,7 @@
 	if len(fo.contents) == 0 {
 		return os.Remove(fo.filename)
 	}
-	return ioutil.WriteFile(fo.filename, fo.contents, 0755)
+	return ioutil.WriteFile(fo.filename, fo.contents, 0644)
 }
 
 type fileInfo struct {
@@ -288,7 +288,7 @@
 		return originalFile{}, err
 	}
 
-	if err = ioutil.WriteFile(fi.filename, b, 0755); err != nil {
+	if err = ioutil.WriteFile(fi.filename, b, 0644); err != nil {
 		// Just in case, attempt to restore original file.
 		fo.restore()
 		return originalFile{}, err
diff --git a/cmd/cue/cmd/root.go b/cmd/cue/cmd/root.go
index 11db921..5b3a964 100644
--- a/cmd/cue/cmd/root.go
+++ b/cmd/cue/cmd/root.go
@@ -97,7 +97,10 @@
 	if err != nil {
 		return err
 	}
-	return cmd.Run(ctx)
+	err = cmd.Run(ctx)
+	// TODO: remove this ugly hack. Either fix Cobra or use something else.
+	stdin = nil
+	return err
 }
 
 type Command struct {
@@ -111,6 +114,11 @@
 	c.root.SetOutput(w)
 }
 
+func (c *Command) SetInput(r io.Reader) {
+	// TODO: ugly hack. Cobra does not have a way to pass the stdin.
+	stdin = r
+}
+
 func (c *Command) Run(ctx context.Context) (err error) {
 	log.SetFlags(0)
 	// Three categories of commands:
diff --git a/doc/tutorial/basics/tut_test.go b/doc/tutorial/basics/tut_test.go
index 38ac49e..29336a6 100644
--- a/doc/tutorial/basics/tut_test.go
+++ b/doc/tutorial/basics/tut_test.go
@@ -85,5 +85,5 @@
 		gold = gold[p+1:]
 	}
 
-	cuetest.Run(t, dir, command, gold)
+	cuetest.Run(t, dir, command, &cuetest.Config{Golden: gold})
 }
diff --git a/doc/tutorial/kubernetes/README.md b/doc/tutorial/kubernetes/README.md
index 08999e4..05bb2a9 100644
--- a/doc/tutorial/kubernetes/README.md
+++ b/doc/tutorial/kubernetes/README.md
@@ -74,7 +74,8 @@
 for good measure.
 
 ```
-$ cue mod init
+$ touch cue.mod
+cue mod init
 ```
 -->
 
@@ -167,7 +168,7 @@
 Now the file looks like:
 
 ```
-$  cat mon/prometheus/configmap.cue
+$ cat mon/prometheus/configmap.cue
 package kube
 
 import "encoding/yaml"
@@ -348,6 +349,14 @@
 We do this by setting a newly defined top-level field in each directory
 to the directory name and modify our master template file to use it.
 
+<!--
+```
+$ cue add */kube.cue -p kube --list <<EOF
+_component: "{{.DisplayPath}}"
+EOF
+```
+-->
+
 ```
 # set the component label to our new top-level field
 $ sed -i "" 's/component:.*string/component: _component/' kube.cue
@@ -831,7 +840,7 @@
 We create the tool file to do just that.
 
 ```
-$ cat << EOF > kube_tool.cue
+$ cat <<EOF > kube_tool.cue
 package kube
 
 objects: [ x for v in objectSets for x in v ]
@@ -857,19 +866,27 @@
 We start by defining the `ls` command which dumps all our objects
 
 ```
-$ cat << EOF > ls_tool.cue
+$ cat <<EOF > ls_tool.cue
 package kube
 
-import "strings"
+import (
+	"text/tabwriter"
+	"tool/cli"
+	"tool/file"
+)
 
 command ls: {
-    task print: {
-        kind: "print"
-        Lines = [
-            "\(x.kind)  \t\(x.metadata.labels.component)   \t\(x.metadata.name)"
-            for x in objects ]
-        text: strings.Join(Lines, "\n")
-    }
+	task print: cli.Print & {
+		text: tabwriter.Write([
+			"\(x.kind)  \t\(x.metadata.labels.component)  \t\(x.metadata.name)"
+			for x in objects
+		])
+	}
+
+	task write: file.Create & {
+		filename: "foo.txt"
+		contents: task.print.text
+	}
 }
 EOF
 ```
@@ -934,16 +951,18 @@
 TODO: add command line flags to filter object types.
 -->
 ```
-$ cat << EOF > dump_tool.cue
+$ cat <<EOF > dump_tool.cue
 package kube
 
-import "encoding/yaml"
+import (
+	"encoding/yaml"
+	"tool/cli"
+)
 
 command dump: {
-    task print: {
-        kind: "print"
-        text: yaml.MarshalStream(objects)
-    }
+	task print: cli.Print & {
+		text: yaml.MarshalStream(objects)
+	}
 }
 EOF
 ```
@@ -968,19 +987,22 @@
 $ cat <<EOF > create_tool.cue
 package kube
 
-import "encoding/yaml"
+import (
+	"encoding/yaml"
+	"tool/exec"
+	"tool/cli"
+)
 
 command create: {
-    task kube: {
-        kind:   "exec"
-        cmd:    "kubectl create --dry-run -f -"
-        stdin:  yaml.MarshalStream(objects)
-        stdout: string
-    }
-    task display: {
-        kind: "print"
-        text: task.kube.stdout
-    }
+	task kube: exec.Run & {
+		cmd:    "kubectl create --dry-run -f -"
+		stdin:  yaml.MarshalStream(objects)
+		stdout: string
+	}
+
+	task display: cli.Print & {
+		text: task.kube.stdout
+	}
 }
 EOF
 ```
@@ -1224,7 +1246,7 @@
 subdirectory.
 Running our usual count yields
 ```
-find . | grep kube.cue | xargs wc | tail -1
+$ find . | grep kube.cue | xargs wc | tail -1
      542    1190   11520 total
 ```
 This does not count our conversion templates.
diff --git a/doc/tutorial/kubernetes/quick/services/kube.cue b/doc/tutorial/kubernetes/quick/services/kube.cue
index d6b4672..6be6276 100644
--- a/doc/tutorial/kubernetes/quick/services/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kube.cue
@@ -6,22 +6,41 @@
 	metadata: {
 		name: Name
 		labels: {
-			app:       Name
-			component: _component
-			domain:    "prod"
+			app:       Name       // by convention
+			domain:    "prod"     // always the same in the given files
+			component: _component // varies per directory
 		}
 	}
 	spec: {
 		// Any port has the following properties.
 		ports: [...{
 			port:     int
-			protocol: *"TCP" | "UDP"
+			protocol: *"TCP" | "UDP" // from the Kubernetes definition
 			name:     string | *"client"
 		}]
 		selector: metadata.labels // we want those to be the same
 	}
 }
 
+deployment <Name>: {
+	apiVersion: "extensions/v1beta1"
+	kind:       "Deployment"
+	metadata name: Name
+	spec: {
+		// 1 is the default, but we allow any number
+		replicas: *1 | int
+		template: {
+			metadata labels: {
+				app:       Name
+				domain:    "prod"
+				component: _component
+			}
+			// we always have one namesake container
+			spec containers: [{name: Name}]
+		}
+	}
+}
+
 _component: string
 
 daemonSet <Name>: _spec & {
diff --git a/doc/tutorial/kubernetes/tut_test.go b/doc/tutorial/kubernetes/tut_test.go
new file mode 100644
index 0000000..774bb73
--- /dev/null
+++ b/doc/tutorial/kubernetes/tut_test.go
@@ -0,0 +1,208 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package kubernetes
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"cuelang.org/go/internal/cuetest"
+	"github.com/kylelemons/godebug/diff"
+	"github.com/retr0h/go-gilt/copy"
+)
+
+var (
+	update  = flag.Bool("update", false, "update test data")
+	cleanup = flag.Bool("cleanup", true, "clean up generated files")
+)
+
+func TestTutorial(t *testing.T) {
+	cwd, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Read the tutorial.
+	b, err := ioutil.ReadFile("README.md")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Copy test data and change the cwd to this directory.
+	dir, err := ioutil.TempDir("", "tutorial")
+	if err != nil {
+		log.Fatal(err)
+	}
+	if *cleanup {
+		defer os.RemoveAll(dir)
+	} else {
+		logf(t, "Temporary dir: %v", dir)
+	}
+
+	wd := filepath.Join(dir, "services")
+	if err := copy.Dir(filepath.Join("testdata", "services"), wd); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.Chdir(wd); err != nil {
+		t.Fatal(err)
+	}
+	defer os.Chdir(cwd)
+	logf(t, "Tmp dir: %s", wd)
+
+	// Execute the tutorial.
+	for c := cuetest.NewChunker(t, b); c.Next("```", "```"); {
+		for c := cuetest.NewChunker(t, c.Bytes()); c.Next("$ ", "\n"); {
+			alt := c.Text()
+			cmd := strings.Replace(alt, "<<EOF", "", -1)
+
+			input := ""
+			if cmd != alt {
+				if !c.Next("", "EOF") {
+					t.Fatalf("non-terminated <<EOF")
+				}
+				input = c.Text()
+			}
+
+			redirect := ""
+			if p := strings.Index(cmd, " >"); p > 0 {
+				redirect = cmd[p+1:]
+				cmd = cmd[:p]
+			}
+
+			switch cmd = strings.TrimSpace(cmd); {
+			case strings.HasPrefix(cmd, "cat"):
+				if input == "" {
+					break
+				}
+				var r *os.File
+				var err error
+				if strings.HasPrefix(redirect, ">>") {
+					// Append input
+					r, err = os.OpenFile(
+						strings.TrimSpace(redirect[2:]),
+						os.O_APPEND|os.O_CREATE|os.O_WRONLY,
+						0666)
+				} else { // strings.HasPrefix(redirect, ">")
+					// Create new file with input
+					r, err = os.Create(strings.TrimSpace(redirect[1:]))
+				}
+				if err != nil {
+					t.Fatal(err)
+				}
+				_, err = io.WriteString(r, input)
+				if err := r.Close(); err != nil {
+					t.Fatal(err)
+				}
+				if err != nil {
+					t.Fatal(err)
+				}
+
+			case strings.HasPrefix(cmd, "cue "):
+				logf(t, "$ %s", cmd)
+
+				if strings.HasPrefix(cmd, "cue create") {
+					// Don't execute the kubernetes dry run.
+					break
+				}
+
+				cuetest.Run(t, wd, cmd, &cuetest.Config{
+					Stdin:  strings.NewReader(input),
+					Stdout: ioutil.Discard,
+				})
+
+			case strings.HasPrefix(cmd, "sed "),
+				strings.HasPrefix(cmd, "touch "):
+				logf(t, "$ %s", cmd)
+				args := cuetest.SplitArgs(t, cmd)
+				cx := exec.Command(args[0], args[1:]...)
+				if input != "" {
+					cx.Stdin = strings.NewReader(input)
+					cx.Stdout = ioutil.Discard
+				}
+				if err := cx.Run(); err != nil {
+					t.Fatal(err)
+				}
+			}
+		}
+	}
+
+	if err := os.Chdir(filepath.Join(cwd, "quick")); err != nil {
+		t.Fatal(err)
+	}
+
+	if *update {
+		// Remove all old cue files.
+		filepath.Walk("", func(path string, info os.FileInfo, err error) error {
+			if isCUE(path) {
+				if err := os.Remove(path); err != nil {
+					t.Fatal(err)
+				}
+			}
+			return nil
+		})
+
+		filepath.Walk(wd, func(path string, info os.FileInfo, err error) error {
+			if isCUE(path) {
+				return copy.File(path, "services"+path[len(wd):])
+			}
+			return nil
+		})
+		return
+	}
+
+	// Compare the output in the temp directory with the quick output.
+	err = filepath.Walk(wd, func(path string, info os.FileInfo, err error) error {
+		if filepath.Ext(path) != ".cue" {
+			return nil
+		}
+		b1, err := ioutil.ReadFile(path)
+		if err != nil {
+			t.Fatal(err)
+		}
+		b2, err := ioutil.ReadFile("services" + path[len(wd):])
+		if err != nil {
+			t.Fatal(err)
+		}
+		got, want := string(b1), string(b2)
+		if got != want {
+			t.Log(diff.Diff(got, want))
+			return fmt.Errorf("file %q differs", path)
+		}
+		return nil
+	})
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func isCUE(filename string) bool {
+	return filepath.Ext(filename) == ".cue" && !strings.Contains(filename, "_tool")
+}
+
+func logf(t *testing.T, format string, args ...interface{}) {
+	t.Logf(format, args...)
+	log.Printf(format, args...)
+}
+
+// TODO:
+// Test manual and quick: evaluation results in output of testdata directory.
diff --git a/internal/cuetest/sim.go b/internal/cuetest/sim.go
index edd92aa..9f6be14 100644
--- a/internal/cuetest/sim.go
+++ b/internal/cuetest/sim.go
@@ -18,6 +18,7 @@
 	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"os"
 	"regexp"
 	"strings"
@@ -27,9 +28,19 @@
 	"github.com/kylelemons/godebug/diff"
 )
 
+type Config struct {
+	Stdin  io.Reader
+	Stdout io.Writer
+	Golden string
+}
+
 // Run executes the given command in the given directory and reports any
 // errors comparing it to the gold standard.
-func Run(t *testing.T, dir, command, gold string) {
+func Run(t *testing.T, dir, command string, cfg *Config) {
+	if cfg == nil {
+		cfg = &Config{}
+	}
+
 	old, err := os.Getwd()
 	if err != nil {
 		t.Fatal(err)
@@ -42,16 +53,23 @@
 	logf(t, "Executing command: %s", command)
 
 	command = strings.TrimSpace(command[4:])
-	args := splitArgs(t, command)
+	args := SplitArgs(t, command)
 	logf(t, "Args: %q", args)
 
 	buf := &bytes.Buffer{}
 	cmd, err := cmd.New(args)
 	cmd.SetOutput(buf)
+	if cfg.Stdin != nil {
+		cmd.SetInput(cfg.Stdin)
+	}
 	if err = cmd.Run(context.Background()); err != nil {
 		logf(t, "Execution failed: %v", err)
 	}
 
+	if cfg.Golden == "" {
+		return
+	}
+
 	pattern := fmt.Sprintf("//.*%s.*", regexp.QuoteMeta(dir))
 	re, err := regexp.Compile(pattern)
 	if err != nil {
@@ -60,7 +78,7 @@
 	got := re.ReplaceAllString(buf.String(), "")
 	got = strings.TrimSpace(got)
 
-	want := strings.TrimSpace(gold)
+	want := strings.TrimSpace(cfg.Golden)
 	if got != want {
 		t.Errorf("files differ:\n%s", diff.Diff(want, got))
 	}
@@ -70,7 +88,7 @@
 	t.Logf(format, args...)
 }
 
-func splitArgs(t *testing.T, s string) (args []string) {
+func SplitArgs(t *testing.T, s string) (args []string) {
 	c := NewChunker(t, []byte(s))
 	for {
 		found := c.Find(" '")