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 }