github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/tools/flow/flow_test.go (about)

     1  // Copyright 2020 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package flow_test
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"path"
    22  	"strings"
    23  	"sync"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/joomcode/cue/cue"
    28  	"github.com/joomcode/cue/cue/cuecontext"
    29  	"github.com/joomcode/cue/cue/errors"
    30  	"github.com/joomcode/cue/cue/format"
    31  	"github.com/joomcode/cue/internal/cuetest"
    32  	"github.com/joomcode/cue/internal/cuetxtar"
    33  	"github.com/joomcode/cue/tools/flow"
    34  )
    35  
    36  // TestTasks tests the logic that determines which nodes are tasks and what are
    37  // their dependencies.
    38  func TestFlow(t *testing.T) {
    39  	test := cuetxtar.TxTarTest{
    40  		Root:   "./testdata",
    41  		Name:   "run",
    42  		Update: cuetest.UpdateGoldenFiles,
    43  	}
    44  
    45  	test.Run(t, func(t *cuetxtar.Test) {
    46  		a := t.ValidInstances()
    47  
    48  		insts, err := cuecontext.New().BuildInstances(a)
    49  		if err != nil {
    50  			t.Fatal(err)
    51  		}
    52  		v := insts[0]
    53  
    54  		seqNum = 0
    55  
    56  		updateFunc := func(c *flow.Controller, task *flow.Task) error {
    57  			str := mermaidGraph(c)
    58  			step := fmt.Sprintf("t%d", seqNum)
    59  			fmt.Fprintln(t.Writer(step), str)
    60  
    61  			if task != nil {
    62  				n := task.Value().Syntax(cue.Final())
    63  				b, err := format.Node(n)
    64  				if err != nil {
    65  					t.Fatal(err)
    66  				}
    67  				fmt.Fprintln(t.Writer(path.Join(step, "value")), string(b))
    68  			}
    69  
    70  			incSeqNum()
    71  
    72  			return nil
    73  		}
    74  
    75  		cfg := &flow.Config{
    76  			Root:            cue.ParsePath("root"),
    77  			InferTasks:      t.Bool("InferTasks"),
    78  			IgnoreConcrete:  t.Bool("IgnoreConcrete"),
    79  			FindHiddenTasks: t.Bool("FindHiddenTasks"),
    80  			UpdateFunc:      updateFunc,
    81  		}
    82  
    83  		c := flow.New(cfg, v, taskFunc)
    84  
    85  		w := t.Writer("errors")
    86  		if err := c.Run(context.Background()); err != nil {
    87  			cwd, _ := os.Getwd()
    88  			fmt.Fprint(w, "error: ")
    89  			errors.Print(w, err, &errors.Config{
    90  				Cwd:     cwd,
    91  				ToSlash: true,
    92  			})
    93  		}
    94  	})
    95  }
    96  
    97  func TestFlowValuePanic(t *testing.T) {
    98  	f := `
    99      root: {
   100          a: {
   101              $id: "slow"
   102              out: string
   103          }
   104          b: {
   105              $id:    "slow"
   106              $after: a
   107              out:    string
   108          }
   109      }
   110      `
   111  	ctx := cuecontext.New()
   112  	v := ctx.CompileString(f)
   113  
   114  	ch := make(chan bool, 1)
   115  
   116  	cfg := &flow.Config{
   117  		Root: cue.ParsePath("root"),
   118  		UpdateFunc: func(c *flow.Controller, t *flow.Task) error {
   119  			ch <- true
   120  			return nil
   121  		},
   122  	}
   123  
   124  	c := flow.New(cfg, v, taskFunc)
   125  
   126  	defer func() { recover() }()
   127  
   128  	go c.Run(context.TODO())
   129  
   130  	// Call Value amidst two task runs. This should trigger a panic as the flow
   131  	// is not terminated.
   132  	<-ch
   133  	c.Value()
   134  	<-ch
   135  
   136  	t.Errorf("Value() did not panic")
   137  }
   138  
   139  func taskFunc(v cue.Value) (flow.Runner, error) {
   140  	switch name, err := v.Lookup("$id").String(); name {
   141  	default:
   142  		if err == nil {
   143  			return flow.RunnerFunc(func(t *flow.Task) error {
   144  				t.Fill(map[string]string{"stdout": "foo"})
   145  				return nil
   146  			}), nil
   147  		}
   148  		if err != nil && v.Lookup("$id").Exists() {
   149  			return nil, err
   150  		}
   151  
   152  	case "valToOut":
   153  		return flow.RunnerFunc(func(t *flow.Task) error {
   154  			if str, err := t.Value().Lookup("val").String(); err == nil {
   155  				t.Fill(map[string]string{"out": str})
   156  			}
   157  			return nil
   158  		}), nil
   159  
   160  	case "failure":
   161  		return flow.RunnerFunc(func(t *flow.Task) error {
   162  			return errors.New("failure")
   163  		}), nil
   164  
   165  	case "abort":
   166  		return flow.RunnerFunc(func(t *flow.Task) error {
   167  			return flow.ErrAbort
   168  		}), nil
   169  
   170  	case "list":
   171  		return flow.RunnerFunc(func(t *flow.Task) error {
   172  			t.Fill(map[string][]int{"out": {1, 2}})
   173  			return nil
   174  		}), nil
   175  
   176  	case "slow":
   177  		return flow.RunnerFunc(func(t *flow.Task) error {
   178  			time.Sleep(10 * time.Millisecond)
   179  			t.Fill(map[string]string{"out": "finished"})
   180  			return nil
   181  		}), nil
   182  
   183  	case "sequenced":
   184  		// This task is used to serialize different runners in case
   185  		// non-deterministic scheduling is possible.
   186  		return flow.RunnerFunc(func(t *flow.Task) error {
   187  			seq, err := t.Value().Lookup("seq").Int64()
   188  			if err != nil {
   189  				return err
   190  			}
   191  
   192  			waitSeqNum(seq)
   193  
   194  			if str, err := t.Value().Lookup("val").String(); err == nil {
   195  				t.Fill(map[string]string{"out": str})
   196  			}
   197  
   198  			return nil
   199  		}), nil
   200  	}
   201  	return nil, nil
   202  }
   203  
   204  // These vars are used to serialize tasks that are run in parallel. This allows
   205  // for testing running tasks in parallel, while obtaining deterministic output.
   206  var (
   207  	seqNum  int64
   208  	seqLock sync.Mutex
   209  	seqCond = sync.NewCond(&seqLock)
   210  )
   211  
   212  func incSeqNum() {
   213  	seqCond.L.Lock()
   214  	seqNum++
   215  	seqCond.Broadcast()
   216  	seqCond.L.Unlock()
   217  }
   218  
   219  func waitSeqNum(seq int64) {
   220  	seqCond.L.Lock()
   221  	for seq != seqNum {
   222  		seqCond.Wait()
   223  	}
   224  	seqCond.L.Unlock()
   225  }
   226  
   227  // mermaidGraph generates a mermaid graph of the current state. This can be
   228  // pasted into https://mermaid-js.github.io/mermaid-live-editor/ for
   229  // visualization.
   230  func mermaidGraph(c *flow.Controller) string {
   231  	w := &strings.Builder{}
   232  	fmt.Fprintln(w, "graph TD")
   233  	for i, t := range c.Tasks() {
   234  		fmt.Fprintf(w, "  t%d(\"%s [%s]\")\n", i, t.Path(), t.State())
   235  		for _, t := range t.Dependencies() {
   236  			fmt.Fprintf(w, "  t%d-->t%d\n", i, t.Index())
   237  		}
   238  	}
   239  	return w.String()
   240  }
   241  
   242  // DO NOT REMOVE: for testing purposes.
   243  func TestX(t *testing.T) {
   244  	in := `
   245  	`
   246  
   247  	if strings.TrimSpace(in) == "" {
   248  		t.Skip()
   249  	}
   250  
   251  	rt := cue.Runtime{}
   252  	inst, err := rt.Compile("", in)
   253  	if err != nil {
   254  		t.Fatal(err)
   255  	}
   256  
   257  	c := flow.New(&flow.Config{
   258  		Root: cue.ParsePath("root"),
   259  	}, inst, taskFunc)
   260  
   261  	t.Error(mermaidGraph(c))
   262  
   263  	if err := c.Run(context.Background()); err != nil {
   264  		t.Fatal(errors.Details(err, nil))
   265  	}
   266  }