c10779873b
P05 — Scheduler moves from daemon-side to CLI-side (R-001) with runtime-awareness. Scheduler (internal/scheduler/scheduler.go, REQ-083): - Pure Schedule(nodes, req) -> []Placement. Job=1 best-fit, Service=count replicas (anti-affinity default, colocation permitted), DaemonSet=1 per matching node. Score(node, req) = (FreeCPU*1000 + FreeMem); fits checks runtime compat (wasm->wasmtime, pve-vm/ct->proxmox), constraints (CEL AND), capacity. Affinity scoring (target + weight, anti-affinity for spreading). CEL evaluator (internal/scheduler/cel.go): - Hand-rolled recursive-descent (no CEL dep in go.mod). Subset: node.* attrs, literals, ==/!=/>=/<=/></>, in/not in, and/or/not, parens. Anything outside subset returns error (no silent wrong answer). Schedule treats eval errors as non-fit (node skipped). 23 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent. 89.5% coverage on internal/scheduler. ---ci--- project: orca phase: P05 milestone: v0.9 status: execute ---/ci---
211 lines
5.7 KiB
Go
211 lines
5.7 KiB
Go
package scheduler
|
|
|
|
import "testing"
|
|
|
|
func TestEvaluateConstraint_Equality(t *testing.T) {
|
|
node := NodeInfo{Hostname: "h-1", Kind: "linux", CPU: 4, Memory: 4096, Tags: []string{"web"}, Runtimes: []string{"process"}}
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
want bool
|
|
}{
|
|
{"hostname eq", `node.hostname == "h-1"`, true},
|
|
{"hostname ne", `node.hostname == "h-2"`, false},
|
|
{"kind eq", `node.kind == "linux"`, true},
|
|
{"kind ne", `node.kind == "proxmox"`, false},
|
|
{"cpus eq", `node.cpus == 4`, true},
|
|
{"memory eq", `node.memory == 4096`, true},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := EvaluateConstraint(c.expr, node)
|
|
if err != nil {
|
|
t.Errorf("%s: %v", c.name, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("%s: got %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateConstraint_Comparison(t *testing.T) {
|
|
node := NodeInfo{Hostname: "h", Kind: "linux", CPU: 4, Memory: 4096}
|
|
cases := []struct {
|
|
expr string
|
|
want bool
|
|
}{
|
|
{"node.cpus >= 2", true},
|
|
{"node.cpus >= 4", true},
|
|
{"node.cpus > 4", false},
|
|
{"node.cpus > 2", true},
|
|
{"node.cpus <= 4", true},
|
|
{"node.cpus < 2", false},
|
|
{"node.cpus != 8", true},
|
|
{"node.cpus == 8", false},
|
|
{"node.memory >= 2048", true},
|
|
{"node.memory < 1024", false},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := EvaluateConstraint(c.expr, node)
|
|
if err != nil {
|
|
t.Errorf("%q: %v", c.expr, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("%q: got %v, want %v", c.expr, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateConstraint_Membership(t *testing.T) {
|
|
node := NodeInfo{Tags: []string{"web", "log-shipper"}, Runtimes: []string{"process", "wasmtime"}}
|
|
cases := []struct {
|
|
expr string
|
|
want bool
|
|
}{
|
|
{`"web" in node.tags`, true},
|
|
{`"missing" in node.tags`, false},
|
|
{`"process" in node.runtimes`, true},
|
|
{`"podman" in node.runtimes`, false},
|
|
{`"log-shipper" not in node.tags`, false},
|
|
{`"missing" not in node.tags`, true},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := EvaluateConstraint(c.expr, node)
|
|
if err != nil {
|
|
t.Errorf("%q: %v", c.expr, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("%q: got %v, want %v", c.expr, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateConstraint_BooleanComposition(t *testing.T) {
|
|
node := NodeInfo{Kind: "linux", CPU: 4, Tags: []string{"web"}}
|
|
cases := []struct {
|
|
expr string
|
|
want bool
|
|
}{
|
|
{`node.kind == "linux" and node.cpus >= 2`, true},
|
|
{`node.kind == "proxmox" and node.cpus >= 2`, false},
|
|
{`node.kind == "linux" or node.kind == "proxmox"`, true},
|
|
{`node.kind == "proxmox" or node.kind == "linux"`, true},
|
|
{`not node.kind == "proxmox"`, true},
|
|
{`not node.kind == "linux"`, false},
|
|
{`(node.kind == "linux") and (node.cpus >= 2)`, true},
|
|
{`node.cpus >= 2 and not "blocked" in node.tags`, true},
|
|
{`node.kind == "linux" and node.cpus >= 2 and "web" in node.tags`, true},
|
|
{`node.kind == "linux" or node.kind == "proxmox" or node.cpus > 100`, true},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := EvaluateConstraint(c.expr, node)
|
|
if err != nil {
|
|
t.Errorf("%q: %v", c.expr, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("%q: got %v, want %v", c.expr, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateConstraint_BareIdentifiers(t *testing.T) {
|
|
// Bare identifiers resolve through the same attribute map as
|
|
// node.<field> (per PRD: constraints may use either form).
|
|
node := NodeInfo{Kind: "linux", CPU: 4}
|
|
got, err := EvaluateConstraint(`kind == "linux"`, node)
|
|
if err != nil {
|
|
t.Fatalf("bare kind: %v", err)
|
|
}
|
|
if !got {
|
|
t.Error("bare kind == linux: got false, want true")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateConstraint_TrueFalseLiterals(t *testing.T) {
|
|
node := NodeInfo{}
|
|
cases := []struct {
|
|
expr string
|
|
want bool
|
|
}{
|
|
{"true", true},
|
|
{"false", false},
|
|
{"not false", true},
|
|
{"not true", false},
|
|
{"true and true", true},
|
|
{"true and false", false},
|
|
{"false or true", true},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := EvaluateConstraint(c.expr, node)
|
|
if err != nil {
|
|
t.Errorf("%q: %v", c.expr, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("%q: got %v, want %v", c.expr, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateConstraint_Errors(t *testing.T) {
|
|
node := NodeInfo{Kind: "linux"}
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
}{
|
|
{"empty", ""},
|
|
{"unterminated string", `node.kind == "linux`},
|
|
{"unknown attribute", `node.bogus == 1`},
|
|
{"unknown bare attr", `bogus == 1`},
|
|
{"dotted on non-node", `host.kind == "linux"`},
|
|
{"bad operator", `node.cpus + 2`},
|
|
{"trailing input", `node.kind == "linux" garbage`},
|
|
{"unbalanced paren", `(node.kind == "linux"`},
|
|
{"missing rhs", `node.cpus >=`},
|
|
{"not without in", `"x" not node.tags`},
|
|
{"ordered compare on bool", `true < false`},
|
|
{"ordered compare on mismatched types", `node.kind > 2`},
|
|
{"in on non-list", `"x" in node.kind`},
|
|
}
|
|
for _, c := range cases {
|
|
_, err := EvaluateConstraint(c.expr, node)
|
|
if err == nil {
|
|
t.Errorf("%s: expected error for %q, got nil", c.name, c.expr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateAll(t *testing.T) {
|
|
node := NodeInfo{Kind: "linux", CPU: 4, Tags: []string{"web"}}
|
|
cases := []struct {
|
|
name string
|
|
constraints []string
|
|
want bool
|
|
}{
|
|
{"empty", nil, true},
|
|
{"all pass", []string{`node.kind == "linux"`, "node.cpus >= 2"}, true},
|
|
{"one fails", []string{`node.kind == "linux"`, "node.cpus >= 8"}, false},
|
|
{"all fail", []string{`node.kind == "proxmox"`, "node.cpus >= 8"}, false},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := EvaluateAll(c.constraints, node)
|
|
if err != nil {
|
|
t.Errorf("%s: %v", c.name, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("%s: got %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateAll_PropagatesError(t *testing.T) {
|
|
node := NodeInfo{}
|
|
if _, err := EvaluateAll([]string{"bogus == 1"}, node); err == nil {
|
|
t.Error("EvaluateAll: expected error for malformed constraint")
|
|
}
|
|
}
|