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. (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") } }