750 lines
16 KiB
Go
750 lines
16 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
)
|
|
|
|
func TestStaticIterator_Reset(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
var nodes []*structs.Node
|
|
for i := 0; i < 3; i++ {
|
|
nodes = append(nodes, mock.Node())
|
|
}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
|
|
for i := 0; i < 6; i++ {
|
|
static.Reset()
|
|
for j := 0; j < i; j++ {
|
|
static.Next()
|
|
}
|
|
static.Reset()
|
|
|
|
out := collectFeasible(static)
|
|
if len(out) != len(nodes) {
|
|
t.Fatalf("out: %#v", out)
|
|
t.Fatalf("missing nodes %d %#v", i, static)
|
|
}
|
|
|
|
ids := make(map[string]struct{})
|
|
for _, o := range out {
|
|
if _, ok := ids[o.ID]; ok {
|
|
t.Fatalf("duplicate")
|
|
}
|
|
ids[o.ID] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStaticIterator_SetNodes(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
var nodes []*structs.Node
|
|
for i := 0; i < 3; i++ {
|
|
nodes = append(nodes, mock.Node())
|
|
}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
|
|
newNodes := []*structs.Node{mock.Node()}
|
|
static.SetNodes(newNodes)
|
|
|
|
out := collectFeasible(static)
|
|
if !reflect.DeepEqual(out, newNodes) {
|
|
t.Fatalf("bad: %#v", out)
|
|
}
|
|
}
|
|
|
|
func TestRandomIterator(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
var nodes []*structs.Node
|
|
for i := 0; i < 10; i++ {
|
|
nodes = append(nodes, mock.Node())
|
|
}
|
|
|
|
nc := make([]*structs.Node, len(nodes))
|
|
copy(nc, nodes)
|
|
rand := NewRandomIterator(ctx, nc)
|
|
|
|
out := collectFeasible(rand)
|
|
if len(out) != len(nodes) {
|
|
t.Fatalf("missing nodes")
|
|
}
|
|
if reflect.DeepEqual(out, nodes) {
|
|
t.Fatalf("same order")
|
|
}
|
|
}
|
|
|
|
func TestDriverChecker(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{
|
|
mock.Node(),
|
|
mock.Node(),
|
|
mock.Node(),
|
|
mock.Node(),
|
|
}
|
|
nodes[0].Attributes["driver.foo"] = "1"
|
|
nodes[1].Attributes["driver.foo"] = "0"
|
|
nodes[2].Attributes["driver.foo"] = "true"
|
|
nodes[3].Attributes["driver.foo"] = "False"
|
|
|
|
drivers := map[string]struct{}{
|
|
"exec": struct{}{},
|
|
"foo": struct{}{},
|
|
}
|
|
checker := NewDriverChecker(ctx, drivers)
|
|
cases := []struct {
|
|
Node *structs.Node
|
|
Result bool
|
|
}{
|
|
{
|
|
Node: nodes[0],
|
|
Result: true,
|
|
},
|
|
{
|
|
Node: nodes[1],
|
|
Result: false,
|
|
},
|
|
{
|
|
Node: nodes[2],
|
|
Result: true,
|
|
},
|
|
{
|
|
Node: nodes[3],
|
|
Result: false,
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
if act := checker.Feasible(c.Node); act != c.Result {
|
|
t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConstraintChecker(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{
|
|
mock.Node(),
|
|
mock.Node(),
|
|
mock.Node(),
|
|
mock.Node(),
|
|
}
|
|
|
|
nodes[0].Attributes["kernel.name"] = "freebsd"
|
|
nodes[1].Datacenter = "dc2"
|
|
nodes[2].NodeClass = "large"
|
|
|
|
constraints := []*structs.Constraint{
|
|
&structs.Constraint{
|
|
Operand: "=",
|
|
LTarget: "$node.datacenter",
|
|
RTarget: "dc1",
|
|
},
|
|
&structs.Constraint{
|
|
Operand: "is",
|
|
LTarget: "$attr.kernel.name",
|
|
RTarget: "linux",
|
|
},
|
|
&structs.Constraint{
|
|
Operand: "is",
|
|
LTarget: "$node.class",
|
|
RTarget: "large",
|
|
},
|
|
}
|
|
checker := NewConstraintChecker(ctx, constraints)
|
|
cases := []struct {
|
|
Node *structs.Node
|
|
Result bool
|
|
}{
|
|
{
|
|
Node: nodes[0],
|
|
Result: false,
|
|
},
|
|
{
|
|
Node: nodes[1],
|
|
Result: false,
|
|
},
|
|
{
|
|
Node: nodes[2],
|
|
Result: true,
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
if act := checker.Feasible(c.Node); act != c.Result {
|
|
t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveConstraintTarget(t *testing.T) {
|
|
type tcase struct {
|
|
target string
|
|
node *structs.Node
|
|
val interface{}
|
|
result bool
|
|
}
|
|
node := mock.Node()
|
|
cases := []tcase{
|
|
{
|
|
target: "$node.unique.id",
|
|
node: node,
|
|
val: node.ID,
|
|
result: true,
|
|
},
|
|
{
|
|
target: "$node.datacenter",
|
|
node: node,
|
|
val: node.Datacenter,
|
|
result: true,
|
|
},
|
|
{
|
|
target: "$node.unique.name",
|
|
node: node,
|
|
val: node.Name,
|
|
result: true,
|
|
},
|
|
{
|
|
target: "$node.class",
|
|
node: node,
|
|
val: node.NodeClass,
|
|
result: true,
|
|
},
|
|
{
|
|
target: "$node.foo",
|
|
node: node,
|
|
result: false,
|
|
},
|
|
{
|
|
target: "$attr.kernel.name",
|
|
node: node,
|
|
val: node.Attributes["kernel.name"],
|
|
result: true,
|
|
},
|
|
{
|
|
target: "$attr.rand",
|
|
node: node,
|
|
result: false,
|
|
},
|
|
{
|
|
target: "$meta.pci-dss",
|
|
node: node,
|
|
val: node.Meta["pci-dss"],
|
|
result: true,
|
|
},
|
|
{
|
|
target: "$meta.rand",
|
|
node: node,
|
|
result: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
res, ok := resolveConstraintTarget(tc.target, tc.node)
|
|
if ok != tc.result {
|
|
t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok)
|
|
}
|
|
if ok && !reflect.DeepEqual(res, tc.val) {
|
|
t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckConstraint(t *testing.T) {
|
|
type tcase struct {
|
|
op string
|
|
lVal, rVal interface{}
|
|
result bool
|
|
}
|
|
cases := []tcase{
|
|
{
|
|
op: "=",
|
|
lVal: "foo", rVal: "foo",
|
|
result: true,
|
|
},
|
|
{
|
|
op: "is",
|
|
lVal: "foo", rVal: "foo",
|
|
result: true,
|
|
},
|
|
{
|
|
op: "==",
|
|
lVal: "foo", rVal: "foo",
|
|
result: true,
|
|
},
|
|
{
|
|
op: "!=",
|
|
lVal: "foo", rVal: "foo",
|
|
result: false,
|
|
},
|
|
{
|
|
op: "!=",
|
|
lVal: "foo", rVal: "bar",
|
|
result: true,
|
|
},
|
|
{
|
|
op: "not",
|
|
lVal: "foo", rVal: "bar",
|
|
result: true,
|
|
},
|
|
{
|
|
op: structs.ConstraintVersion,
|
|
lVal: "1.2.3", rVal: "~> 1.0",
|
|
result: true,
|
|
},
|
|
{
|
|
op: structs.ConstraintRegex,
|
|
lVal: "foobarbaz", rVal: "[\\w]+",
|
|
result: true,
|
|
},
|
|
{
|
|
op: "<",
|
|
lVal: "foo", rVal: "bar",
|
|
result: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
_, ctx := testContext(t)
|
|
if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal); res != tc.result {
|
|
t.Fatalf("TC: %#v, Result: %v", tc, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckLexicalOrder(t *testing.T) {
|
|
type tcase struct {
|
|
op string
|
|
lVal, rVal interface{}
|
|
result bool
|
|
}
|
|
cases := []tcase{
|
|
{
|
|
op: "<",
|
|
lVal: "bar", rVal: "foo",
|
|
result: true,
|
|
},
|
|
{
|
|
op: "<=",
|
|
lVal: "foo", rVal: "foo",
|
|
result: true,
|
|
},
|
|
{
|
|
op: ">",
|
|
lVal: "bar", rVal: "foo",
|
|
result: false,
|
|
},
|
|
{
|
|
op: ">=",
|
|
lVal: "bar", rVal: "bar",
|
|
result: true,
|
|
},
|
|
{
|
|
op: ">",
|
|
lVal: 1, rVal: "foo",
|
|
result: false,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
if res := checkLexicalOrder(tc.op, tc.lVal, tc.rVal); res != tc.result {
|
|
t.Fatalf("TC: %#v, Result: %v", tc, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckVersionConstraint(t *testing.T) {
|
|
type tcase struct {
|
|
lVal, rVal interface{}
|
|
result bool
|
|
}
|
|
cases := []tcase{
|
|
{
|
|
lVal: "1.2.3", rVal: "~> 1.0",
|
|
result: true,
|
|
},
|
|
{
|
|
lVal: "1.2.3", rVal: ">= 1.0, < 1.4",
|
|
result: true,
|
|
},
|
|
{
|
|
lVal: "2.0.1", rVal: "~> 1.0",
|
|
result: false,
|
|
},
|
|
{
|
|
lVal: "1.4", rVal: ">= 1.0, < 1.4",
|
|
result: false,
|
|
},
|
|
{
|
|
lVal: 1, rVal: "~> 1.0",
|
|
result: true,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
_, ctx := testContext(t)
|
|
if res := checkVersionConstraint(ctx, tc.lVal, tc.rVal); res != tc.result {
|
|
t.Fatalf("TC: %#v, Result: %v", tc, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckRegexpConstraint(t *testing.T) {
|
|
type tcase struct {
|
|
lVal, rVal interface{}
|
|
result bool
|
|
}
|
|
cases := []tcase{
|
|
{
|
|
lVal: "foobar", rVal: "bar",
|
|
result: true,
|
|
},
|
|
{
|
|
lVal: "foobar", rVal: "^foo",
|
|
result: true,
|
|
},
|
|
{
|
|
lVal: "foobar", rVal: "^bar",
|
|
result: false,
|
|
},
|
|
{
|
|
lVal: "zipzap", rVal: "foo",
|
|
result: false,
|
|
},
|
|
{
|
|
lVal: 1, rVal: "foo",
|
|
result: false,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
_, ctx := testContext(t)
|
|
if res := checkRegexpConstraint(ctx, tc.lVal, tc.rVal); res != tc.result {
|
|
t.Fatalf("TC: %#v, Result: %v", tc, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProposedAllocConstraint_JobDistinctHosts(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{
|
|
mock.Node(),
|
|
mock.Node(),
|
|
mock.Node(),
|
|
mock.Node(),
|
|
}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
|
|
// Create a job with a distinct_hosts constraint and two task groups.
|
|
tg1 := &structs.TaskGroup{Name: "bar"}
|
|
tg2 := &structs.TaskGroup{Name: "baz"}
|
|
|
|
job := &structs.Job{
|
|
ID: "foo",
|
|
Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
|
|
TaskGroups: []*structs.TaskGroup{tg1, tg2},
|
|
}
|
|
|
|
propsed := NewProposedAllocConstraintIterator(ctx, static)
|
|
propsed.SetTaskGroup(tg1)
|
|
propsed.SetJob(job)
|
|
|
|
out := collectFeasible(propsed)
|
|
if len(out) != 4 {
|
|
t.Fatalf("Bad: %#v", out)
|
|
}
|
|
|
|
selected := make(map[string]struct{}, 4)
|
|
for _, option := range out {
|
|
if _, ok := selected[option.ID]; ok {
|
|
t.Fatalf("selected node %v for more than one alloc", option)
|
|
}
|
|
selected[option.ID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
func TestProposedAllocConstraint_JobDistinctHosts_Infeasible(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{
|
|
mock.Node(),
|
|
mock.Node(),
|
|
}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
|
|
// Create a job with a distinct_hosts constraint and two task groups.
|
|
tg1 := &structs.TaskGroup{Name: "bar"}
|
|
tg2 := &structs.TaskGroup{Name: "baz"}
|
|
|
|
job := &structs.Job{
|
|
ID: "foo",
|
|
Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
|
|
TaskGroups: []*structs.TaskGroup{tg1, tg2},
|
|
}
|
|
|
|
// Add allocs placing tg1 on node1 and tg2 on node2. This should make the
|
|
// job unsatisfiable.
|
|
plan := ctx.Plan()
|
|
plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
|
|
&structs.Allocation{
|
|
TaskGroup: tg1.Name,
|
|
JobID: job.ID,
|
|
},
|
|
|
|
// Should be ignored as it is a different job.
|
|
&structs.Allocation{
|
|
TaskGroup: tg2.Name,
|
|
JobID: "ignore 2",
|
|
},
|
|
}
|
|
plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
|
|
&structs.Allocation{
|
|
TaskGroup: tg2.Name,
|
|
JobID: job.ID,
|
|
},
|
|
|
|
// Should be ignored as it is a different job.
|
|
&structs.Allocation{
|
|
TaskGroup: tg1.Name,
|
|
JobID: "ignore 2",
|
|
},
|
|
}
|
|
|
|
propsed := NewProposedAllocConstraintIterator(ctx, static)
|
|
propsed.SetTaskGroup(tg1)
|
|
propsed.SetJob(job)
|
|
|
|
out := collectFeasible(propsed)
|
|
if len(out) != 0 {
|
|
t.Fatalf("Bad: %#v", out)
|
|
}
|
|
}
|
|
|
|
func TestProposedAllocConstraint_JobDistinctHosts_InfeasibleCount(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{
|
|
mock.Node(),
|
|
mock.Node(),
|
|
}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
|
|
// Create a job with a distinct_hosts constraint and three task groups.
|
|
tg1 := &structs.TaskGroup{Name: "bar"}
|
|
tg2 := &structs.TaskGroup{Name: "baz"}
|
|
tg3 := &structs.TaskGroup{Name: "bam"}
|
|
|
|
job := &structs.Job{
|
|
ID: "foo",
|
|
Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
|
|
TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3},
|
|
}
|
|
|
|
propsed := NewProposedAllocConstraintIterator(ctx, static)
|
|
propsed.SetTaskGroup(tg1)
|
|
propsed.SetJob(job)
|
|
|
|
// It should not be able to place 3 tasks with only two nodes.
|
|
out := collectFeasible(propsed)
|
|
if len(out) != 2 {
|
|
t.Fatalf("Bad: %#v", out)
|
|
}
|
|
}
|
|
|
|
func TestProposedAllocConstraint_TaskGroupDistinctHosts(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{
|
|
mock.Node(),
|
|
mock.Node(),
|
|
}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
|
|
// Create a task group with a distinct_hosts constraint.
|
|
taskGroup := &structs.TaskGroup{
|
|
Name: "example",
|
|
Constraints: []*structs.Constraint{
|
|
{Operand: structs.ConstraintDistinctHosts},
|
|
},
|
|
}
|
|
|
|
// Add a planned alloc to node1.
|
|
plan := ctx.Plan()
|
|
plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
|
|
&structs.Allocation{
|
|
TaskGroup: taskGroup.Name,
|
|
JobID: "foo",
|
|
},
|
|
}
|
|
|
|
// Add a planned alloc to node2 with the same task group name but a
|
|
// different job.
|
|
plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
|
|
&structs.Allocation{
|
|
TaskGroup: taskGroup.Name,
|
|
JobID: "bar",
|
|
},
|
|
}
|
|
|
|
propsed := NewProposedAllocConstraintIterator(ctx, static)
|
|
propsed.SetTaskGroup(taskGroup)
|
|
propsed.SetJob(&structs.Job{ID: "foo"})
|
|
|
|
out := collectFeasible(propsed)
|
|
if len(out) != 1 {
|
|
t.Fatalf("Bad: %#v", out)
|
|
}
|
|
|
|
// Expect it to skip the first node as there is a previous alloc on it for
|
|
// the same task group.
|
|
if out[0] != nodes[1] {
|
|
t.Fatalf("Bad: %v", out)
|
|
}
|
|
}
|
|
|
|
func collectFeasible(iter FeasibleIterator) (out []*structs.Node) {
|
|
for {
|
|
next := iter.Next()
|
|
if next == nil {
|
|
break
|
|
}
|
|
out = append(out, next)
|
|
}
|
|
return
|
|
}
|
|
|
|
// mockFeasibilityChecker is a FeasibilityChecker that returns predetermined
|
|
// feasibility values.
|
|
type mockFeasibilityChecker struct {
|
|
retVals []bool
|
|
i int
|
|
}
|
|
|
|
func newMockFeasiblityChecker(values ...bool) *mockFeasibilityChecker {
|
|
return &mockFeasibilityChecker{retVals: values}
|
|
}
|
|
|
|
func (c *mockFeasibilityChecker) Feasible(*structs.Node) bool {
|
|
if c.i >= len(c.retVals) {
|
|
c.i++
|
|
return false
|
|
}
|
|
|
|
f := c.retVals[c.i]
|
|
c.i++
|
|
return f
|
|
}
|
|
|
|
// calls returns how many times the checker was called.
|
|
func (c *mockFeasibilityChecker) calls() int { return c.i }
|
|
|
|
func TestFeasibilityWrapper_JobIneligible(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{mock.Node()}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
mocked := newMockFeasiblityChecker(false)
|
|
wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil)
|
|
|
|
// Set the job to ineligible
|
|
ctx.Eligibility().SetJobEligibility(false, nodes[0].ComputedClass)
|
|
|
|
// Run the wrapper.
|
|
out := collectFeasible(wrapper)
|
|
|
|
if out != nil || mocked.calls() != 0 {
|
|
t.Fatalf("bad: %#v %d", out, mocked.calls())
|
|
}
|
|
}
|
|
|
|
func TestFeasibilityWrapper_JobEscapes(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{mock.Node()}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
mocked := newMockFeasiblityChecker(false)
|
|
wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil)
|
|
|
|
// Set the job to escaped
|
|
cc := nodes[0].ComputedClass
|
|
ctx.Eligibility().job[cc] = EvalComputedClassEscaped
|
|
|
|
// Run the wrapper.
|
|
out := collectFeasible(wrapper)
|
|
|
|
if out != nil || mocked.calls() != 1 {
|
|
t.Fatalf("bad: %#v", out)
|
|
}
|
|
|
|
// Ensure that the job status didn't change from escaped even though the
|
|
// option failed.
|
|
if status := ctx.Eligibility().JobStatus(cc); status != EvalComputedClassEscaped {
|
|
t.Fatalf("job status is %v; want %v", status, EvalComputedClassEscaped)
|
|
}
|
|
}
|
|
|
|
func TestFeasibilityWrapper_JobAndTg_Eligible(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{mock.Node()}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
jobMock := newMockFeasiblityChecker(true)
|
|
tgMock := newMockFeasiblityChecker(false)
|
|
wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
|
|
|
|
// Set the job to escaped
|
|
cc := nodes[0].ComputedClass
|
|
ctx.Eligibility().job[cc] = EvalComputedClassEligible
|
|
ctx.Eligibility().SetTaskGroupEligibility(true, "foo", cc)
|
|
wrapper.SetTaskGroup("foo")
|
|
|
|
// Run the wrapper.
|
|
out := collectFeasible(wrapper)
|
|
|
|
if out == nil || tgMock.calls() != 0 {
|
|
t.Fatalf("bad: %#v %v", out, tgMock.calls())
|
|
}
|
|
}
|
|
|
|
func TestFeasibilityWrapper_JobEligible_TgIneligible(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{mock.Node()}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
jobMock := newMockFeasiblityChecker(true)
|
|
tgMock := newMockFeasiblityChecker(false)
|
|
wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
|
|
|
|
// Set the job to escaped
|
|
cc := nodes[0].ComputedClass
|
|
ctx.Eligibility().job[cc] = EvalComputedClassEligible
|
|
ctx.Eligibility().SetTaskGroupEligibility(false, "foo", cc)
|
|
wrapper.SetTaskGroup("foo")
|
|
|
|
// Run the wrapper.
|
|
out := collectFeasible(wrapper)
|
|
|
|
if out != nil || tgMock.calls() != 0 {
|
|
t.Fatalf("bad: %#v %v", out, tgMock.calls())
|
|
}
|
|
}
|
|
|
|
func TestFeasibilityWrapper_JobEligible_TgEscaped(t *testing.T) {
|
|
_, ctx := testContext(t)
|
|
nodes := []*structs.Node{mock.Node()}
|
|
static := NewStaticIterator(ctx, nodes)
|
|
jobMock := newMockFeasiblityChecker(true)
|
|
tgMock := newMockFeasiblityChecker(true)
|
|
wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
|
|
|
|
// Set the job to escaped
|
|
cc := nodes[0].ComputedClass
|
|
ctx.Eligibility().job[cc] = EvalComputedClassEligible
|
|
ctx.Eligibility().taskGroups["foo"] =
|
|
map[uint64]ComputedClassFeasibility{cc: EvalComputedClassEscaped}
|
|
wrapper.SetTaskGroup("foo")
|
|
|
|
// Run the wrapper.
|
|
out := collectFeasible(wrapper)
|
|
|
|
if out == nil || tgMock.calls() != 1 {
|
|
t.Fatalf("bad: %#v %v", out, tgMock.calls())
|
|
}
|
|
|
|
if e, ok := ctx.Eligibility().taskGroups["foo"][cc]; !ok || e != EvalComputedClassEscaped {
|
|
t.Fatalf("bad: %v %v", e, ok)
|
|
}
|
|
}
|