github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/scheduler/feasible_test.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"reflect"
     5  	"testing"
     6  
     7  	"github.com/hashicorp/nomad/nomad/mock"
     8  	"github.com/hashicorp/nomad/nomad/structs"
     9  )
    10  
    11  func TestStaticIterator_Reset(t *testing.T) {
    12  	_, ctx := testContext(t)
    13  	var nodes []*structs.Node
    14  	for i := 0; i < 3; i++ {
    15  		nodes = append(nodes, mock.Node())
    16  	}
    17  	static := NewStaticIterator(ctx, nodes)
    18  
    19  	for i := 0; i < 6; i++ {
    20  		static.Reset()
    21  		for j := 0; j < i; j++ {
    22  			static.Next()
    23  		}
    24  		static.Reset()
    25  
    26  		out := collectFeasible(static)
    27  		if len(out) != len(nodes) {
    28  			t.Fatalf("out: %#v", out)
    29  			t.Fatalf("missing nodes %d %#v", i, static)
    30  		}
    31  
    32  		ids := make(map[string]struct{})
    33  		for _, o := range out {
    34  			if _, ok := ids[o.ID]; ok {
    35  				t.Fatalf("duplicate")
    36  			}
    37  			ids[o.ID] = struct{}{}
    38  		}
    39  	}
    40  }
    41  
    42  func TestStaticIterator_SetNodes(t *testing.T) {
    43  	_, ctx := testContext(t)
    44  	var nodes []*structs.Node
    45  	for i := 0; i < 3; i++ {
    46  		nodes = append(nodes, mock.Node())
    47  	}
    48  	static := NewStaticIterator(ctx, nodes)
    49  
    50  	newNodes := []*structs.Node{mock.Node()}
    51  	static.SetNodes(newNodes)
    52  
    53  	out := collectFeasible(static)
    54  	if !reflect.DeepEqual(out, newNodes) {
    55  		t.Fatalf("bad: %#v", out)
    56  	}
    57  }
    58  
    59  func TestRandomIterator(t *testing.T) {
    60  	_, ctx := testContext(t)
    61  	var nodes []*structs.Node
    62  	for i := 0; i < 10; i++ {
    63  		nodes = append(nodes, mock.Node())
    64  	}
    65  
    66  	nc := make([]*structs.Node, len(nodes))
    67  	copy(nc, nodes)
    68  	rand := NewRandomIterator(ctx, nc)
    69  
    70  	out := collectFeasible(rand)
    71  	if len(out) != len(nodes) {
    72  		t.Fatalf("missing nodes")
    73  	}
    74  	if reflect.DeepEqual(out, nodes) {
    75  		t.Fatalf("same order")
    76  	}
    77  }
    78  
    79  func TestDriverChecker(t *testing.T) {
    80  	_, ctx := testContext(t)
    81  	nodes := []*structs.Node{
    82  		mock.Node(),
    83  		mock.Node(),
    84  		mock.Node(),
    85  		mock.Node(),
    86  	}
    87  	nodes[0].Attributes["driver.foo"] = "1"
    88  	nodes[1].Attributes["driver.foo"] = "0"
    89  	nodes[2].Attributes["driver.foo"] = "true"
    90  	nodes[3].Attributes["driver.foo"] = "False"
    91  
    92  	drivers := map[string]struct{}{
    93  		"exec": struct{}{},
    94  		"foo":  struct{}{},
    95  	}
    96  	checker := NewDriverChecker(ctx, drivers)
    97  	cases := []struct {
    98  		Node   *structs.Node
    99  		Result bool
   100  	}{
   101  		{
   102  			Node:   nodes[0],
   103  			Result: true,
   104  		},
   105  		{
   106  			Node:   nodes[1],
   107  			Result: false,
   108  		},
   109  		{
   110  			Node:   nodes[2],
   111  			Result: true,
   112  		},
   113  		{
   114  			Node:   nodes[3],
   115  			Result: false,
   116  		},
   117  	}
   118  
   119  	for i, c := range cases {
   120  		if act := checker.Feasible(c.Node); act != c.Result {
   121  			t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result)
   122  		}
   123  	}
   124  }
   125  
   126  func TestConstraintChecker(t *testing.T) {
   127  	_, ctx := testContext(t)
   128  	nodes := []*structs.Node{
   129  		mock.Node(),
   130  		mock.Node(),
   131  		mock.Node(),
   132  		mock.Node(),
   133  	}
   134  
   135  	nodes[0].Attributes["kernel.name"] = "freebsd"
   136  	nodes[1].Datacenter = "dc2"
   137  	nodes[2].NodeClass = "large"
   138  
   139  	constraints := []*structs.Constraint{
   140  		&structs.Constraint{
   141  			Operand: "=",
   142  			LTarget: "${node.datacenter}",
   143  			RTarget: "dc1",
   144  		},
   145  		&structs.Constraint{
   146  			Operand: "is",
   147  			LTarget: "${attr.kernel.name}",
   148  			RTarget: "linux",
   149  		},
   150  		&structs.Constraint{
   151  			Operand: "is",
   152  			LTarget: "${node.class}",
   153  			RTarget: "large",
   154  		},
   155  	}
   156  	checker := NewConstraintChecker(ctx, constraints)
   157  	cases := []struct {
   158  		Node   *structs.Node
   159  		Result bool
   160  	}{
   161  		{
   162  			Node:   nodes[0],
   163  			Result: false,
   164  		},
   165  		{
   166  			Node:   nodes[1],
   167  			Result: false,
   168  		},
   169  		{
   170  			Node:   nodes[2],
   171  			Result: true,
   172  		},
   173  	}
   174  
   175  	for i, c := range cases {
   176  		if act := checker.Feasible(c.Node); act != c.Result {
   177  			t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result)
   178  		}
   179  	}
   180  }
   181  
   182  func TestResolveConstraintTarget(t *testing.T) {
   183  	type tcase struct {
   184  		target string
   185  		node   *structs.Node
   186  		val    interface{}
   187  		result bool
   188  	}
   189  	node := mock.Node()
   190  	cases := []tcase{
   191  		{
   192  			target: "${node.unique.id}",
   193  			node:   node,
   194  			val:    node.ID,
   195  			result: true,
   196  		},
   197  		{
   198  			target: "${node.datacenter}",
   199  			node:   node,
   200  			val:    node.Datacenter,
   201  			result: true,
   202  		},
   203  		{
   204  			target: "${node.unique.name}",
   205  			node:   node,
   206  			val:    node.Name,
   207  			result: true,
   208  		},
   209  		{
   210  			target: "${node.class}",
   211  			node:   node,
   212  			val:    node.NodeClass,
   213  			result: true,
   214  		},
   215  		{
   216  			target: "${node.foo}",
   217  			node:   node,
   218  			result: false,
   219  		},
   220  		{
   221  			target: "${attr.kernel.name}",
   222  			node:   node,
   223  			val:    node.Attributes["kernel.name"],
   224  			result: true,
   225  		},
   226  		{
   227  			target: "${attr.rand}",
   228  			node:   node,
   229  			result: false,
   230  		},
   231  		{
   232  			target: "${meta.pci-dss}",
   233  			node:   node,
   234  			val:    node.Meta["pci-dss"],
   235  			result: true,
   236  		},
   237  		{
   238  			target: "${meta.rand}",
   239  			node:   node,
   240  			result: false,
   241  		},
   242  	}
   243  
   244  	for _, tc := range cases {
   245  		res, ok := resolveConstraintTarget(tc.target, tc.node)
   246  		if ok != tc.result {
   247  			t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok)
   248  		}
   249  		if ok && !reflect.DeepEqual(res, tc.val) {
   250  			t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok)
   251  		}
   252  	}
   253  }
   254  
   255  func TestCheckConstraint(t *testing.T) {
   256  	type tcase struct {
   257  		op         string
   258  		lVal, rVal interface{}
   259  		result     bool
   260  	}
   261  	cases := []tcase{
   262  		{
   263  			op:   "=",
   264  			lVal: "foo", rVal: "foo",
   265  			result: true,
   266  		},
   267  		{
   268  			op:   "is",
   269  			lVal: "foo", rVal: "foo",
   270  			result: true,
   271  		},
   272  		{
   273  			op:   "==",
   274  			lVal: "foo", rVal: "foo",
   275  			result: true,
   276  		},
   277  		{
   278  			op:   "!=",
   279  			lVal: "foo", rVal: "foo",
   280  			result: false,
   281  		},
   282  		{
   283  			op:   "!=",
   284  			lVal: "foo", rVal: "bar",
   285  			result: true,
   286  		},
   287  		{
   288  			op:   "not",
   289  			lVal: "foo", rVal: "bar",
   290  			result: true,
   291  		},
   292  		{
   293  			op:   structs.ConstraintVersion,
   294  			lVal: "1.2.3", rVal: "~> 1.0",
   295  			result: true,
   296  		},
   297  		{
   298  			op:   structs.ConstraintRegex,
   299  			lVal: "foobarbaz", rVal: "[\\w]+",
   300  			result: true,
   301  		},
   302  		{
   303  			op:   "<",
   304  			lVal: "foo", rVal: "bar",
   305  			result: false,
   306  		},
   307  		{
   308  			op:   structs.ConstraintSetContains,
   309  			lVal: "foo,bar,baz", rVal: "foo,  bar  ",
   310  			result: true,
   311  		},
   312  		{
   313  			op:   structs.ConstraintSetContains,
   314  			lVal: "foo,bar,baz", rVal: "foo,bam",
   315  			result: false,
   316  		},
   317  	}
   318  
   319  	for _, tc := range cases {
   320  		_, ctx := testContext(t)
   321  		if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal); res != tc.result {
   322  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   323  		}
   324  	}
   325  }
   326  
   327  func TestCheckLexicalOrder(t *testing.T) {
   328  	type tcase struct {
   329  		op         string
   330  		lVal, rVal interface{}
   331  		result     bool
   332  	}
   333  	cases := []tcase{
   334  		{
   335  			op:   "<",
   336  			lVal: "bar", rVal: "foo",
   337  			result: true,
   338  		},
   339  		{
   340  			op:   "<=",
   341  			lVal: "foo", rVal: "foo",
   342  			result: true,
   343  		},
   344  		{
   345  			op:   ">",
   346  			lVal: "bar", rVal: "foo",
   347  			result: false,
   348  		},
   349  		{
   350  			op:   ">=",
   351  			lVal: "bar", rVal: "bar",
   352  			result: true,
   353  		},
   354  		{
   355  			op:   ">",
   356  			lVal: 1, rVal: "foo",
   357  			result: false,
   358  		},
   359  	}
   360  	for _, tc := range cases {
   361  		if res := checkLexicalOrder(tc.op, tc.lVal, tc.rVal); res != tc.result {
   362  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   363  		}
   364  	}
   365  }
   366  
   367  func TestCheckVersionConstraint(t *testing.T) {
   368  	type tcase struct {
   369  		lVal, rVal interface{}
   370  		result     bool
   371  	}
   372  	cases := []tcase{
   373  		{
   374  			lVal: "1.2.3", rVal: "~> 1.0",
   375  			result: true,
   376  		},
   377  		{
   378  			lVal: "1.2.3", rVal: ">= 1.0, < 1.4",
   379  			result: true,
   380  		},
   381  		{
   382  			lVal: "2.0.1", rVal: "~> 1.0",
   383  			result: false,
   384  		},
   385  		{
   386  			lVal: "1.4", rVal: ">= 1.0, < 1.4",
   387  			result: false,
   388  		},
   389  		{
   390  			lVal: 1, rVal: "~> 1.0",
   391  			result: true,
   392  		},
   393  	}
   394  	for _, tc := range cases {
   395  		_, ctx := testContext(t)
   396  		if res := checkVersionConstraint(ctx, tc.lVal, tc.rVal); res != tc.result {
   397  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   398  		}
   399  	}
   400  }
   401  
   402  func TestCheckRegexpConstraint(t *testing.T) {
   403  	type tcase struct {
   404  		lVal, rVal interface{}
   405  		result     bool
   406  	}
   407  	cases := []tcase{
   408  		{
   409  			lVal: "foobar", rVal: "bar",
   410  			result: true,
   411  		},
   412  		{
   413  			lVal: "foobar", rVal: "^foo",
   414  			result: true,
   415  		},
   416  		{
   417  			lVal: "foobar", rVal: "^bar",
   418  			result: false,
   419  		},
   420  		{
   421  			lVal: "zipzap", rVal: "foo",
   422  			result: false,
   423  		},
   424  		{
   425  			lVal: 1, rVal: "foo",
   426  			result: false,
   427  		},
   428  	}
   429  	for _, tc := range cases {
   430  		_, ctx := testContext(t)
   431  		if res := checkRegexpConstraint(ctx, tc.lVal, tc.rVal); res != tc.result {
   432  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   433  		}
   434  	}
   435  }
   436  
   437  func TestProposedAllocConstraint_JobDistinctHosts(t *testing.T) {
   438  	_, ctx := testContext(t)
   439  	nodes := []*structs.Node{
   440  		mock.Node(),
   441  		mock.Node(),
   442  		mock.Node(),
   443  		mock.Node(),
   444  	}
   445  	static := NewStaticIterator(ctx, nodes)
   446  
   447  	// Create a job with a distinct_hosts constraint and two task groups.
   448  	tg1 := &structs.TaskGroup{Name: "bar"}
   449  	tg2 := &structs.TaskGroup{Name: "baz"}
   450  
   451  	job := &structs.Job{
   452  		ID:          "foo",
   453  		Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
   454  		TaskGroups:  []*structs.TaskGroup{tg1, tg2},
   455  	}
   456  
   457  	propsed := NewProposedAllocConstraintIterator(ctx, static)
   458  	propsed.SetTaskGroup(tg1)
   459  	propsed.SetJob(job)
   460  
   461  	out := collectFeasible(propsed)
   462  	if len(out) != 4 {
   463  		t.Fatalf("Bad: %#v", out)
   464  	}
   465  
   466  	selected := make(map[string]struct{}, 4)
   467  	for _, option := range out {
   468  		if _, ok := selected[option.ID]; ok {
   469  			t.Fatalf("selected node %v for more than one alloc", option)
   470  		}
   471  		selected[option.ID] = struct{}{}
   472  	}
   473  }
   474  
   475  func TestProposedAllocConstraint_JobDistinctHosts_Infeasible(t *testing.T) {
   476  	_, ctx := testContext(t)
   477  	nodes := []*structs.Node{
   478  		mock.Node(),
   479  		mock.Node(),
   480  	}
   481  	static := NewStaticIterator(ctx, nodes)
   482  
   483  	// Create a job with a distinct_hosts constraint and two task groups.
   484  	tg1 := &structs.TaskGroup{Name: "bar"}
   485  	tg2 := &structs.TaskGroup{Name: "baz"}
   486  
   487  	job := &structs.Job{
   488  		ID:          "foo",
   489  		Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
   490  		TaskGroups:  []*structs.TaskGroup{tg1, tg2},
   491  	}
   492  
   493  	// Add allocs placing tg1 on node1 and tg2 on node2. This should make the
   494  	// job unsatisfiable.
   495  	plan := ctx.Plan()
   496  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   497  		&structs.Allocation{
   498  			TaskGroup: tg1.Name,
   499  			JobID:     job.ID,
   500  			ID:        structs.GenerateUUID(),
   501  		},
   502  
   503  		// Should be ignored as it is a different job.
   504  		&structs.Allocation{
   505  			TaskGroup: tg2.Name,
   506  			JobID:     "ignore 2",
   507  			ID:        structs.GenerateUUID(),
   508  		},
   509  	}
   510  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
   511  		&structs.Allocation{
   512  			TaskGroup: tg2.Name,
   513  			JobID:     job.ID,
   514  			ID:        structs.GenerateUUID(),
   515  		},
   516  
   517  		// Should be ignored as it is a different job.
   518  		&structs.Allocation{
   519  			TaskGroup: tg1.Name,
   520  			JobID:     "ignore 2",
   521  			ID:        structs.GenerateUUID(),
   522  		},
   523  	}
   524  
   525  	propsed := NewProposedAllocConstraintIterator(ctx, static)
   526  	propsed.SetTaskGroup(tg1)
   527  	propsed.SetJob(job)
   528  
   529  	out := collectFeasible(propsed)
   530  	if len(out) != 0 {
   531  		t.Fatalf("Bad: %#v", out)
   532  	}
   533  }
   534  
   535  func TestProposedAllocConstraint_JobDistinctHosts_InfeasibleCount(t *testing.T) {
   536  	_, ctx := testContext(t)
   537  	nodes := []*structs.Node{
   538  		mock.Node(),
   539  		mock.Node(),
   540  	}
   541  	static := NewStaticIterator(ctx, nodes)
   542  
   543  	// Create a job with a distinct_hosts constraint and three task groups.
   544  	tg1 := &structs.TaskGroup{Name: "bar"}
   545  	tg2 := &structs.TaskGroup{Name: "baz"}
   546  	tg3 := &structs.TaskGroup{Name: "bam"}
   547  
   548  	job := &structs.Job{
   549  		ID:          "foo",
   550  		Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
   551  		TaskGroups:  []*structs.TaskGroup{tg1, tg2, tg3},
   552  	}
   553  
   554  	propsed := NewProposedAllocConstraintIterator(ctx, static)
   555  	propsed.SetTaskGroup(tg1)
   556  	propsed.SetJob(job)
   557  
   558  	// It should not be able to place 3 tasks with only two nodes.
   559  	out := collectFeasible(propsed)
   560  	if len(out) != 2 {
   561  		t.Fatalf("Bad: %#v", out)
   562  	}
   563  }
   564  
   565  func TestProposedAllocConstraint_TaskGroupDistinctHosts(t *testing.T) {
   566  	_, ctx := testContext(t)
   567  	nodes := []*structs.Node{
   568  		mock.Node(),
   569  		mock.Node(),
   570  	}
   571  	static := NewStaticIterator(ctx, nodes)
   572  
   573  	// Create a task group with a distinct_hosts constraint.
   574  	taskGroup := &structs.TaskGroup{
   575  		Name: "example",
   576  		Constraints: []*structs.Constraint{
   577  			{Operand: structs.ConstraintDistinctHosts},
   578  		},
   579  	}
   580  
   581  	// Add a planned alloc to node1.
   582  	plan := ctx.Plan()
   583  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   584  		&structs.Allocation{
   585  			TaskGroup: taskGroup.Name,
   586  			JobID:     "foo",
   587  		},
   588  	}
   589  
   590  	// Add a planned alloc to node2 with the same task group name but a
   591  	// different job.
   592  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
   593  		&structs.Allocation{
   594  			TaskGroup: taskGroup.Name,
   595  			JobID:     "bar",
   596  		},
   597  	}
   598  
   599  	propsed := NewProposedAllocConstraintIterator(ctx, static)
   600  	propsed.SetTaskGroup(taskGroup)
   601  	propsed.SetJob(&structs.Job{ID: "foo"})
   602  
   603  	out := collectFeasible(propsed)
   604  	if len(out) != 1 {
   605  		t.Fatalf("Bad: %#v", out)
   606  	}
   607  
   608  	// Expect it to skip the first node as there is a previous alloc on it for
   609  	// the same task group.
   610  	if out[0] != nodes[1] {
   611  		t.Fatalf("Bad: %v", out)
   612  	}
   613  }
   614  
   615  func collectFeasible(iter FeasibleIterator) (out []*structs.Node) {
   616  	for {
   617  		next := iter.Next()
   618  		if next == nil {
   619  			break
   620  		}
   621  		out = append(out, next)
   622  	}
   623  	return
   624  }
   625  
   626  // mockFeasibilityChecker is a FeasibilityChecker that returns predetermined
   627  // feasibility values.
   628  type mockFeasibilityChecker struct {
   629  	retVals []bool
   630  	i       int
   631  }
   632  
   633  func newMockFeasiblityChecker(values ...bool) *mockFeasibilityChecker {
   634  	return &mockFeasibilityChecker{retVals: values}
   635  }
   636  
   637  func (c *mockFeasibilityChecker) Feasible(*structs.Node) bool {
   638  	if c.i >= len(c.retVals) {
   639  		c.i++
   640  		return false
   641  	}
   642  
   643  	f := c.retVals[c.i]
   644  	c.i++
   645  	return f
   646  }
   647  
   648  // calls returns how many times the checker was called.
   649  func (c *mockFeasibilityChecker) calls() int { return c.i }
   650  
   651  func TestFeasibilityWrapper_JobIneligible(t *testing.T) {
   652  	_, ctx := testContext(t)
   653  	nodes := []*structs.Node{mock.Node()}
   654  	static := NewStaticIterator(ctx, nodes)
   655  	mocked := newMockFeasiblityChecker(false)
   656  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil)
   657  
   658  	// Set the job to ineligible
   659  	ctx.Eligibility().SetJobEligibility(false, nodes[0].ComputedClass)
   660  
   661  	// Run the wrapper.
   662  	out := collectFeasible(wrapper)
   663  
   664  	if out != nil || mocked.calls() != 0 {
   665  		t.Fatalf("bad: %#v %d", out, mocked.calls())
   666  	}
   667  }
   668  
   669  func TestFeasibilityWrapper_JobEscapes(t *testing.T) {
   670  	_, ctx := testContext(t)
   671  	nodes := []*structs.Node{mock.Node()}
   672  	static := NewStaticIterator(ctx, nodes)
   673  	mocked := newMockFeasiblityChecker(false)
   674  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil)
   675  
   676  	// Set the job to escaped
   677  	cc := nodes[0].ComputedClass
   678  	ctx.Eligibility().job[cc] = EvalComputedClassEscaped
   679  
   680  	// Run the wrapper.
   681  	out := collectFeasible(wrapper)
   682  
   683  	if out != nil || mocked.calls() != 1 {
   684  		t.Fatalf("bad: %#v", out)
   685  	}
   686  
   687  	// Ensure that the job status didn't change from escaped even though the
   688  	// option failed.
   689  	if status := ctx.Eligibility().JobStatus(cc); status != EvalComputedClassEscaped {
   690  		t.Fatalf("job status is %v; want %v", status, EvalComputedClassEscaped)
   691  	}
   692  }
   693  
   694  func TestFeasibilityWrapper_JobAndTg_Eligible(t *testing.T) {
   695  	_, ctx := testContext(t)
   696  	nodes := []*structs.Node{mock.Node()}
   697  	static := NewStaticIterator(ctx, nodes)
   698  	jobMock := newMockFeasiblityChecker(true)
   699  	tgMock := newMockFeasiblityChecker(false)
   700  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
   701  
   702  	// Set the job to escaped
   703  	cc := nodes[0].ComputedClass
   704  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
   705  	ctx.Eligibility().SetTaskGroupEligibility(true, "foo", cc)
   706  	wrapper.SetTaskGroup("foo")
   707  
   708  	// Run the wrapper.
   709  	out := collectFeasible(wrapper)
   710  
   711  	if out == nil || tgMock.calls() != 0 {
   712  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
   713  	}
   714  }
   715  
   716  func TestFeasibilityWrapper_JobEligible_TgIneligible(t *testing.T) {
   717  	_, ctx := testContext(t)
   718  	nodes := []*structs.Node{mock.Node()}
   719  	static := NewStaticIterator(ctx, nodes)
   720  	jobMock := newMockFeasiblityChecker(true)
   721  	tgMock := newMockFeasiblityChecker(false)
   722  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
   723  
   724  	// Set the job to escaped
   725  	cc := nodes[0].ComputedClass
   726  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
   727  	ctx.Eligibility().SetTaskGroupEligibility(false, "foo", cc)
   728  	wrapper.SetTaskGroup("foo")
   729  
   730  	// Run the wrapper.
   731  	out := collectFeasible(wrapper)
   732  
   733  	if out != nil || tgMock.calls() != 0 {
   734  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
   735  	}
   736  }
   737  
   738  func TestFeasibilityWrapper_JobEligible_TgEscaped(t *testing.T) {
   739  	_, ctx := testContext(t)
   740  	nodes := []*structs.Node{mock.Node()}
   741  	static := NewStaticIterator(ctx, nodes)
   742  	jobMock := newMockFeasiblityChecker(true)
   743  	tgMock := newMockFeasiblityChecker(true)
   744  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
   745  
   746  	// Set the job to escaped
   747  	cc := nodes[0].ComputedClass
   748  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
   749  	ctx.Eligibility().taskGroups["foo"] =
   750  		map[string]ComputedClassFeasibility{cc: EvalComputedClassEscaped}
   751  	wrapper.SetTaskGroup("foo")
   752  
   753  	// Run the wrapper.
   754  	out := collectFeasible(wrapper)
   755  
   756  	if out == nil || tgMock.calls() != 1 {
   757  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
   758  	}
   759  
   760  	if e, ok := ctx.Eligibility().taskGroups["foo"][cc]; !ok || e != EvalComputedClassEscaped {
   761  		t.Fatalf("bad: %v %v", e, ok)
   762  	}
   763  }