blob: c34d97c05fade247a0afcf677b6b4b83ab8237ac [file] [log] [blame]
// Copyright 2020 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 flow_test
import (
"context"
"fmt"
"os"
"path"
"strings"
"sync"
"testing"
"cuelang.org/go/cue"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/format"
"cuelang.org/go/internal/cuetest"
"cuelang.org/go/internal/cuetxtar"
"cuelang.org/go/tools/flow"
)
// TestTasks tests the logic that determines which nodes are tasks and what are
// their dependencies.
func TestFlow(t *testing.T) {
test := cuetxtar.TxTarTest{
Root: "./testdata",
Name: "run",
Update: cuetest.UpdateGoldenFiles,
}
test.Run(t, func(t *cuetxtar.Test) {
a := t.ValidInstances()
inst := cue.Build(a)[0]
if inst.Err != nil {
t.Fatal(inst.Err)
}
seqNum = 0
updateFunc := func(c *flow.Controller, task *flow.Task) error {
str := mermaidGraph(c)
step := fmt.Sprintf("t%d", seqNum)
fmt.Fprintln(t.Writer(step), str)
if task != nil {
n := task.Value().Syntax(cue.Final())
b, err := format.Node(n)
if err != nil {
t.Fatal(err)
}
fmt.Fprintln(t.Writer(path.Join(step, "value")), string(b))
}
incSeqNum()
return nil
}
cfg := &flow.Config{
Root: cue.ParsePath("root"),
InferTasks: t.Bool("InferTasks"),
IgnoreConcrete: t.Bool("IgnoreConcrete"),
UpdateFunc: updateFunc,
}
c := flow.New(cfg, inst, taskFunc)
w := t.Writer("errors")
if err := c.Run(context.Background()); err != nil {
cwd, _ := os.Getwd()
fmt.Fprint(w, "error: ")
errors.Print(w, err, &errors.Config{
Cwd: cwd,
ToSlash: true,
})
}
})
}
func taskFunc(v cue.Value) (flow.Runner, error) {
switch name, err := v.Lookup("$id").String(); name {
default:
if err == nil {
return flow.RunnerFunc(func(t *flow.Task) error {
t.Fill(map[string]string{"stdout": "foo"})
return nil
}), nil
}
if err != nil && v.Lookup("$id").Exists() {
return nil, err
}
case "valToOut":
return flow.RunnerFunc(func(t *flow.Task) error {
if str, err := t.Value().Lookup("val").String(); err == nil {
t.Fill(map[string]string{"out": str})
}
return nil
}), nil
case "failure":
return flow.RunnerFunc(func(t *flow.Task) error {
return errors.New("failure")
}), nil
case "abort":
return flow.RunnerFunc(func(t *flow.Task) error {
return flow.ErrAbort
}), nil
case "list":
return flow.RunnerFunc(func(t *flow.Task) error {
t.Fill(map[string][]int{"out": []int{1, 2}})
return nil
}), nil
case "sequenced":
// This task is used to serialize different runners in case
// non-deterministic scheduling is possible.
return flow.RunnerFunc(func(t *flow.Task) error {
seq, err := t.Value().Lookup("seq").Int64()
if err != nil {
return err
}
waitSeqNum(seq)
if str, err := t.Value().Lookup("val").String(); err == nil {
t.Fill(map[string]string{"out": str})
}
return nil
}), nil
}
return nil, nil
}
// These vars are used to serialize tasks that are run in parallel. This allows
// for testing running tasks in parallel, while obtaining deterministic output.
var (
seqNum int64
seqLock sync.Mutex
seqCond = sync.NewCond(&seqLock)
)
func incSeqNum() {
seqCond.L.Lock()
seqNum++
seqCond.Broadcast()
seqCond.L.Unlock()
}
func waitSeqNum(seq int64) {
seqCond.L.Lock()
for seq != seqNum {
seqCond.Wait()
}
seqCond.L.Unlock()
}
// mermaidGraph generates a mermaid graph of the current state. This can be
// pasted into https://mermaid-js.github.io/mermaid-live-editor/ for
// visualization.
func mermaidGraph(c *flow.Controller) string {
w := &strings.Builder{}
fmt.Fprintln(w, "graph TD")
for i, t := range c.Tasks() {
fmt.Fprintf(w, " t%d(\"%s [%s]\")\n", i, t.Path(), t.State())
for _, t := range t.Dependencies() {
fmt.Fprintf(w, " t%d-->t%d\n", i, t.Index())
}
}
return w.String()
}
// DO NOT REMOVE: for testing purposes.
func TestX(t *testing.T) {
in := `
`
if strings.TrimSpace(in) == "" {
t.Skip()
}
rt := cue.Runtime{}
inst, err := rt.Compile("", in)
if err != nil {
t.Fatal(err)
}
c := flow.New(&flow.Config{
Root: cue.ParsePath("root"),
}, inst, taskFunc)
t.Error(mermaidGraph(c))
if err := c.Run(context.Background()); err != nil {
t.Fatal(errors.Details(err, nil))
}
}