doc/tutorial/basics: add test for tutorial

test whether examples in tutorial produce the desired
results.

Closes #50.

Change-Id: I3c5b8a465e6ed98dad2962efd88ce246173250a6
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2178
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/doc/tutorial/basics/bottom.md b/doc/tutorial/basics/bottom.md
index e86cfae..46515da 100644
--- a/doc/tutorial/basics/bottom.md
+++ b/doc/tutorial/basics/bottom.md
@@ -31,8 +31,8 @@
 <!-- result -->
 `$ cue eval -i bottom.cue`
 ```
-a:    _|_
-l:    _|_
+a: _|_ /* conflicting values: 4 != 5 */
+l: [1, _|_ /* conflicting values: 2 != 3 */]
 list: [0, 1, 2]
-val:  _|_
+val: _|_ /* index 3 out of bounds */
 ```
diff --git a/doc/tutorial/basics/coalesce.md b/doc/tutorial/basics/coalesce.md
index a3b64bd..d1e0495 100644
--- a/doc/tutorial/basics/coalesce.md
+++ b/doc/tutorial/basics/coalesce.md
@@ -40,8 +40,7 @@
 <!-- result -->
 `$ cue eval coalesce.cue`
 ```
-list: [ "Cat", "Mouse", "Dog" ]
-
+list: ["Cat", "Mouse", "Dog"]
 a: "Cat"
 b: "None"
 n: [null]
diff --git a/doc/tutorial/basics/conditional.md b/doc/tutorial/basics/conditional.md
index 571efb8..86e5932 100644
--- a/doc/tutorial/basics/conditional.md
+++ b/doc/tutorial/basics/conditional.md
@@ -14,7 +14,7 @@
 <!-- CUE editor -->
 _conditional.cue:_
 ```
-price: float
+price: number
 
 // Require a justification if price is too high
 justification: string if price > 100
diff --git a/doc/tutorial/basics/cycle.md b/doc/tutorial/basics/cycle.md
index 4eda301..10b2206 100644
--- a/doc/tutorial/basics/cycle.md
+++ b/doc/tutorial/basics/cycle.md
@@ -33,7 +33,6 @@
 ```
 x: 200
 y: 100
-
-a: _|_ // cycle detected
-b: _|_ // cycle detected
+a: _|_ /* cycle detected */
+b: _|_ /* cycle detected */
 ```
diff --git a/doc/tutorial/basics/cycleref.md b/doc/tutorial/basics/cycleref.md
index 3f559ef..d29e650 100644
--- a/doc/tutorial/basics/cycleref.md
+++ b/doc/tutorial/basics/cycleref.md
@@ -22,6 +22,12 @@
 <!-- result -->
 `$ cue eval cycleref.cue`
 ```
-labels:    {app: "foo", name: "bar"}
-selectors: {app: "foo", name: "bar"}
+labels: {
+    name: "bar"
+    app:  "foo"
+}
+selectors: {
+    name: "bar"
+    app:  "foo"
+}
 ```
diff --git a/doc/tutorial/basics/defaults.md b/doc/tutorial/basics/defaults.md
index 0c4d3bc..c348c25 100644
--- a/doc/tutorial/basics/defaults.md
+++ b/doc/tutorial/basics/defaults.md
@@ -29,5 +29,5 @@
 `$ cue eval defaults.cue`
 ```
 replicas: 1
-protocol: *"tcp" | *"udp"
+protocol: "tcp" | "udp" | *_|_
 ```
\ No newline at end of file
diff --git a/doc/tutorial/basics/fieldcomp.md b/doc/tutorial/basics/fieldcomp.md
index f2443f3..92375b7 100644
--- a/doc/tutorial/basics/fieldcomp.md
+++ b/doc/tutorial/basics/fieldcomp.md
@@ -29,18 +29,18 @@
 `$ cue eval fieldcomp.cue`
 ```
 barcelona: {
-    pos:     1
     name:    "Barcelona"
+    pos:     1
     nameLen: 9
 }
 shanghai: {
-    pos:     2
     name:    "Shanghai"
+    pos:     2
     nameLen: 8
 }
 munich: {
-    pos:     3
     name:    "Munich"
+    pos:     3
     nameLen: 6
 }
 ```
diff --git a/doc/tutorial/basics/interpolfield.md b/doc/tutorial/basics/interpolfield.md
index dd07c22..896b7aa 100644
--- a/doc/tutorial/basics/interpolfield.md
+++ b/doc/tutorial/basics/interpolfield.md
@@ -9,7 +9,7 @@
 One cannot refer to generated fields with references.
 
 <!-- CUE editor -->
-_- genfield.cue:_
+_genfield.cue:_
 ```
 sandwich: {
     type:            "Cheese"
@@ -24,8 +24,8 @@
 ```
 sandwich: {
     type:            "Cheese"
-    hasCheese:       true
     hasButter:       true
-    butterAndCheese: _|_ // unknown reference 'hasCheese'
+    butterAndCheese: _|_ /* reference "hasCheese" not found */
+    hasCheese:       true
 }
 ```
\ No newline at end of file
diff --git a/doc/tutorial/basics/lists.md b/doc/tutorial/basics/lists.md
index aadcd6d..b349c56 100644
--- a/doc/tutorial/basics/lists.md
+++ b/doc/tutorial/basics/lists.md
@@ -38,12 +38,8 @@
 <!-- result -->
 `$ cue eval -i lists.cue`
 ```
-IP: [>=0 & <=255, >=0 & <=255, >=0 & <=255, >=0 & <=255]
-PrivateIP:
-    [10, >=0 & <=255, >=0 & <=255, >=0 & <=255] |
-    [192, 168, >=0 & <=255, >=0 & <=255] |
-    [172, >=16 & <=32, >=0 & <=255, >=0 & <=255]
-
-myIP:   [10, 2, 3, 4]
-yourIP: _|_
+IP: [uint8, uint8, uint8, uint8]
+PrivateIP: [10, uint8, uint8, uint8] | [192, 168, uint8, uint8] | [172, >=16 & <=32, uint8, uint8]
+myIP: [10, 2, 3, 4]
+yourIP: _|_ /* empty disjunction: [((10 & (int & >=0 & int & <=255)) & 11),((int & >=0 & int & <=255) & 1),((int & >=0 & int & <=255) & 2),((int & >=0 & int & <=255) & 3)] */
 ```
\ No newline at end of file
diff --git a/doc/tutorial/basics/numbers.md b/doc/tutorial/basics/numbers.md
index f703f45..d2fb464 100644
--- a/doc/tutorial/basics/numbers.md
+++ b/doc/tutorial/basics/numbers.md
@@ -20,7 +20,7 @@
 a: int
 a: 4 // type int
 
-b: float
+b: number
 b: 4 // type float
 
 c: int
@@ -33,7 +33,7 @@
 `$ cue eval -i numbers.cue`
 ```
 a: 4
-b: 4.0
-c: _|_
+b: 4
+c: _|_ /* unsupported op &(int, float) */
 d: 4
 ```
\ No newline at end of file
diff --git a/doc/tutorial/basics/packages.md b/doc/tutorial/basics/packages.md
index b460e40..9cc0d19 100644
--- a/doc/tutorial/basics/packages.md
+++ b/doc/tutorial/basics/packages.md
@@ -16,7 +16,7 @@
 The order in which files are loaded is undefined, but any order will result
 in the same outcome, given that order does not matter.
 
-<!-- CUE editor tab 1-->
+<!-- CUE editor -->
 _a.cue:_
 ```
 package config
@@ -25,7 +25,7 @@
 bar: int
 ```
 
-<!-- CUE editor tab 2-->
+<!-- CUE editor -->
 _b.cue:_
 ```
 package config
diff --git a/doc/tutorial/basics/rangedef.md b/doc/tutorial/basics/rangedef.md
index 4c49c6a..d0939c3 100644
--- a/doc/tutorial/basics/rangedef.md
+++ b/doc/tutorial/basics/rangedef.md
@@ -43,7 +43,7 @@
 <!-- result -->
 `$ cue eval -i range.cue`
 ```
-a: _|_
+a: _|_ /* -1 not within bound int & >=0 */
 b: 128
 c: 2000000000
 ```
\ No newline at end of file
diff --git a/doc/tutorial/basics/ranges.md b/doc/tutorial/basics/ranges.md
index 5f8fcc8..e7677fe 100644
--- a/doc/tutorial/basics/ranges.md
+++ b/doc/tutorial/basics/ranges.md
@@ -35,9 +35,9 @@
 `$ cue eval -i bounds.cue`
 ```
 a:  3.5
-b:  _|_
-c:  3.0
+b:  _|_ /* unsupported op &(int, float) */
+c:  3
 d:  "ma"
-e:  _|_
+e:  _|_ /* "mu" not within bound <"mo" */
 r1: >=5 & <8
 ```
diff --git a/doc/tutorial/basics/regexp.md b/doc/tutorial/basics/regexp.md
index 1b472f6..504f40d 100644
--- a/doc/tutorial/basics/regexp.md
+++ b/doc/tutorial/basics/regexp.md
@@ -33,9 +33,7 @@
 ```
 a: true
 b: true
-
-c: "^[a-z]{3}$"
-
+c: =~"^[a-z]{3}$"
 d: "foo"
-e: _|_  // "foo bar" does not match =~"^[a-z]{3}$"
+e: _|_ /* "foo bar" does not match =~"^[a-z]{3}$" */
 ```
\ No newline at end of file
diff --git a/doc/tutorial/basics/tut_test.go b/doc/tutorial/basics/tut_test.go
new file mode 100644
index 0000000..38ac49e
--- /dev/null
+++ b/doc/tutorial/basics/tut_test.go
@@ -0,0 +1,89 @@
+// 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 basics
+
+import (
+	"bytes"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"cuelang.org/go/internal/cuetest"
+)
+
+func TestTutorial(t *testing.T) {
+	// t.Skip()
+
+	err := filepath.Walk(".", func(path string, f os.FileInfo, err error) error {
+		if strings.HasSuffix(path, ".md") {
+			t.Run(path, func(t *testing.T) { simulateFile(t, path) })
+		}
+		return nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func simulateFile(t *testing.T, path string) {
+	b, err := ioutil.ReadFile(path)
+	if err != nil {
+		t.Fatalf("failed to open file %q: %v", path, err)
+	}
+
+	dir, err := ioutil.TempDir("", "tutbasics")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(dir)
+
+	c := cuetest.NewChunker(t, b)
+
+	// collect files
+	for c.Find("<!-- CUE editor -->") {
+		if !c.Next("_", "_") {
+			continue
+		}
+		filename := strings.TrimRight(c.Text(), ":")
+
+		if !c.Next("```", "```") {
+			t.Fatalf("No body for filename %q in file %q", filename, path)
+		}
+		b := bytes.TrimSpace(c.Bytes())
+
+		ioutil.WriteFile(filepath.Join(dir, filename), b, 0644)
+	}
+
+	if !c.Find("<!-- result -->") {
+		return
+	}
+
+	if !c.Next("`$ ", "`") {
+		t.Fatalf("No command for result section in file %q", path)
+	}
+	command := c.Text()
+
+	if !c.Next("```", "```") {
+		t.Fatalf("No body for result section in file %q", path)
+	}
+	gold := c.Text()
+	if p := strings.Index(gold, "\n"); p > 0 {
+		gold = gold[p+1:]
+	}
+
+	cuetest.Run(t, dir, command, gold)
+}
diff --git a/doc/tutorial/basics/types.md b/doc/tutorial/basics/types.md
index aa8c870..466a0fd 100644
--- a/doc/tutorial/basics/types.md
+++ b/doc/tutorial/basics/types.md
@@ -39,8 +39,8 @@
 _types.cue:_
 ```
 point: {
-    x: float
-    y: float
+    x: number
+    y: number
 }
 
 xaxis: point
@@ -56,15 +56,15 @@
 `$ cue eval types.cue`
 ```
 point: {
-    x: float
-    y: float
+    x: number
+    y: number
 }
 xaxis: {
     x: 0
-    y: float
+    y: number
 }
 yaxis: {
-    x: float
+    x: number
     y: 0
 }
 origin: {
diff --git a/doc/tutorial/basics/unification.md b/doc/tutorial/basics/unification.md
index b28ecf8..e631768 100644
--- a/doc/tutorial/basics/unification.md
+++ b/doc/tutorial/basics/unification.md
@@ -30,11 +30,31 @@
 <!-- result -->
 `$ cue eval -i unification.cue`
 ```
-a: { x: 1, y: 2 }
-b: { y: 2, z: 3 }
-c: { x: 1, z: 4 }
-
-q: { x: 1, y: 2, z: _|_ }
-r: { x: 1, y: 2, z: _|_ }
-s: { x: 1, y: 2, z: _|_ }
+a: {
+    x: 1
+    y: 2
+}
+b: {
+    y: 2
+    z: 3
+}
+c: {
+    x: 1
+    z: 4
+}
+q: {
+    x: 1
+    y: 2
+    z: _|_ /* conflicting values: 3 != 4 */
+}
+r: {
+    x: 1
+    y: 2
+    z: _|_ /* conflicting values: 3 != 4 */
+}
+s: {
+    x: 1
+    y: 2
+    z: _|_ /* conflicting values: 4 != 3 */
+}
 ```
\ No newline at end of file
diff --git a/internal/cuetest/chunker.go b/internal/cuetest/chunker.go
new file mode 100644
index 0000000..edcbb1b
--- /dev/null
+++ b/internal/cuetest/chunker.go
@@ -0,0 +1,73 @@
+// 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 cuetest
+
+import (
+	"bytes"
+	"testing"
+)
+
+// A Chunker is used to find segments in text.
+type Chunker struct {
+	t *testing.T
+	b []byte
+	s []byte
+	p int
+}
+
+// NewChunker returns a new chunker.
+func NewChunker(t *testing.T, b []byte) *Chunker {
+	return &Chunker{t: t, b: b}
+}
+
+// Next finds the first occurrence from the current scan position of beg,
+// records the segment from that position until the first occurrence of end
+// and then updates the current position. It reports whether a segment enclosed
+// by beg and end can be found.
+func (c *Chunker) Next(beg, end string) bool {
+	if !c.Find(beg) {
+		return false
+	}
+	if !c.Find(end) {
+		c.t.Fatalf("quotes at position %d not terminated", c.p)
+	}
+	return true
+}
+
+// Text returns the text segment captured by the last call to Next or Find.
+func (c *Chunker) Text() string {
+	return string(c.s)
+}
+
+// Bytes returns the segment captured by the last call to Next or Find.
+func (c *Chunker) Bytes() []byte {
+	return c.s
+}
+
+// Find searches for key from the current position and sets the current segment
+// to the text from current position up till the key's position. If successful,
+// the position is updated to point directly after the occurrence of key.
+func (c *Chunker) Find(key string) bool {
+	p := bytes.Index(c.b, []byte(key))
+	if p == -1 {
+		c.s = c.b
+		return false
+	}
+	c.p += p + len(key)
+	b := c.b
+	c.s = b[:p]
+	c.b = b[p+len(key):]
+	return true
+}
diff --git a/internal/cuetest/sim.go b/internal/cuetest/sim.go
new file mode 100644
index 0000000..edd92aa
--- /dev/null
+++ b/internal/cuetest/sim.go
@@ -0,0 +1,85 @@
+// 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 cuetest
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"os"
+	"regexp"
+	"strings"
+	"testing"
+
+	"cuelang.org/go/cmd/cue/cmd"
+	"github.com/kylelemons/godebug/diff"
+)
+
+// 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) {
+	old, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err = os.Chdir(dir); err != nil {
+		t.Fatal(err)
+	}
+	defer func() { os.Chdir(old) }()
+
+	logf(t, "Executing command: %s", command)
+
+	command = strings.TrimSpace(command[4:])
+	args := splitArgs(t, command)
+	logf(t, "Args: %q", args)
+
+	buf := &bytes.Buffer{}
+	cmd, err := cmd.New(args)
+	cmd.SetOutput(buf)
+	if err = cmd.Run(context.Background()); err != nil {
+		logf(t, "Execution failed: %v", err)
+	}
+
+	pattern := fmt.Sprintf("//.*%s.*", regexp.QuoteMeta(dir))
+	re, err := regexp.Compile(pattern)
+	if err != nil {
+		t.Fatal(err)
+	}
+	got := re.ReplaceAllString(buf.String(), "")
+	got = strings.TrimSpace(got)
+
+	want := strings.TrimSpace(gold)
+	if got != want {
+		t.Errorf("files differ:\n%s", diff.Diff(want, got))
+	}
+}
+
+func logf(t *testing.T, format string, args ...interface{}) {
+	t.Logf(format, args...)
+}
+
+func splitArgs(t *testing.T, s string) (args []string) {
+	c := NewChunker(t, []byte(s))
+	for {
+		found := c.Find(" '")
+		args = append(args, strings.Split(c.Text(), " ")...)
+		if !found {
+			break
+		}
+		c.Next("", "' ")
+		args = append(args, c.Text())
+	}
+	return args
+}