cuelang.org/go@v0.13.0/internal/core/adt/sched_test.go (about) 1 // Copyright 2023 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 adt 16 17 import ( 18 "fmt" 19 "strings" 20 "testing" 21 22 "cuelang.org/go/internal" 23 "cuelang.org/go/internal/cuetest" 24 ) 25 26 // These states are used for testing. Each has a suffix of their corresponding 27 // name in states.go. Debuggers, when resolving constants, will often only 28 // show the debug constants. Adding the suffix clarifies which states these 29 // correspond to in the main program. 30 // 31 // We could also use the states in the main program directly, but states may 32 // shift, so this way we ensure that we separate these concerns. 33 const ( 34 c1AllAncestorsProcessed condition = 1 << iota 35 c2ArcTypeKnown 36 c3ValueKnown 37 c4ScalarKnown 38 39 // autoFieldConjunctsKnown is a condition that is automatically set by the simulator. 40 autoFieldConjunctsKnown 41 ) 42 43 func TestStateNames(t *testing.T) { 44 if c1AllAncestorsProcessed != allAncestorsProcessed { 45 t.Error("inconsistent state name for allAncestorsProcessed") 46 } 47 if c2ArcTypeKnown != arcTypeKnown { 48 t.Error("inconsistent state name for arcTypeKnown") 49 } 50 if c3ValueKnown != valueKnown { 51 t.Error("inconsistent state name for valueKnown") 52 } 53 if c4ScalarKnown != scalarKnown { 54 t.Error("inconsistent state name for scalarKnown") 55 } 56 if autoFieldConjunctsKnown != fieldConjunctsKnown { 57 t.Error("inconsistent state name for fieldConjunctsKnown") 58 } 59 } 60 61 // TestScheduler tests the non-CUE specific scheduler functionality. 62 func TestScheduler(t *testing.T) { 63 ctx := &OpContext{ 64 Version: internal.EvalV2, 65 taskContext: taskContext{ 66 counterMask: c1AllAncestorsProcessed | c2ArcTypeKnown | c3ValueKnown | c4ScalarKnown, 67 complete: func(s *scheduler) condition { return 0 }, 68 }, 69 } 70 71 // shared state 72 nodeID := 0 73 w := &strings.Builder{} 74 nodes := []*nodeContext{} 75 76 node := func(parent *nodeContext) *nodeContext { 77 if nodeID == 0 { 78 if parent != nil { 79 t.Fatal("root node must be created first") 80 } 81 } else { 82 if parent == nil { 83 t.Fatal("non-root node must have parent") 84 } 85 } 86 87 n := &nodeContext{scheduler: scheduler{ctx: ctx}, refCount: nodeID} 88 nodeID++ 89 nodes = append(nodes, n) 90 return n 91 } 92 93 // dep encodes a dependency on a node uncovered while running a task. It 94 // corresponds to a single evaluation of a top-level expression within a 95 // task. 96 type dep struct { 97 node *nodeContext 98 needs condition 99 } 100 101 // process simulates the running of a task with the given dependencies on 102 // other nodes/ schedulers. 103 // 104 // Note that tasks indicate their dependencies at runtime, and that these 105 // are not statically declared at the time of task creation. This is because 106 // dependencies may only be known after evaluating some CUE. As a 107 // consequence, it may be possible for a tasks to be started before one of 108 // its dependencies is run. Blocking only occurs if there is a mutual 109 // dependency that cannot be resolved without first blocking the task and 110 // coming back to it later. 111 process := func(name string, t *task, deps ...dep) (ok bool) { 112 fmt.Fprintf(w, "\n\t\t running task %s", name) 113 ok = true 114 for _, d := range deps { 115 func() { 116 defer func() { 117 if x := recover(); x != nil { 118 fmt.Fprintf(w, "\n\t\t task %s waiting for v%d meeting %x", name, d.node.refCount, d.needs) 119 fmt.Fprint(w, ": BLOCKED") 120 panic(x) 121 } 122 }() 123 if !d.node.process(d.needs, yield) { 124 ok = false 125 } 126 }() 127 } 128 return ok 129 } 130 131 // success creates a task that will succeed. 132 success := func(name string, n *nodeContext, completes, needs condition, deps ...dep) *task { 133 t := &task{ 134 run: &runner{ 135 f: func(ctx *OpContext, t *task, mode runMode) { 136 process(name, t, deps...) 137 }, 138 completes: completes, 139 needs: needs, 140 }, 141 node: n, 142 x: &String{Str: name}, // Set name for debugging purposes. 143 } 144 n.insertTask(t) 145 return t 146 } 147 148 // signal is a task that unconditionally sets a completion bit. 149 signal := func(name string, n *nodeContext, completes condition, deps ...dep) *task { 150 t := &task{ 151 run: &runner{ 152 f: func(ctx *OpContext, t *task, mode runMode) { 153 if process(name, t, deps...) { 154 n.scheduler.signal(completes) 155 } 156 }, 157 completes: completes, 158 }, 159 node: n, 160 x: &String{Str: name}, // Set name for debugging purposes. 161 } 162 n.insertTask(t) 163 return t 164 } 165 166 // completes creates a task that completes some state in another node. 167 completes := func(name string, n, other *nodeContext, completes condition, deps ...dep) *task { 168 other.scheduler.incrementCounts(completes) 169 t := &task{ 170 run: &runner{ 171 f: func(ctx *OpContext, t *task, mode runMode) { 172 if process(name, t, deps...) { 173 other.scheduler.decrementCounts(completes) 174 } 175 }, 176 completes: completes, 177 }, 178 node: n, 179 x: &String{Str: name}, // Set name for debugging purposes. 180 } 181 n.insertTask(t) 182 return t 183 } 184 185 // fail creates a task that will fail. 186 fail := func(name string, n *nodeContext, completes, needs condition, deps ...dep) *task { 187 t := &task{ 188 189 run: &runner{ 190 f: func(ctx *OpContext, t *task, mode runMode) { 191 fmt.Fprintf(w, "\n\t\t running task %s:", name) 192 t.err = &Bottom{} 193 fmt.Fprint(w, " FAIL") 194 }, 195 completes: completes, 196 needs: needs, 197 }, 198 node: n, 199 x: &String{Str: name}, // Set name for debugging purposes. 200 } 201 n.insertTask(t) 202 return t 203 } 204 205 type testCase struct { 206 name string 207 init func() 208 209 log string // A lot 210 state string // A textual representation of the task state 211 } 212 213 cases := []testCase{{ 214 name: "empty scheduler", 215 init: func() { 216 node(nil) 217 }, 218 log: ``, 219 220 state: ` 221 v0 (SUCCESS):`, 222 }, { 223 name: "node with one task", 224 init: func() { 225 v0 := node(nil) 226 success("t1", v0, c1AllAncestorsProcessed, 0) 227 }, 228 log: ` 229 running task t1`, 230 231 state: ` 232 v0 (SUCCESS): 233 task: t1: SUCCESS`, 234 }, { 235 name: "node with two tasks", 236 init: func() { 237 v0 := node(nil) 238 success("t1", v0, c1AllAncestorsProcessed, 0) 239 success("t2", v0, c2ArcTypeKnown, 0) 240 }, 241 log: ` 242 running task t1 243 running task t2`, 244 245 state: ` 246 v0 (SUCCESS): 247 task: t1: SUCCESS 248 task: t2: SUCCESS`, 249 }, { 250 name: "node failing task", 251 init: func() { 252 v0 := node(nil) 253 fail("t1", v0, c1AllAncestorsProcessed, 0) 254 }, 255 log: ` 256 running task t1: FAIL`, 257 258 state: ` 259 v0 (SUCCESS): 260 task: t1: FAILED`, 261 }, { 262 // Tasks will have to be run in order according to their dependencies. 263 // Note that the tasks will be run in order, as they all depend on the 264 // same node, in which case the order must be and will be strictly 265 // enforced. 266 name: "dependency chain on nodes within scheduler", 267 init: func() { 268 v0 := node(nil) 269 success("third", v0, c3ValueKnown, c2ArcTypeKnown) 270 success("fourth", v0, c4ScalarKnown, c3ValueKnown) 271 success("second", v0, c2ArcTypeKnown, c1AllAncestorsProcessed) 272 success("first", v0, c1AllAncestorsProcessed, 0) 273 }, 274 log: ` 275 running task first 276 running task second 277 running task third 278 running task fourth`, 279 280 state: ` 281 v0 (SUCCESS): 282 task: third: SUCCESS 283 task: fourth: SUCCESS 284 task: second: SUCCESS 285 task: first: SUCCESS`, 286 }, { 287 // If a task depends on a state completion for which there is no task, 288 // it should be considered as completed, because essentially all 289 // information is known about that state. 290 name: "task depends on state for which there is no task", 291 init: func() { 292 v0 := node(nil) 293 success("t1", v0, c2ArcTypeKnown, c1AllAncestorsProcessed) 294 }, 295 log: ` 296 running task t1`, 297 state: ` 298 v0 (SUCCESS): 299 task: t1: SUCCESS`, 300 }, { 301 // Same as previous, but now for another node. 302 name: "task depends on state of other node for which there is no task", 303 init: func() { 304 v0 := node(nil) 305 v1 := node(v0) 306 v2 := node(v0) 307 success("t1", v1, c1AllAncestorsProcessed, 0, dep{node: v2, needs: c2ArcTypeKnown}) 308 }, 309 log: ` 310 running task t1`, 311 state: ` 312 v0 (SUCCESS): 313 v1 (SUCCESS): 314 task: t1: SUCCESS 315 v2 (SUCCESS):`, 316 }, { 317 name: "tasks depend on multiple other tasks within same scheduler", 318 init: func() { 319 v0 := node(nil) 320 success("before1", v0, c2ArcTypeKnown, 0) 321 success("last", v0, c4ScalarKnown, c1AllAncestorsProcessed|c2ArcTypeKnown|c3ValueKnown) 322 success("block", v0, c3ValueKnown, c1AllAncestorsProcessed|c2ArcTypeKnown) 323 success("before2", v0, c1AllAncestorsProcessed, 0) 324 }, 325 log: ` 326 running task before1 327 running task before2 328 running task block 329 running task last`, 330 331 state: ` 332 v0 (SUCCESS): 333 task: before1: SUCCESS 334 task: last: SUCCESS 335 task: block: SUCCESS 336 task: before2: SUCCESS`, 337 }, { 338 // In this test we simulate dynamic reference that are dependent 339 // on each other in a chain to form the fields. Task t0 would not be 340 // a task in the regular evaluator, but it is included there as a 341 // task in absence of the ability to simulate static elements. 342 // 343 // v0: { 344 // (v0.baz): "bar" // task t1 345 // (v0.foo): "baz" // task t2 346 // baz: "foo" // task t0 347 // } 348 // 349 name: "non-cyclic dependencies between nodes p1", 350 init: func() { 351 v0 := node(nil) 352 baz := node(v0) 353 success("t0", baz, c1AllAncestorsProcessed, 0) 354 foo := node(v0) 355 356 completes("t1:bar", v0, foo, c2ArcTypeKnown, dep{node: baz, needs: c1AllAncestorsProcessed}) 357 success("t2:baz", v0, c1AllAncestorsProcessed, 0, dep{node: foo, needs: c2ArcTypeKnown}) 358 }, 359 log: ` 360 running task t1:bar 361 running task t0 362 running task t2:baz`, 363 state: ` 364 v0 (SUCCESS): 365 task: t1:bar: SUCCESS 366 task: t2:baz: SUCCESS 367 v1 (SUCCESS): 368 task: t0: SUCCESS 369 v2 (SUCCESS):`, 370 }, { 371 // Like the previous test, but different order of execution. 372 // 373 // v0: { 374 // (v0.foo): "baz" // task t2 375 // (v0.baz): "bar" // task t1 376 // baz: "foo" // task t0 377 // } 378 // 379 name: "non-cyclic dependencies between nodes p2", 380 init: func() { 381 v0 := node(nil) 382 baz := node(v0) 383 success("foo", baz, c1AllAncestorsProcessed, 0) 384 foo := node(v0) 385 386 success("t2:baz", v0, c1AllAncestorsProcessed, 0, dep{node: foo, needs: c2ArcTypeKnown}) 387 completes("t1:bar", v0, foo, c2ArcTypeKnown, dep{node: baz, needs: c1AllAncestorsProcessed}) 388 }, 389 log: ` 390 running task t2:baz 391 running task t1:bar 392 running task foo`, 393 state: ` 394 v0 (SUCCESS): 395 task: t2:baz: SUCCESS 396 task: t1:bar: SUCCESS 397 v1 (SUCCESS): 398 task: foo: SUCCESS 399 v2 (SUCCESS):`, 400 }, { 401 // b: a - 10 402 // a: b + 10 403 name: "cycle in mutually referencing expressions", 404 init: func() { 405 v0 := node(nil) 406 v1 := node(v0) 407 v2 := node(v0) 408 success("a-10", v1, c1AllAncestorsProcessed|c2ArcTypeKnown, 0, dep{node: v2, needs: c1AllAncestorsProcessed}) 409 success("b+10", v2, c1AllAncestorsProcessed|c2ArcTypeKnown, 0, dep{node: v1, needs: c1AllAncestorsProcessed}) 410 }, 411 log: ` 412 running task a-10 413 running task b+10 414 task b+10 waiting for v1 meeting 1: BLOCKED 415 task a-10 waiting for v2 meeting 1: BLOCKED 416 running task b+10 417 running task a-10`, 418 state: ` 419 v0 (SUCCESS): 420 v1 (SUCCESS): (frozen) 421 task: a-10: SUCCESS (unblocked) 422 v2 (SUCCESS): (frozen) 423 task: b+10: SUCCESS (unblocked)`, 424 }, { 425 // b: a - 10 426 // a: b + 10 427 // a: 5 428 name: "broken cyclic reference in expressions", 429 init: func() { 430 v0 := node(nil) 431 v1 := node(v0) 432 v2 := node(v0) 433 success("a-10", v1, c1AllAncestorsProcessed|c2ArcTypeKnown, 0, dep{node: v2, needs: c1AllAncestorsProcessed}) 434 success("b+10", v2, c1AllAncestorsProcessed|c2ArcTypeKnown, 0, dep{node: v1, needs: c1AllAncestorsProcessed}) 435 436 // NOTE: using success("5", v2, c1, 0) here would cause the cyclic 437 // references to block, as they would both provide and depend on 438 // v1 and v2 becoming scalars. Once a field is known to be a scalar, 439 // it can safely be signaled as unification cannot make it more 440 // concrete. Further unification could result in an error, but that 441 // will be caught by completing the unification. 442 signal("5", v2, c1AllAncestorsProcessed) 443 }, 444 log: ` 445 running task a-10 446 running task b+10 447 task b+10 waiting for v1 meeting 1: BLOCKED 448 running task 5 449 running task b+10`, 450 state: ` 451 v0 (SUCCESS): 452 v1 (SUCCESS): 453 task: a-10: SUCCESS 454 v2 (SUCCESS): 455 task: b+10: SUCCESS 456 task: 5: SUCCESS`, 457 }, { 458 // This test simulates a case where a comprehension projects 459 // onto itself. The cycle is broken by allowing a required state 460 // to be dropped upon detecting a cycle. For comprehensions, 461 // for instance, one usually would define that it provides fields in 462 // the vertex in which it is defined. However, for self-projections 463 // this results in a cycle. By dropping the requirement that all fields 464 // need be specified the cycle is broken. However, this means the 465 // comprehension may no longer add new fields to the vertex. 466 // 467 // x: { 468 // for k, v in x { 469 // (k): v 470 // } 471 // foo: 5 472 // } 473 name: "self cyclic", 474 init: func() { 475 x := node(nil) 476 foo := node(x) 477 success("5", foo, c1AllAncestorsProcessed, 0) 478 success("comprehension", x, c1AllAncestorsProcessed, 0, dep{node: x, needs: c1AllAncestorsProcessed}) 479 }, 480 log: ` 481 running task comprehension 482 task comprehension waiting for v0 meeting 1: BLOCKED 483 running task comprehension 484 running task 5`, 485 state: ` 486 v0 (SUCCESS): (frozen) 487 task: comprehension: SUCCESS (unblocked) 488 v1 (SUCCESS): 489 task: 5: SUCCESS`, 490 }, { 491 // This test simulates a case where comprehensions are not allowed to 492 // project on themselves. CUE allows this, but it is to test that 493 // similar constructions where this is not allowed do not cause 494 // infinite loops. 495 // 496 // x: { 497 // for k, v in x { 498 // (k+"X"): v 499 // } 500 // foo: 5 501 // } 502 // TODO: override freeze. 503 name: "self cyclic not allowed", 504 init: func() { 505 x := node(nil) 506 foo := node(x) 507 success("5", foo, c1AllAncestorsProcessed, 0) 508 success("comprehension", x, c1AllAncestorsProcessed, 0, dep{node: x, needs: c1AllAncestorsProcessed}) 509 }, 510 log: ` 511 running task comprehension 512 task comprehension waiting for v0 meeting 1: BLOCKED 513 running task comprehension 514 running task 5`, 515 state: ` 516 v0 (SUCCESS): (frozen) 517 task: comprehension: SUCCESS (unblocked) 518 v1 (SUCCESS): 519 task: 5: SUCCESS`, 520 }, { 521 // This test simulates a case where comprehensions mutually project 522 // on each other. 523 // 524 // x: { 525 // for k, v in y { 526 // (k): v 527 // } 528 // } 529 // y: { 530 // for k, v in x { 531 // (k): v 532 // } 533 // } 534 name: "mutually cyclic projection", 535 init: func() { 536 v0 := node(nil) 537 x := node(v0) 538 y := node(v0) 539 540 success("comprehension", x, c1AllAncestorsProcessed, 0, dep{node: y, needs: c1AllAncestorsProcessed}) 541 success("comprehension", y, c1AllAncestorsProcessed, 0, dep{node: x, needs: c1AllAncestorsProcessed}) 542 543 }, 544 log: ` 545 running task comprehension 546 running task comprehension 547 task comprehension waiting for v1 meeting 1: BLOCKED 548 task comprehension waiting for v2 meeting 1: BLOCKED 549 running task comprehension 550 running task comprehension`, 551 state: ` 552 v0 (SUCCESS): 553 v1 (SUCCESS): (frozen) 554 task: comprehension: SUCCESS (unblocked) 555 v2 (SUCCESS): (frozen) 556 task: comprehension: SUCCESS (unblocked)`, 557 }, { 558 // This test simulates a case where comprehensions are not allowed to 559 // project on each other cyclicly. CUE allows this, but it is to test 560 // that similar constructions where this is not allowed do not cause 561 // infinite loops. 562 // 563 // x: { 564 // for k, v in y { 565 // (k): v 566 // } 567 // } 568 // y: { 569 // for k, v in x { 570 // (k): v 571 // } 572 // foo: 5 573 // } 574 name: "disallowed mutually cyclic projection", 575 init: func() { 576 v0 := node(nil) 577 x := node(v0) 578 y := node(v0) 579 foo := node(y) 580 success("5", foo, c1AllAncestorsProcessed, 0) 581 582 success("comprehension", x, c1AllAncestorsProcessed, 0, dep{node: y, needs: c1AllAncestorsProcessed}) 583 success("comprehension", y, c1AllAncestorsProcessed, 0, dep{node: x, needs: c1AllAncestorsProcessed}) 584 585 }, 586 log: ` 587 running task comprehension 588 running task comprehension 589 task comprehension waiting for v1 meeting 1: BLOCKED 590 task comprehension waiting for v2 meeting 1: BLOCKED 591 running task comprehension 592 running task comprehension 593 running task 5`, 594 state: ` 595 v0 (SUCCESS): 596 v1 (SUCCESS): (frozen) 597 task: comprehension: SUCCESS (unblocked) 598 v2 (SUCCESS): (frozen) 599 task: comprehension: SUCCESS (unblocked) 600 v3 (SUCCESS): 601 task: 5: SUCCESS`, 602 }} 603 604 cuetest.Run(t, cases, func(t *cuetest.T, tc *testCase) { 605 // t.Update(true) 606 // t.Select("non-cyclic_dependencies_between_nodes_p2") 607 608 nodeID = 0 609 nodes = nodes[:0] 610 w.Reset() 611 612 // Create and run root scheduler. 613 tc.init() 614 for _, n := range nodes { 615 n.provided |= autoFieldConjunctsKnown 616 n.signalDoneAdding() 617 } 618 for _, n := range nodes { 619 n.finalize(autoFieldConjunctsKnown) 620 } 621 622 t.Equal(w.String(), tc.log) 623 624 w := &strings.Builder{} 625 for _, n := range nodes { 626 fmt.Fprintf(w, "\n\t\t\tv%d (%v):", n.refCount, n.state) 627 if n.scheduler.isFrozen { 628 fmt.Fprint(w, " (frozen)") 629 } 630 for _, t := range n.tasks { 631 fmt.Fprintf(w, "\n\t\t\t task: %s: %v", t.x.(*String).Str, t.state) 632 if t.unblocked { 633 fmt.Fprint(w, " (unblocked)") 634 } 635 } 636 for _, t := range n.blocking { 637 if t.blockedOn != nil { 638 fmt.Fprintf(w, "\n\t\t\t blocked: %s: %v", t.x.(*String).Str, t.state) 639 } 640 } 641 } 642 643 t.Equal(w.String(), tc.state) 644 }) 645 }