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 }