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(" '")