cuelang.org/go@v0.10.1/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  	"cuelang.org/go/cue"
    28  	"cuelang.org/go/cue/cuecontext"
    29  	"cuelang.org/go/cue/errors"
    30  	"cuelang.org/go/cue/format"
    31  	"cuelang.org/go/cue/stats"
    32  	"cuelang.org/go/internal/cuetdtest"
    33  	"cuelang.org/go/internal/cuetxtar"
    34  	"cuelang.org/go/tools/flow"
    35  )
    36  
    37  // TestTasks tests the logic that determines which nodes are tasks and what are
    38  // their dependencies.
    39  func TestFlow(t *testing.T) {
    40  	test := cuetxtar.TxTarTest{
    41  		Root: "./testdata",
    42  		Name: "run",
    43  		// TODO(evalv3): the breaking tests causes the synchronization to go out
    44  		// of whack, causing the test to become flaky. We revert to the default
    45  		// evaluator for now. Switch back to SmallMatrix when the tests are
    46  		// fixed for the new evaluator.
    47  		Matrix: cuetdtest.SmallMatrix,
    48  	}
    49  
    50  	test.Run(t, func(t *cuetxtar.Test) {
    51  		v := t.Context().BuildInstance(t.Instance())
    52  		if err := v.Err(); err != nil {
    53  			t.Fatal(errors.Details(err, nil))
    54  		}
    55  
    56  		seqNum = 0
    57  
    58  		var tasksTotal stats.Counts
    59  
    60  		updateFunc := func(c *flow.Controller, task *flow.Task) error {
    61  			str := flow.MermaidGraph(c)
    62  			step := fmt.Sprintf("t%d", seqNum)
    63  			fmt.Fprintln(t.Writer(step), str)
    64  
    65  			if task != nil {
    66  				n := task.Value().Syntax(cue.Final())
    67  				b, err := format.Node(n)
    68  				if err != nil {
    69  					t.Fatal(err)
    70  				}
    71  				fmt.Fprintln(t.Writer(path.Join(step, "value")), string(b))
    72  
    73  				if t.M.IsDefault() {
    74  					stats := task.Stats()
    75  					tasksTotal.Add(stats)
    76  					fmt.Fprintln(t.Writer(path.Join(step, "stats")), &stats)
    77  				}
    78  			}
    79  
    80  			incSeqNum()
    81  
    82  			return nil
    83  		}
    84  
    85  		cfg := &flow.Config{
    86  			Root:            cue.ParsePath("root"),
    87  			InferTasks:      t.Bool("InferTasks"),
    88  			IgnoreConcrete:  t.Bool("IgnoreConcrete"),
    89  			FindHiddenTasks: t.Bool("FindHiddenTasks"),
    90  			UpdateFunc:      updateFunc,
    91  		}
    92  
    93  		c := flow.New(cfg, v, taskFunc)
    94  
    95  		w := t.Writer("errors")
    96  		if err := c.Run(context.Background()); err != nil {
    97  			cwd, _ := os.Getwd()
    98  			fmt.Fprint(w, "error: ")
    99  			errors.Print(w, err, &errors.Config{
   100  				Cwd:     cwd,
   101  				ToSlash: true,
   102  			})
   103  		}
   104  
   105  		if !t.M.IsDefault() {
   106  			return
   107  		}
   108  
   109  		totals := c.Stats()
   110  		if tasksTotal != zeroStats && totals != tasksTotal {
   111  			t.Errorf(diffMsg, tasksTotal, totals, tasksTotal.Since(totals))
   112  		}
   113  		fmt.Fprintln(t.Writer("stats/totals"), totals)
   114  	})
   115  }
   116  
   117  var zeroStats stats.Counts
   118  
   119  const diffMsg = `
   120  stats: task totals different from controller:
   121  task totals:
   122  %v
   123  
   124  controller totals:
   125  %v
   126  
   127  task totals - controller totals:
   128  %v`
   129  
   130  func TestFlowValuePanic(t *testing.T) {
   131  	f := `
   132      root: {
   133          a: {
   134              $id: "slow"
   135              out: string
   136          }
   137          b: {
   138              $id:    "slow"
   139              $after: a
   140              out:    string
   141          }
   142      }
   143      `
   144  	ctx := cuecontext.New()
   145  	v := ctx.CompileString(f)
   146  
   147  	ch := make(chan bool, 1)
   148  
   149  	cfg := &flow.Config{
   150  		Root: cue.ParsePath("root"),
   151  		UpdateFunc: func(c *flow.Controller, t *flow.Task) error {
   152  			ch <- true
   153  			return nil
   154  		},
   155  	}
   156  
   157  	c := flow.New(cfg, v, taskFunc)
   158  
   159  	defer func() { recover() }()
   160  
   161  	go c.Run(context.TODO())
   162  
   163  	// Call Value amidst two task runs. This should trigger a panic as the flow
   164  	// is not terminated.
   165  	<-ch
   166  	c.Value()
   167  	<-ch
   168  
   169  	t.Errorf("Value() did not panic")
   170  }
   171  
   172  func taskFunc(v cue.Value) (flow.Runner, error) {
   173  	idPath := cue.MakePath(cue.Str("$id"))
   174  	valPath := cue.MakePath(cue.Str("val"))
   175  
   176  	switch name, err := v.LookupPath(idPath).String(); name {
   177  	default:
   178  		if err == nil {
   179  			return flow.RunnerFunc(func(t *flow.Task) error {
   180  				t.Fill(map[string]string{"stdout": "foo"})
   181  				return nil
   182  			}), nil
   183  		} else if v.LookupPath(idPath).Exists() {
   184  			return nil, err
   185  		}
   186  
   187  	case "valToOut":
   188  		return flow.RunnerFunc(func(t *flow.Task) error {
   189  			if str, err := t.Value().LookupPath(valPath).String(); err == nil {
   190  				t.Fill(map[string]string{"out": str})
   191  			}
   192  			return nil
   193  		}), nil
   194  
   195  	case "failure":
   196  		return flow.RunnerFunc(func(t *flow.Task) error {
   197  			return errors.New("failure")
   198  		}), nil
   199  
   200  	case "abort":
   201  		return flow.RunnerFunc(func(t *flow.Task) error {
   202  			return flow.ErrAbort
   203  		}), nil
   204  
   205  	case "list":
   206  		return flow.RunnerFunc(func(t *flow.Task) error {
   207  			t.Fill(map[string][]int{"out": {1, 2}})
   208  			return nil
   209  		}), nil
   210  
   211  	case "slow":
   212  		return flow.RunnerFunc(func(t *flow.Task) error {
   213  			time.Sleep(10 * time.Millisecond)
   214  			t.Fill(map[string]string{"out": "finished"})
   215  			return nil
   216  		}), nil
   217  
   218  	case "sequenced":
   219  		// This task is used to serialize different runners in case
   220  		// non-deterministic scheduling is possible.
   221  		return flow.RunnerFunc(func(t *flow.Task) error {
   222  			seq, err := t.Value().LookupPath(cue.MakePath(cue.Str("seq"))).Int64()
   223  			if err != nil {
   224  				return err
   225  			}
   226  
   227  			waitSeqNum(seq)
   228  
   229  			if str, err := t.Value().LookupPath(valPath).String(); err == nil {
   230  				t.Fill(map[string]string{"out": str})
   231  			}
   232  
   233  			return nil
   234  		}), nil
   235  	}
   236  	return nil, nil
   237  }
   238  
   239  // These vars are used to serialize tasks that are run in parallel. This allows
   240  // for testing running tasks in parallel, while obtaining deterministic output.
   241  var (
   242  	seqNum  int64
   243  	seqLock sync.Mutex
   244  	seqCond = sync.NewCond(&seqLock)
   245  )
   246  
   247  func incSeqNum() {
   248  	seqCond.L.Lock()
   249  	seqNum++
   250  	seqCond.Broadcast()
   251  	seqCond.L.Unlock()
   252  }
   253  
   254  func waitSeqNum(seq int64) {
   255  	seqCond.L.Lock()
   256  	for seq != seqNum {
   257  		seqCond.Wait()
   258  	}
   259  	seqCond.L.Unlock()
   260  }
   261  
   262  // DO NOT REMOVE: for testing purposes.
   263  func TestX(t *testing.T) {
   264  	in := `
   265  	`
   266  
   267  	if strings.TrimSpace(in) == "" {
   268  		t.Skip()
   269  	}
   270  
   271  	rt := cuecontext.New()
   272  	v := rt.CompileString(in)
   273  	if err := v.Err(); err != nil {
   274  		t.Fatal(err)
   275  	}
   276  
   277  	c := flow.New(&flow.Config{
   278  		Root: cue.ParsePath("root"),
   279  		UpdateFunc: func(c *flow.Controller, ft *flow.Task) error {
   280  			if ft != nil {
   281  				t.Errorf("\nTASK:\n%s", ft.Stats())
   282  			}
   283  			return nil
   284  		},
   285  	}, v, taskFunc)
   286  
   287  	t.Error(flow.MermaidGraph(c))
   288  
   289  	if err := c.Run(context.Background()); err != nil {
   290  		t.Fatal(errors.Details(err, nil))
   291  	}
   292  
   293  	t.Errorf("\nCONTROLLER:\n%s", c.Stats())
   294  }