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
+}