github.com/bigcommerce/nomad@v0.9.3-bc/scheduler/feasible_test.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/hashicorp/nomad/helper/uuid"
    10  	"github.com/hashicorp/nomad/nomad/mock"
    11  	"github.com/hashicorp/nomad/nomad/structs"
    12  	psstructs "github.com/hashicorp/nomad/plugins/shared/structs"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  func TestStaticIterator_Reset(t *testing.T) {
    17  	_, ctx := testContext(t)
    18  	var nodes []*structs.Node
    19  	for i := 0; i < 3; i++ {
    20  		nodes = append(nodes, mock.Node())
    21  	}
    22  	static := NewStaticIterator(ctx, nodes)
    23  
    24  	for i := 0; i < 6; i++ {
    25  		static.Reset()
    26  		for j := 0; j < i; j++ {
    27  			static.Next()
    28  		}
    29  		static.Reset()
    30  
    31  		out := collectFeasible(static)
    32  		if len(out) != len(nodes) {
    33  			t.Fatalf("out: %#v", out)
    34  			t.Fatalf("missing nodes %d %#v", i, static)
    35  		}
    36  
    37  		ids := make(map[string]struct{})
    38  		for _, o := range out {
    39  			if _, ok := ids[o.ID]; ok {
    40  				t.Fatalf("duplicate")
    41  			}
    42  			ids[o.ID] = struct{}{}
    43  		}
    44  	}
    45  }
    46  
    47  func TestStaticIterator_SetNodes(t *testing.T) {
    48  	_, ctx := testContext(t)
    49  	var nodes []*structs.Node
    50  	for i := 0; i < 3; i++ {
    51  		nodes = append(nodes, mock.Node())
    52  	}
    53  	static := NewStaticIterator(ctx, nodes)
    54  
    55  	newNodes := []*structs.Node{mock.Node()}
    56  	static.SetNodes(newNodes)
    57  
    58  	out := collectFeasible(static)
    59  	if !reflect.DeepEqual(out, newNodes) {
    60  		t.Fatalf("bad: %#v", out)
    61  	}
    62  }
    63  
    64  func TestRandomIterator(t *testing.T) {
    65  	_, ctx := testContext(t)
    66  	var nodes []*structs.Node
    67  	for i := 0; i < 10; i++ {
    68  		nodes = append(nodes, mock.Node())
    69  	}
    70  
    71  	nc := make([]*structs.Node, len(nodes))
    72  	copy(nc, nodes)
    73  	rand := NewRandomIterator(ctx, nc)
    74  
    75  	out := collectFeasible(rand)
    76  	if len(out) != len(nodes) {
    77  		t.Fatalf("missing nodes")
    78  	}
    79  	if reflect.DeepEqual(out, nodes) {
    80  		t.Fatalf("same order")
    81  	}
    82  }
    83  
    84  func TestDriverChecker(t *testing.T) {
    85  	_, ctx := testContext(t)
    86  	nodes := []*structs.Node{
    87  		mock.Node(),
    88  		mock.Node(),
    89  		mock.Node(),
    90  		mock.Node(),
    91  	}
    92  	nodes[0].Attributes["driver.foo"] = "1"
    93  	nodes[1].Attributes["driver.foo"] = "0"
    94  	nodes[2].Attributes["driver.foo"] = "true"
    95  	nodes[3].Attributes["driver.foo"] = "False"
    96  
    97  	drivers := map[string]struct{}{
    98  		"exec": {},
    99  		"foo":  {},
   100  	}
   101  	checker := NewDriverChecker(ctx, drivers)
   102  	cases := []struct {
   103  		Node   *structs.Node
   104  		Result bool
   105  	}{
   106  		{
   107  			Node:   nodes[0],
   108  			Result: true,
   109  		},
   110  		{
   111  			Node:   nodes[1],
   112  			Result: false,
   113  		},
   114  		{
   115  			Node:   nodes[2],
   116  			Result: true,
   117  		},
   118  		{
   119  			Node:   nodes[3],
   120  			Result: false,
   121  		},
   122  	}
   123  
   124  	for i, c := range cases {
   125  		if act := checker.Feasible(c.Node); act != c.Result {
   126  			t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result)
   127  		}
   128  	}
   129  }
   130  
   131  func Test_HealthChecks(t *testing.T) {
   132  	require := require.New(t)
   133  	_, ctx := testContext(t)
   134  
   135  	nodes := []*structs.Node{
   136  		mock.Node(),
   137  		mock.Node(),
   138  		mock.Node(),
   139  	}
   140  	for _, e := range nodes {
   141  		e.Drivers = make(map[string]*structs.DriverInfo)
   142  	}
   143  	nodes[0].Attributes["driver.foo"] = "1"
   144  	nodes[0].Drivers["foo"] = &structs.DriverInfo{
   145  		Detected:          true,
   146  		Healthy:           true,
   147  		HealthDescription: "running",
   148  		UpdateTime:        time.Now(),
   149  	}
   150  	nodes[1].Attributes["driver.bar"] = "1"
   151  	nodes[1].Drivers["bar"] = &structs.DriverInfo{
   152  		Detected:          true,
   153  		Healthy:           false,
   154  		HealthDescription: "not running",
   155  		UpdateTime:        time.Now(),
   156  	}
   157  	nodes[2].Attributes["driver.baz"] = "0"
   158  	nodes[2].Drivers["baz"] = &structs.DriverInfo{
   159  		Detected:          false,
   160  		Healthy:           false,
   161  		HealthDescription: "not running",
   162  		UpdateTime:        time.Now(),
   163  	}
   164  
   165  	testDrivers := []string{"foo", "bar", "baz"}
   166  	cases := []struct {
   167  		Node   *structs.Node
   168  		Result bool
   169  	}{
   170  		{
   171  			Node:   nodes[0],
   172  			Result: true,
   173  		},
   174  		{
   175  			Node:   nodes[1],
   176  			Result: false,
   177  		},
   178  		{
   179  			Node:   nodes[2],
   180  			Result: false,
   181  		},
   182  	}
   183  
   184  	for i, c := range cases {
   185  		drivers := map[string]struct{}{
   186  			testDrivers[i]: {},
   187  		}
   188  		checker := NewDriverChecker(ctx, drivers)
   189  		act := checker.Feasible(c.Node)
   190  		require.Equal(act, c.Result)
   191  	}
   192  }
   193  
   194  func TestConstraintChecker(t *testing.T) {
   195  	_, ctx := testContext(t)
   196  	nodes := []*structs.Node{
   197  		mock.Node(),
   198  		mock.Node(),
   199  		mock.Node(),
   200  	}
   201  
   202  	nodes[0].Attributes["kernel.name"] = "freebsd"
   203  	nodes[1].Datacenter = "dc2"
   204  	nodes[2].NodeClass = "large"
   205  	nodes[2].Attributes["foo"] = "bar"
   206  
   207  	constraints := []*structs.Constraint{
   208  		{
   209  			Operand: "=",
   210  			LTarget: "${node.datacenter}",
   211  			RTarget: "dc1",
   212  		},
   213  		{
   214  			Operand: "is",
   215  			LTarget: "${attr.kernel.name}",
   216  			RTarget: "linux",
   217  		},
   218  		{
   219  			Operand: "!=",
   220  			LTarget: "${node.class}",
   221  			RTarget: "linux-medium-pci",
   222  		},
   223  		{
   224  			Operand: "is_set",
   225  			LTarget: "${attr.foo}",
   226  		},
   227  	}
   228  	checker := NewConstraintChecker(ctx, constraints)
   229  	cases := []struct {
   230  		Node   *structs.Node
   231  		Result bool
   232  	}{
   233  		{
   234  			Node:   nodes[0],
   235  			Result: false,
   236  		},
   237  		{
   238  			Node:   nodes[1],
   239  			Result: false,
   240  		},
   241  		{
   242  			Node:   nodes[2],
   243  			Result: true,
   244  		},
   245  	}
   246  
   247  	for i, c := range cases {
   248  		if act := checker.Feasible(c.Node); act != c.Result {
   249  			t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result)
   250  		}
   251  	}
   252  }
   253  
   254  func TestResolveConstraintTarget(t *testing.T) {
   255  	type tcase struct {
   256  		target string
   257  		node   *structs.Node
   258  		val    interface{}
   259  		result bool
   260  	}
   261  	node := mock.Node()
   262  	cases := []tcase{
   263  		{
   264  			target: "${node.unique.id}",
   265  			node:   node,
   266  			val:    node.ID,
   267  			result: true,
   268  		},
   269  		{
   270  			target: "${node.datacenter}",
   271  			node:   node,
   272  			val:    node.Datacenter,
   273  			result: true,
   274  		},
   275  		{
   276  			target: "${node.unique.name}",
   277  			node:   node,
   278  			val:    node.Name,
   279  			result: true,
   280  		},
   281  		{
   282  			target: "${node.class}",
   283  			node:   node,
   284  			val:    node.NodeClass,
   285  			result: true,
   286  		},
   287  		{
   288  			target: "${node.foo}",
   289  			node:   node,
   290  			result: false,
   291  		},
   292  		{
   293  			target: "${attr.kernel.name}",
   294  			node:   node,
   295  			val:    node.Attributes["kernel.name"],
   296  			result: true,
   297  		},
   298  		{
   299  			target: "${attr.rand}",
   300  			node:   node,
   301  			val:    "",
   302  			result: false,
   303  		},
   304  		{
   305  			target: "${meta.pci-dss}",
   306  			node:   node,
   307  			val:    node.Meta["pci-dss"],
   308  			result: true,
   309  		},
   310  		{
   311  			target: "${meta.rand}",
   312  			node:   node,
   313  			val:    "",
   314  			result: false,
   315  		},
   316  	}
   317  
   318  	for _, tc := range cases {
   319  		res, ok := resolveTarget(tc.target, tc.node)
   320  		if ok != tc.result {
   321  			t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok)
   322  		}
   323  		if ok && !reflect.DeepEqual(res, tc.val) {
   324  			t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok)
   325  		}
   326  	}
   327  }
   328  
   329  func TestCheckConstraint(t *testing.T) {
   330  	type tcase struct {
   331  		op         string
   332  		lVal, rVal interface{}
   333  		result     bool
   334  	}
   335  	cases := []tcase{
   336  		{
   337  			op:   "=",
   338  			lVal: "foo", rVal: "foo",
   339  			result: true,
   340  		},
   341  		{
   342  			op:   "is",
   343  			lVal: "foo", rVal: "foo",
   344  			result: true,
   345  		},
   346  		{
   347  			op:   "==",
   348  			lVal: "foo", rVal: "foo",
   349  			result: true,
   350  		},
   351  		{
   352  			op:   "==",
   353  			lVal: "foo", rVal: nil,
   354  			result: false,
   355  		},
   356  		{
   357  			op:   "==",
   358  			lVal: nil, rVal: "foo",
   359  			result: false,
   360  		},
   361  		{
   362  			op:   "==",
   363  			lVal: nil, rVal: nil,
   364  			result: false,
   365  		},
   366  		{
   367  			op:   "!=",
   368  			lVal: "foo", rVal: "foo",
   369  			result: false,
   370  		},
   371  		{
   372  			op:   "!=",
   373  			lVal: "foo", rVal: "bar",
   374  			result: true,
   375  		},
   376  		{
   377  			op:   "!=",
   378  			lVal: nil, rVal: "foo",
   379  			result: true,
   380  		},
   381  		{
   382  			op:   "!=",
   383  			lVal: "foo", rVal: nil,
   384  			result: true,
   385  		},
   386  		{
   387  			op:   "!=",
   388  			lVal: nil, rVal: nil,
   389  			result: false,
   390  		},
   391  		{
   392  			op:   "not",
   393  			lVal: "foo", rVal: "bar",
   394  			result: true,
   395  		},
   396  		{
   397  			op:   structs.ConstraintVersion,
   398  			lVal: "1.2.3", rVal: "~> 1.0",
   399  			result: true,
   400  		},
   401  		{
   402  			op:   structs.ConstraintVersion,
   403  			lVal: nil, rVal: "~> 1.0",
   404  			result: false,
   405  		},
   406  		{
   407  			op:   structs.ConstraintRegex,
   408  			lVal: "foobarbaz", rVal: "[\\w]+",
   409  			result: true,
   410  		},
   411  		{
   412  			op:   structs.ConstraintRegex,
   413  			lVal: nil, rVal: "[\\w]+",
   414  			result: false,
   415  		},
   416  		{
   417  			op:   "<",
   418  			lVal: "foo", rVal: "bar",
   419  			result: false,
   420  		},
   421  		{
   422  			op:   "<",
   423  			lVal: nil, rVal: "bar",
   424  			result: false,
   425  		},
   426  		{
   427  			op:   structs.ConstraintSetContains,
   428  			lVal: "foo,bar,baz", rVal: "foo,  bar  ",
   429  			result: true,
   430  		},
   431  		{
   432  			op:   structs.ConstraintSetContains,
   433  			lVal: "foo,bar,baz", rVal: "foo,bam",
   434  			result: false,
   435  		},
   436  		{
   437  			op:     structs.ConstraintAttributeIsSet,
   438  			lVal:   "foo",
   439  			result: true,
   440  		},
   441  		{
   442  			op:     structs.ConstraintAttributeIsSet,
   443  			lVal:   nil,
   444  			result: false,
   445  		},
   446  		{
   447  			op:     structs.ConstraintAttributeIsNotSet,
   448  			lVal:   nil,
   449  			result: true,
   450  		},
   451  		{
   452  			op:     structs.ConstraintAttributeIsNotSet,
   453  			lVal:   "foo",
   454  			result: false,
   455  		},
   456  	}
   457  
   458  	for _, tc := range cases {
   459  		_, ctx := testContext(t)
   460  		if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal, tc.lVal != nil, tc.rVal != nil); res != tc.result {
   461  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   462  		}
   463  	}
   464  }
   465  
   466  func TestCheckLexicalOrder(t *testing.T) {
   467  	type tcase struct {
   468  		op         string
   469  		lVal, rVal interface{}
   470  		result     bool
   471  	}
   472  	cases := []tcase{
   473  		{
   474  			op:   "<",
   475  			lVal: "bar", rVal: "foo",
   476  			result: true,
   477  		},
   478  		{
   479  			op:   "<=",
   480  			lVal: "foo", rVal: "foo",
   481  			result: true,
   482  		},
   483  		{
   484  			op:   ">",
   485  			lVal: "bar", rVal: "foo",
   486  			result: false,
   487  		},
   488  		{
   489  			op:   ">=",
   490  			lVal: "bar", rVal: "bar",
   491  			result: true,
   492  		},
   493  		{
   494  			op:   ">",
   495  			lVal: 1, rVal: "foo",
   496  			result: false,
   497  		},
   498  	}
   499  	for _, tc := range cases {
   500  		if res := checkLexicalOrder(tc.op, tc.lVal, tc.rVal); res != tc.result {
   501  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   502  		}
   503  	}
   504  }
   505  
   506  func TestCheckVersionConstraint(t *testing.T) {
   507  	type tcase struct {
   508  		lVal, rVal interface{}
   509  		result     bool
   510  	}
   511  	cases := []tcase{
   512  		{
   513  			lVal: "1.2.3", rVal: "~> 1.0",
   514  			result: true,
   515  		},
   516  		{
   517  			lVal: "1.2.3", rVal: ">= 1.0, < 1.4",
   518  			result: true,
   519  		},
   520  		{
   521  			lVal: "2.0.1", rVal: "~> 1.0",
   522  			result: false,
   523  		},
   524  		{
   525  			lVal: "1.4", rVal: ">= 1.0, < 1.4",
   526  			result: false,
   527  		},
   528  		{
   529  			lVal: 1, rVal: "~> 1.0",
   530  			result: true,
   531  		},
   532  	}
   533  	for _, tc := range cases {
   534  		_, ctx := testContext(t)
   535  		if res := checkVersionMatch(ctx, tc.lVal, tc.rVal); res != tc.result {
   536  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   537  		}
   538  	}
   539  }
   540  
   541  func TestCheckRegexpConstraint(t *testing.T) {
   542  	type tcase struct {
   543  		lVal, rVal interface{}
   544  		result     bool
   545  	}
   546  	cases := []tcase{
   547  		{
   548  			lVal: "foobar", rVal: "bar",
   549  			result: true,
   550  		},
   551  		{
   552  			lVal: "foobar", rVal: "^foo",
   553  			result: true,
   554  		},
   555  		{
   556  			lVal: "foobar", rVal: "^bar",
   557  			result: false,
   558  		},
   559  		{
   560  			lVal: "zipzap", rVal: "foo",
   561  			result: false,
   562  		},
   563  		{
   564  			lVal: 1, rVal: "foo",
   565  			result: false,
   566  		},
   567  	}
   568  	for _, tc := range cases {
   569  		_, ctx := testContext(t)
   570  		if res := checkRegexpMatch(ctx, tc.lVal, tc.rVal); res != tc.result {
   571  			t.Fatalf("TC: %#v, Result: %v", tc, res)
   572  		}
   573  	}
   574  }
   575  
   576  // This test puts allocations on the node to test if it detects infeasibility of
   577  // nodes correctly and picks the only feasible one
   578  func TestDistinctHostsIterator_JobDistinctHosts(t *testing.T) {
   579  	_, ctx := testContext(t)
   580  	nodes := []*structs.Node{
   581  		mock.Node(),
   582  		mock.Node(),
   583  		mock.Node(),
   584  	}
   585  	static := NewStaticIterator(ctx, nodes)
   586  
   587  	// Create a job with a distinct_hosts constraint and two task groups.
   588  	tg1 := &structs.TaskGroup{Name: "bar"}
   589  	tg2 := &structs.TaskGroup{Name: "baz"}
   590  
   591  	job := &structs.Job{
   592  		ID:          "foo",
   593  		Namespace:   structs.DefaultNamespace,
   594  		Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
   595  		TaskGroups:  []*structs.TaskGroup{tg1, tg2},
   596  	}
   597  
   598  	// Add allocs placing tg1 on node1 and tg2 on node2. This should make the
   599  	// job unsatisfiable on all nodes but node3
   600  	plan := ctx.Plan()
   601  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   602  		{
   603  			Namespace: structs.DefaultNamespace,
   604  			TaskGroup: tg1.Name,
   605  			JobID:     job.ID,
   606  			Job:       job,
   607  			ID:        uuid.Generate(),
   608  		},
   609  
   610  		// Should be ignored as it is a different job.
   611  		{
   612  			Namespace: structs.DefaultNamespace,
   613  			TaskGroup: tg2.Name,
   614  			JobID:     "ignore 2",
   615  			Job:       job,
   616  			ID:        uuid.Generate(),
   617  		},
   618  	}
   619  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
   620  		{
   621  			Namespace: structs.DefaultNamespace,
   622  			TaskGroup: tg2.Name,
   623  			JobID:     job.ID,
   624  			Job:       job,
   625  			ID:        uuid.Generate(),
   626  		},
   627  
   628  		// Should be ignored as it is a different job.
   629  		{
   630  			Namespace: structs.DefaultNamespace,
   631  			TaskGroup: tg1.Name,
   632  			JobID:     "ignore 2",
   633  			Job:       job,
   634  			ID:        uuid.Generate(),
   635  		},
   636  	}
   637  
   638  	proposed := NewDistinctHostsIterator(ctx, static)
   639  	proposed.SetTaskGroup(tg1)
   640  	proposed.SetJob(job)
   641  
   642  	out := collectFeasible(proposed)
   643  	if len(out) != 1 {
   644  		t.Fatalf("Bad: %#v", out)
   645  	}
   646  
   647  	if out[0].ID != nodes[2].ID {
   648  		t.Fatalf("wrong node picked")
   649  	}
   650  }
   651  
   652  func TestDistinctHostsIterator_JobDistinctHosts_InfeasibleCount(t *testing.T) {
   653  	_, ctx := testContext(t)
   654  	nodes := []*structs.Node{
   655  		mock.Node(),
   656  		mock.Node(),
   657  	}
   658  	static := NewStaticIterator(ctx, nodes)
   659  
   660  	// Create a job with a distinct_hosts constraint and three task groups.
   661  	tg1 := &structs.TaskGroup{Name: "bar"}
   662  	tg2 := &structs.TaskGroup{Name: "baz"}
   663  	tg3 := &structs.TaskGroup{Name: "bam"}
   664  
   665  	job := &structs.Job{
   666  		ID:          "foo",
   667  		Namespace:   structs.DefaultNamespace,
   668  		Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}},
   669  		TaskGroups:  []*structs.TaskGroup{tg1, tg2, tg3},
   670  	}
   671  
   672  	// Add allocs placing tg1 on node1 and tg2 on node2. This should make the
   673  	// job unsatisfiable for tg3
   674  	plan := ctx.Plan()
   675  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   676  		{
   677  			Namespace: structs.DefaultNamespace,
   678  			TaskGroup: tg1.Name,
   679  			JobID:     job.ID,
   680  			ID:        uuid.Generate(),
   681  		},
   682  	}
   683  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
   684  		{
   685  			Namespace: structs.DefaultNamespace,
   686  			TaskGroup: tg2.Name,
   687  			JobID:     job.ID,
   688  			ID:        uuid.Generate(),
   689  		},
   690  	}
   691  
   692  	proposed := NewDistinctHostsIterator(ctx, static)
   693  	proposed.SetTaskGroup(tg3)
   694  	proposed.SetJob(job)
   695  
   696  	// It should not be able to place 3 tasks with only two nodes.
   697  	out := collectFeasible(proposed)
   698  	if len(out) != 0 {
   699  		t.Fatalf("Bad: %#v", out)
   700  	}
   701  }
   702  
   703  func TestDistinctHostsIterator_TaskGroupDistinctHosts(t *testing.T) {
   704  	_, ctx := testContext(t)
   705  	nodes := []*structs.Node{
   706  		mock.Node(),
   707  		mock.Node(),
   708  	}
   709  	static := NewStaticIterator(ctx, nodes)
   710  
   711  	// Create a task group with a distinct_hosts constraint.
   712  	tg1 := &structs.TaskGroup{
   713  		Name: "example",
   714  		Constraints: []*structs.Constraint{
   715  			{Operand: structs.ConstraintDistinctHosts},
   716  		},
   717  	}
   718  	tg2 := &structs.TaskGroup{Name: "baz"}
   719  
   720  	// Add a planned alloc to node1.
   721  	plan := ctx.Plan()
   722  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   723  		{
   724  			Namespace: structs.DefaultNamespace,
   725  			TaskGroup: tg1.Name,
   726  			JobID:     "foo",
   727  		},
   728  	}
   729  
   730  	// Add a planned alloc to node2 with the same task group name but a
   731  	// different job.
   732  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
   733  		{
   734  			Namespace: structs.DefaultNamespace,
   735  			TaskGroup: tg1.Name,
   736  			JobID:     "bar",
   737  		},
   738  	}
   739  
   740  	proposed := NewDistinctHostsIterator(ctx, static)
   741  	proposed.SetTaskGroup(tg1)
   742  	proposed.SetJob(&structs.Job{
   743  		ID:        "foo",
   744  		Namespace: structs.DefaultNamespace,
   745  	})
   746  
   747  	out := collectFeasible(proposed)
   748  	if len(out) != 1 {
   749  		t.Fatalf("Bad: %#v", out)
   750  	}
   751  
   752  	// Expect it to skip the first node as there is a previous alloc on it for
   753  	// the same task group.
   754  	if out[0] != nodes[1] {
   755  		t.Fatalf("Bad: %v", out)
   756  	}
   757  
   758  	// Since the other task group doesn't have the constraint, both nodes should
   759  	// be feasible.
   760  	proposed.Reset()
   761  	proposed.SetTaskGroup(tg2)
   762  	out = collectFeasible(proposed)
   763  	if len(out) != 2 {
   764  		t.Fatalf("Bad: %#v", out)
   765  	}
   766  }
   767  
   768  // This test puts creates allocations across task groups that use a property
   769  // value to detect if the constraint at the job level properly considers all
   770  // task groups.
   771  func TestDistinctPropertyIterator_JobDistinctProperty(t *testing.T) {
   772  	state, ctx := testContext(t)
   773  	nodes := []*structs.Node{
   774  		mock.Node(),
   775  		mock.Node(),
   776  		mock.Node(),
   777  		mock.Node(),
   778  		mock.Node(),
   779  	}
   780  
   781  	for i, n := range nodes {
   782  		n.Meta["rack"] = fmt.Sprintf("%d", i)
   783  
   784  		// Add to state store
   785  		if err := state.UpsertNode(uint64(100+i), n); err != nil {
   786  			t.Fatalf("failed to upsert node: %v", err)
   787  		}
   788  	}
   789  
   790  	static := NewStaticIterator(ctx, nodes)
   791  
   792  	// Create a job with a distinct_property constraint and a task groups.
   793  	tg1 := &structs.TaskGroup{Name: "bar"}
   794  	tg2 := &structs.TaskGroup{Name: "baz"}
   795  
   796  	job := &structs.Job{
   797  		ID:        "foo",
   798  		Namespace: structs.DefaultNamespace,
   799  		Constraints: []*structs.Constraint{
   800  			{
   801  				Operand: structs.ConstraintDistinctProperty,
   802  				LTarget: "${meta.rack}",
   803  			},
   804  		},
   805  		TaskGroups: []*structs.TaskGroup{tg1, tg2},
   806  	}
   807  
   808  	// Add allocs placing tg1 on node1 and 2 and tg2 on node3 and 4. This should make the
   809  	// job unsatisfiable on all nodes but node5. Also mix the allocations
   810  	// existing in the plan and the state store.
   811  	plan := ctx.Plan()
   812  	alloc1ID := uuid.Generate()
   813  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   814  		{
   815  			Namespace: structs.DefaultNamespace,
   816  			TaskGroup: tg1.Name,
   817  			JobID:     job.ID,
   818  			Job:       job,
   819  			ID:        alloc1ID,
   820  			NodeID:    nodes[0].ID,
   821  		},
   822  
   823  		// Should be ignored as it is a different job.
   824  		{
   825  			Namespace: structs.DefaultNamespace,
   826  			TaskGroup: tg2.Name,
   827  			JobID:     "ignore 2",
   828  			Job:       job,
   829  			ID:        uuid.Generate(),
   830  			NodeID:    nodes[0].ID,
   831  		},
   832  	}
   833  	plan.NodeAllocation[nodes[2].ID] = []*structs.Allocation{
   834  		{
   835  			Namespace: structs.DefaultNamespace,
   836  			TaskGroup: tg2.Name,
   837  			JobID:     job.ID,
   838  			Job:       job,
   839  			ID:        uuid.Generate(),
   840  			NodeID:    nodes[2].ID,
   841  		},
   842  
   843  		// Should be ignored as it is a different job.
   844  		{
   845  			Namespace: structs.DefaultNamespace,
   846  			TaskGroup: tg1.Name,
   847  			JobID:     "ignore 2",
   848  			Job:       job,
   849  			ID:        uuid.Generate(),
   850  			NodeID:    nodes[2].ID,
   851  		},
   852  	}
   853  
   854  	// Put an allocation on Node 5 but make it stopped in the plan
   855  	stoppingAllocID := uuid.Generate()
   856  	plan.NodeUpdate[nodes[4].ID] = []*structs.Allocation{
   857  		{
   858  			Namespace: structs.DefaultNamespace,
   859  			TaskGroup: tg2.Name,
   860  			JobID:     job.ID,
   861  			Job:       job,
   862  			ID:        stoppingAllocID,
   863  			NodeID:    nodes[4].ID,
   864  		},
   865  	}
   866  
   867  	upserting := []*structs.Allocation{
   868  		// Have one of the allocations exist in both the plan and the state
   869  		// store. This resembles an allocation update
   870  		{
   871  			Namespace: structs.DefaultNamespace,
   872  			TaskGroup: tg1.Name,
   873  			JobID:     job.ID,
   874  			Job:       job,
   875  			ID:        alloc1ID,
   876  			EvalID:    uuid.Generate(),
   877  			NodeID:    nodes[0].ID,
   878  		},
   879  
   880  		{
   881  			Namespace: structs.DefaultNamespace,
   882  			TaskGroup: tg1.Name,
   883  			JobID:     job.ID,
   884  			Job:       job,
   885  			ID:        uuid.Generate(),
   886  			EvalID:    uuid.Generate(),
   887  			NodeID:    nodes[1].ID,
   888  		},
   889  
   890  		// Should be ignored as it is a different job.
   891  		{
   892  			Namespace: structs.DefaultNamespace,
   893  			TaskGroup: tg2.Name,
   894  			JobID:     "ignore 2",
   895  			Job:       job,
   896  			ID:        uuid.Generate(),
   897  			EvalID:    uuid.Generate(),
   898  			NodeID:    nodes[1].ID,
   899  		},
   900  		{
   901  			Namespace: structs.DefaultNamespace,
   902  			TaskGroup: tg2.Name,
   903  			JobID:     job.ID,
   904  			Job:       job,
   905  			ID:        uuid.Generate(),
   906  			EvalID:    uuid.Generate(),
   907  			NodeID:    nodes[3].ID,
   908  		},
   909  
   910  		// Should be ignored as it is a different job.
   911  		{
   912  			Namespace: structs.DefaultNamespace,
   913  			TaskGroup: tg1.Name,
   914  			JobID:     "ignore 2",
   915  			Job:       job,
   916  			ID:        uuid.Generate(),
   917  			EvalID:    uuid.Generate(),
   918  			NodeID:    nodes[3].ID,
   919  		},
   920  		{
   921  			Namespace: structs.DefaultNamespace,
   922  			TaskGroup: tg2.Name,
   923  			JobID:     job.ID,
   924  			Job:       job,
   925  			ID:        stoppingAllocID,
   926  			EvalID:    uuid.Generate(),
   927  			NodeID:    nodes[4].ID,
   928  		},
   929  	}
   930  	if err := state.UpsertAllocs(1000, upserting); err != nil {
   931  		t.Fatalf("failed to UpsertAllocs: %v", err)
   932  	}
   933  
   934  	proposed := NewDistinctPropertyIterator(ctx, static)
   935  	proposed.SetJob(job)
   936  	proposed.SetTaskGroup(tg2)
   937  	proposed.Reset()
   938  
   939  	out := collectFeasible(proposed)
   940  	if len(out) != 1 {
   941  		t.Fatalf("Bad: %#v", out)
   942  	}
   943  	if out[0].ID != nodes[4].ID {
   944  		t.Fatalf("wrong node picked")
   945  	}
   946  }
   947  
   948  // This test creates allocations across task groups that use a property value to
   949  // detect if the constraint at the job level properly considers all task groups
   950  // when the constraint allows a count greater than one
   951  func TestDistinctPropertyIterator_JobDistinctProperty_Count(t *testing.T) {
   952  	state, ctx := testContext(t)
   953  	nodes := []*structs.Node{
   954  		mock.Node(),
   955  		mock.Node(),
   956  		mock.Node(),
   957  	}
   958  
   959  	for i, n := range nodes {
   960  		n.Meta["rack"] = fmt.Sprintf("%d", i)
   961  
   962  		// Add to state store
   963  		if err := state.UpsertNode(uint64(100+i), n); err != nil {
   964  			t.Fatalf("failed to upsert node: %v", err)
   965  		}
   966  	}
   967  
   968  	static := NewStaticIterator(ctx, nodes)
   969  
   970  	// Create a job with a distinct_property constraint and a task groups.
   971  	tg1 := &structs.TaskGroup{Name: "bar"}
   972  	tg2 := &structs.TaskGroup{Name: "baz"}
   973  
   974  	job := &structs.Job{
   975  		ID:        "foo",
   976  		Namespace: structs.DefaultNamespace,
   977  		Constraints: []*structs.Constraint{
   978  			{
   979  				Operand: structs.ConstraintDistinctProperty,
   980  				LTarget: "${meta.rack}",
   981  				RTarget: "2",
   982  			},
   983  		},
   984  		TaskGroups: []*structs.TaskGroup{tg1, tg2},
   985  	}
   986  
   987  	// Add allocs placing two allocations on both node 1 and 2 and only one on
   988  	// node 3. This should make the job unsatisfiable on all nodes but node5.
   989  	// Also mix the allocations existing in the plan and the state store.
   990  	plan := ctx.Plan()
   991  	alloc1ID := uuid.Generate()
   992  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
   993  		{
   994  			Namespace: structs.DefaultNamespace,
   995  			TaskGroup: tg1.Name,
   996  			JobID:     job.ID,
   997  			Job:       job,
   998  			ID:        alloc1ID,
   999  			NodeID:    nodes[0].ID,
  1000  		},
  1001  
  1002  		{
  1003  			Namespace: structs.DefaultNamespace,
  1004  			TaskGroup: tg2.Name,
  1005  			JobID:     job.ID,
  1006  			Job:       job,
  1007  			ID:        alloc1ID,
  1008  			NodeID:    nodes[0].ID,
  1009  		},
  1010  
  1011  		// Should be ignored as it is a different job.
  1012  		{
  1013  			Namespace: structs.DefaultNamespace,
  1014  			TaskGroup: tg2.Name,
  1015  			JobID:     "ignore 2",
  1016  			Job:       job,
  1017  			ID:        uuid.Generate(),
  1018  			NodeID:    nodes[0].ID,
  1019  		},
  1020  	}
  1021  	plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{
  1022  		{
  1023  			Namespace: structs.DefaultNamespace,
  1024  			TaskGroup: tg1.Name,
  1025  			JobID:     job.ID,
  1026  			Job:       job,
  1027  			ID:        uuid.Generate(),
  1028  			NodeID:    nodes[1].ID,
  1029  		},
  1030  
  1031  		{
  1032  			Namespace: structs.DefaultNamespace,
  1033  			TaskGroup: tg2.Name,
  1034  			JobID:     job.ID,
  1035  			Job:       job,
  1036  			ID:        uuid.Generate(),
  1037  			NodeID:    nodes[1].ID,
  1038  		},
  1039  
  1040  		// Should be ignored as it is a different job.
  1041  		{
  1042  			Namespace: structs.DefaultNamespace,
  1043  			TaskGroup: tg1.Name,
  1044  			JobID:     "ignore 2",
  1045  			Job:       job,
  1046  			ID:        uuid.Generate(),
  1047  			NodeID:    nodes[1].ID,
  1048  		},
  1049  	}
  1050  	plan.NodeAllocation[nodes[2].ID] = []*structs.Allocation{
  1051  		{
  1052  			Namespace: structs.DefaultNamespace,
  1053  			TaskGroup: tg1.Name,
  1054  			JobID:     job.ID,
  1055  			Job:       job,
  1056  			ID:        uuid.Generate(),
  1057  			NodeID:    nodes[2].ID,
  1058  		},
  1059  
  1060  		// Should be ignored as it is a different job.
  1061  		{
  1062  			Namespace: structs.DefaultNamespace,
  1063  			TaskGroup: tg1.Name,
  1064  			JobID:     "ignore 2",
  1065  			Job:       job,
  1066  			ID:        uuid.Generate(),
  1067  			NodeID:    nodes[2].ID,
  1068  		},
  1069  	}
  1070  
  1071  	// Put an allocation on Node 3 but make it stopped in the plan
  1072  	stoppingAllocID := uuid.Generate()
  1073  	plan.NodeUpdate[nodes[2].ID] = []*structs.Allocation{
  1074  		{
  1075  			Namespace: structs.DefaultNamespace,
  1076  			TaskGroup: tg2.Name,
  1077  			JobID:     job.ID,
  1078  			Job:       job,
  1079  			ID:        stoppingAllocID,
  1080  			NodeID:    nodes[2].ID,
  1081  		},
  1082  	}
  1083  
  1084  	upserting := []*structs.Allocation{
  1085  		// Have one of the allocations exist in both the plan and the state
  1086  		// store. This resembles an allocation update
  1087  		{
  1088  			Namespace: structs.DefaultNamespace,
  1089  			TaskGroup: tg1.Name,
  1090  			JobID:     job.ID,
  1091  			Job:       job,
  1092  			ID:        alloc1ID,
  1093  			EvalID:    uuid.Generate(),
  1094  			NodeID:    nodes[0].ID,
  1095  		},
  1096  
  1097  		{
  1098  			Namespace: structs.DefaultNamespace,
  1099  			TaskGroup: tg1.Name,
  1100  			JobID:     job.ID,
  1101  			Job:       job,
  1102  			ID:        uuid.Generate(),
  1103  			EvalID:    uuid.Generate(),
  1104  			NodeID:    nodes[1].ID,
  1105  		},
  1106  
  1107  		{
  1108  			Namespace: structs.DefaultNamespace,
  1109  			TaskGroup: tg2.Name,
  1110  			JobID:     job.ID,
  1111  			Job:       job,
  1112  			ID:        uuid.Generate(),
  1113  			EvalID:    uuid.Generate(),
  1114  			NodeID:    nodes[0].ID,
  1115  		},
  1116  
  1117  		// Should be ignored as it is a different job.
  1118  		{
  1119  			Namespace: structs.DefaultNamespace,
  1120  			TaskGroup: tg1.Name,
  1121  			JobID:     "ignore 2",
  1122  			Job:       job,
  1123  			ID:        uuid.Generate(),
  1124  			EvalID:    uuid.Generate(),
  1125  			NodeID:    nodes[1].ID,
  1126  		},
  1127  		{
  1128  			Namespace: structs.DefaultNamespace,
  1129  			TaskGroup: tg2.Name,
  1130  			JobID:     "ignore 2",
  1131  			Job:       job,
  1132  			ID:        uuid.Generate(),
  1133  			EvalID:    uuid.Generate(),
  1134  			NodeID:    nodes[1].ID,
  1135  		},
  1136  	}
  1137  	if err := state.UpsertAllocs(1000, upserting); err != nil {
  1138  		t.Fatalf("failed to UpsertAllocs: %v", err)
  1139  	}
  1140  
  1141  	proposed := NewDistinctPropertyIterator(ctx, static)
  1142  	proposed.SetJob(job)
  1143  	proposed.SetTaskGroup(tg2)
  1144  	proposed.Reset()
  1145  
  1146  	out := collectFeasible(proposed)
  1147  	if len(out) != 1 {
  1148  		t.Fatalf("Bad: %#v", out)
  1149  	}
  1150  	if out[0].ID != nodes[2].ID {
  1151  		t.Fatalf("wrong node picked")
  1152  	}
  1153  }
  1154  
  1155  // This test checks that if a node has an allocation on it that gets stopped,
  1156  // there is a plan to re-use that for a new allocation, that the next select
  1157  // won't select that node.
  1158  func TestDistinctPropertyIterator_JobDistinctProperty_RemoveAndReplace(t *testing.T) {
  1159  	state, ctx := testContext(t)
  1160  	nodes := []*structs.Node{
  1161  		mock.Node(),
  1162  	}
  1163  
  1164  	nodes[0].Meta["rack"] = "1"
  1165  
  1166  	// Add to state store
  1167  	if err := state.UpsertNode(uint64(100), nodes[0]); err != nil {
  1168  		t.Fatalf("failed to upsert node: %v", err)
  1169  	}
  1170  
  1171  	static := NewStaticIterator(ctx, nodes)
  1172  
  1173  	// Create a job with a distinct_property constraint and a task groups.
  1174  	tg1 := &structs.TaskGroup{Name: "bar"}
  1175  	job := &structs.Job{
  1176  		Namespace: structs.DefaultNamespace,
  1177  		ID:        "foo",
  1178  		Constraints: []*structs.Constraint{
  1179  			{
  1180  				Operand: structs.ConstraintDistinctProperty,
  1181  				LTarget: "${meta.rack}",
  1182  			},
  1183  		},
  1184  		TaskGroups: []*structs.TaskGroup{tg1},
  1185  	}
  1186  
  1187  	plan := ctx.Plan()
  1188  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
  1189  		{
  1190  			Namespace: structs.DefaultNamespace,
  1191  			TaskGroup: tg1.Name,
  1192  			JobID:     job.ID,
  1193  			Job:       job,
  1194  			ID:        uuid.Generate(),
  1195  			NodeID:    nodes[0].ID,
  1196  		},
  1197  	}
  1198  
  1199  	stoppingAllocID := uuid.Generate()
  1200  	plan.NodeUpdate[nodes[0].ID] = []*structs.Allocation{
  1201  		{
  1202  			Namespace: structs.DefaultNamespace,
  1203  			TaskGroup: tg1.Name,
  1204  			JobID:     job.ID,
  1205  			Job:       job,
  1206  			ID:        stoppingAllocID,
  1207  			NodeID:    nodes[0].ID,
  1208  		},
  1209  	}
  1210  
  1211  	upserting := []*structs.Allocation{
  1212  		{
  1213  			Namespace: structs.DefaultNamespace,
  1214  			TaskGroup: tg1.Name,
  1215  			JobID:     job.ID,
  1216  			Job:       job,
  1217  			ID:        stoppingAllocID,
  1218  			EvalID:    uuid.Generate(),
  1219  			NodeID:    nodes[0].ID,
  1220  		},
  1221  	}
  1222  	if err := state.UpsertAllocs(1000, upserting); err != nil {
  1223  		t.Fatalf("failed to UpsertAllocs: %v", err)
  1224  	}
  1225  
  1226  	proposed := NewDistinctPropertyIterator(ctx, static)
  1227  	proposed.SetJob(job)
  1228  	proposed.SetTaskGroup(tg1)
  1229  	proposed.Reset()
  1230  
  1231  	out := collectFeasible(proposed)
  1232  	if len(out) != 0 {
  1233  		t.Fatalf("Bad: %#v", out)
  1234  	}
  1235  }
  1236  
  1237  // This test creates previous allocations selecting certain property values to
  1238  // test if it detects infeasibility of property values correctly and picks the
  1239  // only feasible one
  1240  func TestDistinctPropertyIterator_JobDistinctProperty_Infeasible(t *testing.T) {
  1241  	state, ctx := testContext(t)
  1242  	nodes := []*structs.Node{
  1243  		mock.Node(),
  1244  		mock.Node(),
  1245  	}
  1246  
  1247  	for i, n := range nodes {
  1248  		n.Meta["rack"] = fmt.Sprintf("%d", i)
  1249  
  1250  		// Add to state store
  1251  		if err := state.UpsertNode(uint64(100+i), n); err != nil {
  1252  			t.Fatalf("failed to upsert node: %v", err)
  1253  		}
  1254  	}
  1255  
  1256  	static := NewStaticIterator(ctx, nodes)
  1257  
  1258  	// Create a job with a distinct_property constraint and a task groups.
  1259  	tg1 := &structs.TaskGroup{Name: "bar"}
  1260  	tg2 := &structs.TaskGroup{Name: "baz"}
  1261  	tg3 := &structs.TaskGroup{Name: "bam"}
  1262  
  1263  	job := &structs.Job{
  1264  		Namespace: structs.DefaultNamespace,
  1265  		ID:        "foo",
  1266  		Constraints: []*structs.Constraint{
  1267  			{
  1268  				Operand: structs.ConstraintDistinctProperty,
  1269  				LTarget: "${meta.rack}",
  1270  			},
  1271  		},
  1272  		TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3},
  1273  	}
  1274  
  1275  	// Add allocs placing tg1 on node1 and tg2 on node2. This should make the
  1276  	// job unsatisfiable for tg3.
  1277  	plan := ctx.Plan()
  1278  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
  1279  		{
  1280  			Namespace: structs.DefaultNamespace,
  1281  			TaskGroup: tg1.Name,
  1282  			JobID:     job.ID,
  1283  			Job:       job,
  1284  			ID:        uuid.Generate(),
  1285  			NodeID:    nodes[0].ID,
  1286  		},
  1287  	}
  1288  	upserting := []*structs.Allocation{
  1289  		{
  1290  			Namespace: structs.DefaultNamespace,
  1291  			TaskGroup: tg2.Name,
  1292  			JobID:     job.ID,
  1293  			Job:       job,
  1294  			ID:        uuid.Generate(),
  1295  			EvalID:    uuid.Generate(),
  1296  			NodeID:    nodes[1].ID,
  1297  		},
  1298  	}
  1299  	if err := state.UpsertAllocs(1000, upserting); err != nil {
  1300  		t.Fatalf("failed to UpsertAllocs: %v", err)
  1301  	}
  1302  
  1303  	proposed := NewDistinctPropertyIterator(ctx, static)
  1304  	proposed.SetJob(job)
  1305  	proposed.SetTaskGroup(tg3)
  1306  	proposed.Reset()
  1307  
  1308  	out := collectFeasible(proposed)
  1309  	if len(out) != 0 {
  1310  		t.Fatalf("Bad: %#v", out)
  1311  	}
  1312  }
  1313  
  1314  // This test creates previous allocations selecting certain property values to
  1315  // test if it detects infeasibility of property values correctly and picks the
  1316  // only feasible one
  1317  func TestDistinctPropertyIterator_JobDistinctProperty_Infeasible_Count(t *testing.T) {
  1318  	state, ctx := testContext(t)
  1319  	nodes := []*structs.Node{
  1320  		mock.Node(),
  1321  		mock.Node(),
  1322  	}
  1323  
  1324  	for i, n := range nodes {
  1325  		n.Meta["rack"] = fmt.Sprintf("%d", i)
  1326  
  1327  		// Add to state store
  1328  		if err := state.UpsertNode(uint64(100+i), n); err != nil {
  1329  			t.Fatalf("failed to upsert node: %v", err)
  1330  		}
  1331  	}
  1332  
  1333  	static := NewStaticIterator(ctx, nodes)
  1334  
  1335  	// Create a job with a distinct_property constraint and a task groups.
  1336  	tg1 := &structs.TaskGroup{Name: "bar"}
  1337  	tg2 := &structs.TaskGroup{Name: "baz"}
  1338  	tg3 := &structs.TaskGroup{Name: "bam"}
  1339  
  1340  	job := &structs.Job{
  1341  		Namespace: structs.DefaultNamespace,
  1342  		ID:        "foo",
  1343  		Constraints: []*structs.Constraint{
  1344  			{
  1345  				Operand: structs.ConstraintDistinctProperty,
  1346  				LTarget: "${meta.rack}",
  1347  				RTarget: "2",
  1348  			},
  1349  		},
  1350  		TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3},
  1351  	}
  1352  
  1353  	// Add allocs placing two tg1's on node1 and two tg2's on node2. This should
  1354  	// make the job unsatisfiable for tg3.
  1355  	plan := ctx.Plan()
  1356  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
  1357  		{
  1358  			Namespace: structs.DefaultNamespace,
  1359  			TaskGroup: tg1.Name,
  1360  			JobID:     job.ID,
  1361  			Job:       job,
  1362  			ID:        uuid.Generate(),
  1363  			NodeID:    nodes[0].ID,
  1364  		},
  1365  		{
  1366  			Namespace: structs.DefaultNamespace,
  1367  			TaskGroup: tg2.Name,
  1368  			JobID:     job.ID,
  1369  			Job:       job,
  1370  			ID:        uuid.Generate(),
  1371  			NodeID:    nodes[0].ID,
  1372  		},
  1373  	}
  1374  	upserting := []*structs.Allocation{
  1375  		{
  1376  			Namespace: structs.DefaultNamespace,
  1377  			TaskGroup: tg1.Name,
  1378  			JobID:     job.ID,
  1379  			Job:       job,
  1380  			ID:        uuid.Generate(),
  1381  			EvalID:    uuid.Generate(),
  1382  			NodeID:    nodes[1].ID,
  1383  		},
  1384  		{
  1385  			Namespace: structs.DefaultNamespace,
  1386  			TaskGroup: tg2.Name,
  1387  			JobID:     job.ID,
  1388  			Job:       job,
  1389  			ID:        uuid.Generate(),
  1390  			EvalID:    uuid.Generate(),
  1391  			NodeID:    nodes[1].ID,
  1392  		},
  1393  	}
  1394  	if err := state.UpsertAllocs(1000, upserting); err != nil {
  1395  		t.Fatalf("failed to UpsertAllocs: %v", err)
  1396  	}
  1397  
  1398  	proposed := NewDistinctPropertyIterator(ctx, static)
  1399  	proposed.SetJob(job)
  1400  	proposed.SetTaskGroup(tg3)
  1401  	proposed.Reset()
  1402  
  1403  	out := collectFeasible(proposed)
  1404  	if len(out) != 0 {
  1405  		t.Fatalf("Bad: %#v", out)
  1406  	}
  1407  }
  1408  
  1409  // This test creates previous allocations selecting certain property values to
  1410  // test if it detects infeasibility of property values correctly and picks the
  1411  // only feasible one when the constraint is at the task group.
  1412  func TestDistinctPropertyIterator_TaskGroupDistinctProperty(t *testing.T) {
  1413  	state, ctx := testContext(t)
  1414  	nodes := []*structs.Node{
  1415  		mock.Node(),
  1416  		mock.Node(),
  1417  		mock.Node(),
  1418  	}
  1419  
  1420  	for i, n := range nodes {
  1421  		n.Meta["rack"] = fmt.Sprintf("%d", i)
  1422  
  1423  		// Add to state store
  1424  		if err := state.UpsertNode(uint64(100+i), n); err != nil {
  1425  			t.Fatalf("failed to upsert node: %v", err)
  1426  		}
  1427  	}
  1428  
  1429  	static := NewStaticIterator(ctx, nodes)
  1430  
  1431  	// Create a job with a task group with the distinct_property constraint
  1432  	tg1 := &structs.TaskGroup{
  1433  		Name: "example",
  1434  		Constraints: []*structs.Constraint{
  1435  			{
  1436  				Operand: structs.ConstraintDistinctProperty,
  1437  				LTarget: "${meta.rack}",
  1438  			},
  1439  		},
  1440  	}
  1441  	tg2 := &structs.TaskGroup{Name: "baz"}
  1442  
  1443  	job := &structs.Job{
  1444  		Namespace:  structs.DefaultNamespace,
  1445  		ID:         "foo",
  1446  		TaskGroups: []*structs.TaskGroup{tg1, tg2},
  1447  	}
  1448  
  1449  	// Add allocs placing tg1 on node1 and 2. This should make the
  1450  	// job unsatisfiable on all nodes but node3. Also mix the allocations
  1451  	// existing in the plan and the state store.
  1452  	plan := ctx.Plan()
  1453  	plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{
  1454  		{
  1455  			Namespace: structs.DefaultNamespace,
  1456  			TaskGroup: tg1.Name,
  1457  			JobID:     job.ID,
  1458  			Job:       job,
  1459  			ID:        uuid.Generate(),
  1460  			NodeID:    nodes[0].ID,
  1461  		},
  1462  	}
  1463  
  1464  	// Put an allocation on Node 3 but make it stopped in the plan
  1465  	stoppingAllocID := uuid.Generate()
  1466  	plan.NodeUpdate[nodes[2].ID] = []*structs.Allocation{
  1467  		{
  1468  			Namespace: structs.DefaultNamespace,
  1469  			TaskGroup: tg1.Name,
  1470  			JobID:     job.ID,
  1471  			Job:       job,
  1472  			ID:        stoppingAllocID,
  1473  			NodeID:    nodes[2].ID,
  1474  		},
  1475  	}
  1476  
  1477  	upserting := []*structs.Allocation{
  1478  		{
  1479  			Namespace: structs.DefaultNamespace,
  1480  			TaskGroup: tg1.Name,
  1481  			JobID:     job.ID,
  1482  			Job:       job,
  1483  			ID:        uuid.Generate(),
  1484  			EvalID:    uuid.Generate(),
  1485  			NodeID:    nodes[1].ID,
  1486  		},
  1487  
  1488  		// Should be ignored as it is a different job.
  1489  		{
  1490  			Namespace: structs.DefaultNamespace,
  1491  			TaskGroup: tg1.Name,
  1492  			JobID:     "ignore 2",
  1493  			Job:       job,
  1494  			ID:        uuid.Generate(),
  1495  			EvalID:    uuid.Generate(),
  1496  			NodeID:    nodes[2].ID,
  1497  		},
  1498  
  1499  		{
  1500  			Namespace: structs.DefaultNamespace,
  1501  			TaskGroup: tg1.Name,
  1502  			JobID:     job.ID,
  1503  			Job:       job,
  1504  			ID:        stoppingAllocID,
  1505  			EvalID:    uuid.Generate(),
  1506  			NodeID:    nodes[2].ID,
  1507  		},
  1508  	}
  1509  	if err := state.UpsertAllocs(1000, upserting); err != nil {
  1510  		t.Fatalf("failed to UpsertAllocs: %v", err)
  1511  	}
  1512  
  1513  	proposed := NewDistinctPropertyIterator(ctx, static)
  1514  	proposed.SetJob(job)
  1515  	proposed.SetTaskGroup(tg1)
  1516  	proposed.Reset()
  1517  
  1518  	out := collectFeasible(proposed)
  1519  	if len(out) != 1 {
  1520  		t.Fatalf("Bad: %#v", out)
  1521  	}
  1522  	if out[0].ID != nodes[2].ID {
  1523  		t.Fatalf("wrong node picked")
  1524  	}
  1525  
  1526  	// Since the other task group doesn't have the constraint, both nodes should
  1527  	// be feasible.
  1528  	proposed.SetTaskGroup(tg2)
  1529  	proposed.Reset()
  1530  
  1531  	out = collectFeasible(proposed)
  1532  	if len(out) != 3 {
  1533  		t.Fatalf("Bad: %#v", out)
  1534  	}
  1535  }
  1536  
  1537  func collectFeasible(iter FeasibleIterator) (out []*structs.Node) {
  1538  	for {
  1539  		next := iter.Next()
  1540  		if next == nil {
  1541  			break
  1542  		}
  1543  		out = append(out, next)
  1544  	}
  1545  	return
  1546  }
  1547  
  1548  // mockFeasibilityChecker is a FeasibilityChecker that returns predetermined
  1549  // feasibility values.
  1550  type mockFeasibilityChecker struct {
  1551  	retVals []bool
  1552  	i       int
  1553  }
  1554  
  1555  func newMockFeasibilityChecker(values ...bool) *mockFeasibilityChecker {
  1556  	return &mockFeasibilityChecker{retVals: values}
  1557  }
  1558  
  1559  func (c *mockFeasibilityChecker) Feasible(*structs.Node) bool {
  1560  	if c.i >= len(c.retVals) {
  1561  		c.i++
  1562  		return false
  1563  	}
  1564  
  1565  	f := c.retVals[c.i]
  1566  	c.i++
  1567  	return f
  1568  }
  1569  
  1570  // calls returns how many times the checker was called.
  1571  func (c *mockFeasibilityChecker) calls() int { return c.i }
  1572  
  1573  func TestFeasibilityWrapper_JobIneligible(t *testing.T) {
  1574  	_, ctx := testContext(t)
  1575  	nodes := []*structs.Node{mock.Node()}
  1576  	static := NewStaticIterator(ctx, nodes)
  1577  	mocked := newMockFeasibilityChecker(false)
  1578  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil)
  1579  
  1580  	// Set the job to ineligible
  1581  	ctx.Eligibility().SetJobEligibility(false, nodes[0].ComputedClass)
  1582  
  1583  	// Run the wrapper.
  1584  	out := collectFeasible(wrapper)
  1585  
  1586  	if out != nil || mocked.calls() != 0 {
  1587  		t.Fatalf("bad: %#v %d", out, mocked.calls())
  1588  	}
  1589  }
  1590  
  1591  func TestFeasibilityWrapper_JobEscapes(t *testing.T) {
  1592  	_, ctx := testContext(t)
  1593  	nodes := []*structs.Node{mock.Node()}
  1594  	static := NewStaticIterator(ctx, nodes)
  1595  	mocked := newMockFeasibilityChecker(false)
  1596  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil)
  1597  
  1598  	// Set the job to escaped
  1599  	cc := nodes[0].ComputedClass
  1600  	ctx.Eligibility().job[cc] = EvalComputedClassEscaped
  1601  
  1602  	// Run the wrapper.
  1603  	out := collectFeasible(wrapper)
  1604  
  1605  	if out != nil || mocked.calls() != 1 {
  1606  		t.Fatalf("bad: %#v", out)
  1607  	}
  1608  
  1609  	// Ensure that the job status didn't change from escaped even though the
  1610  	// option failed.
  1611  	if status := ctx.Eligibility().JobStatus(cc); status != EvalComputedClassEscaped {
  1612  		t.Fatalf("job status is %v; want %v", status, EvalComputedClassEscaped)
  1613  	}
  1614  }
  1615  
  1616  func TestFeasibilityWrapper_JobAndTg_Eligible(t *testing.T) {
  1617  	_, ctx := testContext(t)
  1618  	nodes := []*structs.Node{mock.Node()}
  1619  	static := NewStaticIterator(ctx, nodes)
  1620  	jobMock := newMockFeasibilityChecker(true)
  1621  	tgMock := newMockFeasibilityChecker(false)
  1622  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
  1623  
  1624  	// Set the job to escaped
  1625  	cc := nodes[0].ComputedClass
  1626  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
  1627  	ctx.Eligibility().SetTaskGroupEligibility(true, "foo", cc)
  1628  	wrapper.SetTaskGroup("foo")
  1629  
  1630  	// Run the wrapper.
  1631  	out := collectFeasible(wrapper)
  1632  
  1633  	if out == nil || tgMock.calls() != 0 {
  1634  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
  1635  	}
  1636  }
  1637  
  1638  func TestFeasibilityWrapper_JobEligible_TgIneligible(t *testing.T) {
  1639  	_, ctx := testContext(t)
  1640  	nodes := []*structs.Node{mock.Node()}
  1641  	static := NewStaticIterator(ctx, nodes)
  1642  	jobMock := newMockFeasibilityChecker(true)
  1643  	tgMock := newMockFeasibilityChecker(false)
  1644  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
  1645  
  1646  	// Set the job to escaped
  1647  	cc := nodes[0].ComputedClass
  1648  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
  1649  	ctx.Eligibility().SetTaskGroupEligibility(false, "foo", cc)
  1650  	wrapper.SetTaskGroup("foo")
  1651  
  1652  	// Run the wrapper.
  1653  	out := collectFeasible(wrapper)
  1654  
  1655  	if out != nil || tgMock.calls() != 0 {
  1656  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
  1657  	}
  1658  }
  1659  
  1660  func TestFeasibilityWrapper_JobEligible_TgEscaped(t *testing.T) {
  1661  	_, ctx := testContext(t)
  1662  	nodes := []*structs.Node{mock.Node()}
  1663  	static := NewStaticIterator(ctx, nodes)
  1664  	jobMock := newMockFeasibilityChecker(true)
  1665  	tgMock := newMockFeasibilityChecker(true)
  1666  	wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock})
  1667  
  1668  	// Set the job to escaped
  1669  	cc := nodes[0].ComputedClass
  1670  	ctx.Eligibility().job[cc] = EvalComputedClassEligible
  1671  	ctx.Eligibility().taskGroups["foo"] =
  1672  		map[string]ComputedClassFeasibility{cc: EvalComputedClassEscaped}
  1673  	wrapper.SetTaskGroup("foo")
  1674  
  1675  	// Run the wrapper.
  1676  	out := collectFeasible(wrapper)
  1677  
  1678  	if out == nil || tgMock.calls() != 1 {
  1679  		t.Fatalf("bad: %#v %v", out, tgMock.calls())
  1680  	}
  1681  
  1682  	if e, ok := ctx.Eligibility().taskGroups["foo"][cc]; !ok || e != EvalComputedClassEscaped {
  1683  		t.Fatalf("bad: %v %v", e, ok)
  1684  	}
  1685  }
  1686  
  1687  func TestSetContainsAny(t *testing.T) {
  1688  	require.True(t, checkSetContainsAny("a", "a"))
  1689  	require.True(t, checkSetContainsAny("a,b", "a"))
  1690  	require.True(t, checkSetContainsAny("  a,b  ", "a "))
  1691  	require.True(t, checkSetContainsAny("a", "a"))
  1692  	require.False(t, checkSetContainsAny("b", "a"))
  1693  }
  1694  
  1695  func TestDeviceChecker(t *testing.T) {
  1696  	getTg := func(devices ...*structs.RequestedDevice) *structs.TaskGroup {
  1697  		return &structs.TaskGroup{
  1698  			Name: "example",
  1699  			Tasks: []*structs.Task{
  1700  				{
  1701  					Resources: &structs.Resources{
  1702  						Devices: devices,
  1703  					},
  1704  				},
  1705  			},
  1706  		}
  1707  	}
  1708  
  1709  	// Just type
  1710  	gpuTypeReq := &structs.RequestedDevice{
  1711  		Name:  "gpu",
  1712  		Count: 1,
  1713  	}
  1714  	fpgaTypeReq := &structs.RequestedDevice{
  1715  		Name:  "fpga",
  1716  		Count: 1,
  1717  	}
  1718  
  1719  	// vendor/type
  1720  	gpuVendorTypeReq := &structs.RequestedDevice{
  1721  		Name:  "nvidia/gpu",
  1722  		Count: 1,
  1723  	}
  1724  	fpgaVendorTypeReq := &structs.RequestedDevice{
  1725  		Name:  "nvidia/fpga",
  1726  		Count: 1,
  1727  	}
  1728  
  1729  	// vendor/type/model
  1730  	gpuFullReq := &structs.RequestedDevice{
  1731  		Name:  "nvidia/gpu/1080ti",
  1732  		Count: 1,
  1733  	}
  1734  	fpgaFullReq := &structs.RequestedDevice{
  1735  		Name:  "nvidia/fpga/F100",
  1736  		Count: 1,
  1737  	}
  1738  
  1739  	// Just type but high count
  1740  	gpuTypeHighCountReq := &structs.RequestedDevice{
  1741  		Name:  "gpu",
  1742  		Count: 3,
  1743  	}
  1744  
  1745  	getNode := func(devices ...*structs.NodeDeviceResource) *structs.Node {
  1746  		n := mock.Node()
  1747  		n.NodeResources.Devices = devices
  1748  		return n
  1749  	}
  1750  
  1751  	nvidia := &structs.NodeDeviceResource{
  1752  		Vendor: "nvidia",
  1753  		Type:   "gpu",
  1754  		Name:   "1080ti",
  1755  		Attributes: map[string]*psstructs.Attribute{
  1756  			"memory":        psstructs.NewIntAttribute(4, psstructs.UnitGiB),
  1757  			"pci_bandwidth": psstructs.NewIntAttribute(995, psstructs.UnitMiBPerS),
  1758  			"cores_clock":   psstructs.NewIntAttribute(800, psstructs.UnitMHz),
  1759  		},
  1760  		Instances: []*structs.NodeDevice{
  1761  			{
  1762  				ID:      uuid.Generate(),
  1763  				Healthy: true,
  1764  			},
  1765  			{
  1766  				ID:      uuid.Generate(),
  1767  				Healthy: true,
  1768  			},
  1769  		},
  1770  	}
  1771  
  1772  	nvidiaUnhealthy := &structs.NodeDeviceResource{
  1773  		Vendor: "nvidia",
  1774  		Type:   "gpu",
  1775  		Name:   "1080ti",
  1776  		Instances: []*structs.NodeDevice{
  1777  			{
  1778  				ID:      uuid.Generate(),
  1779  				Healthy: false,
  1780  			},
  1781  			{
  1782  				ID:      uuid.Generate(),
  1783  				Healthy: false,
  1784  			},
  1785  		},
  1786  	}
  1787  
  1788  	cases := []struct {
  1789  		Name             string
  1790  		Result           bool
  1791  		NodeDevices      []*structs.NodeDeviceResource
  1792  		RequestedDevices []*structs.RequestedDevice
  1793  	}{
  1794  		{
  1795  			Name:             "no devices on node",
  1796  			Result:           false,
  1797  			NodeDevices:      nil,
  1798  			RequestedDevices: []*structs.RequestedDevice{gpuTypeReq},
  1799  		},
  1800  		{
  1801  			Name:             "no requested devices on empty node",
  1802  			Result:           true,
  1803  			NodeDevices:      nil,
  1804  			RequestedDevices: nil,
  1805  		},
  1806  		{
  1807  			Name:             "gpu devices by type",
  1808  			Result:           true,
  1809  			NodeDevices:      []*structs.NodeDeviceResource{nvidia},
  1810  			RequestedDevices: []*structs.RequestedDevice{gpuTypeReq},
  1811  		},
  1812  		{
  1813  			Name:             "wrong devices by type",
  1814  			Result:           false,
  1815  			NodeDevices:      []*structs.NodeDeviceResource{nvidia},
  1816  			RequestedDevices: []*structs.RequestedDevice{fpgaTypeReq},
  1817  		},
  1818  		{
  1819  			Name:             "devices by type unhealthy node",
  1820  			Result:           false,
  1821  			NodeDevices:      []*structs.NodeDeviceResource{nvidiaUnhealthy},
  1822  			RequestedDevices: []*structs.RequestedDevice{gpuTypeReq},
  1823  		},
  1824  		{
  1825  			Name:             "gpu devices by vendor/type",
  1826  			Result:           true,
  1827  			NodeDevices:      []*structs.NodeDeviceResource{nvidia},
  1828  			RequestedDevices: []*structs.RequestedDevice{gpuVendorTypeReq},
  1829  		},
  1830  		{
  1831  			Name:             "wrong devices by vendor/type",
  1832  			Result:           false,
  1833  			NodeDevices:      []*structs.NodeDeviceResource{nvidia},
  1834  			RequestedDevices: []*structs.RequestedDevice{fpgaVendorTypeReq},
  1835  		},
  1836  		{
  1837  			Name:             "gpu devices by vendor/type/model",
  1838  			Result:           true,
  1839  			NodeDevices:      []*structs.NodeDeviceResource{nvidia},
  1840  			RequestedDevices: []*structs.RequestedDevice{gpuFullReq},
  1841  		},
  1842  		{
  1843  			Name:             "wrong devices by vendor/type/model",
  1844  			Result:           false,
  1845  			NodeDevices:      []*structs.NodeDeviceResource{nvidia},
  1846  			RequestedDevices: []*structs.RequestedDevice{fpgaFullReq},
  1847  		},
  1848  		{
  1849  			Name:             "too many requested",
  1850  			Result:           false,
  1851  			NodeDevices:      []*structs.NodeDeviceResource{nvidia},
  1852  			RequestedDevices: []*structs.RequestedDevice{gpuTypeHighCountReq},
  1853  		},
  1854  		{
  1855  			Name:        "meets constraints requirement",
  1856  			Result:      true,
  1857  			NodeDevices: []*structs.NodeDeviceResource{nvidia},
  1858  			RequestedDevices: []*structs.RequestedDevice{
  1859  				{
  1860  					Name:  "nvidia/gpu",
  1861  					Count: 1,
  1862  					Constraints: []*structs.Constraint{
  1863  						{
  1864  							Operand: "=",
  1865  							LTarget: "${device.model}",
  1866  							RTarget: "1080ti",
  1867  						},
  1868  						{
  1869  							Operand: ">",
  1870  							LTarget: "${device.attr.memory}",
  1871  							RTarget: "1320.5 MB",
  1872  						},
  1873  						{
  1874  							Operand: "<=",
  1875  							LTarget: "${device.attr.pci_bandwidth}",
  1876  							RTarget: ".98   GiB/s",
  1877  						},
  1878  						{
  1879  							Operand: "=",
  1880  							LTarget: "${device.attr.cores_clock}",
  1881  							RTarget: "800MHz",
  1882  						},
  1883  					},
  1884  				},
  1885  			},
  1886  		},
  1887  		{
  1888  			Name:        "meets constraints requirement multiple count",
  1889  			Result:      true,
  1890  			NodeDevices: []*structs.NodeDeviceResource{nvidia},
  1891  			RequestedDevices: []*structs.RequestedDevice{
  1892  				{
  1893  					Name:  "nvidia/gpu",
  1894  					Count: 2,
  1895  					Constraints: []*structs.Constraint{
  1896  						{
  1897  							Operand: "=",
  1898  							LTarget: "${device.model}",
  1899  							RTarget: "1080ti",
  1900  						},
  1901  						{
  1902  							Operand: ">",
  1903  							LTarget: "${device.attr.memory}",
  1904  							RTarget: "1320.5 MB",
  1905  						},
  1906  						{
  1907  							Operand: "<=",
  1908  							LTarget: "${device.attr.pci_bandwidth}",
  1909  							RTarget: ".98   GiB/s",
  1910  						},
  1911  						{
  1912  							Operand: "=",
  1913  							LTarget: "${device.attr.cores_clock}",
  1914  							RTarget: "800MHz",
  1915  						},
  1916  					},
  1917  				},
  1918  			},
  1919  		},
  1920  		{
  1921  			Name:        "meets constraints requirement over count",
  1922  			Result:      false,
  1923  			NodeDevices: []*structs.NodeDeviceResource{nvidia},
  1924  			RequestedDevices: []*structs.RequestedDevice{
  1925  				{
  1926  					Name:  "nvidia/gpu",
  1927  					Count: 5,
  1928  					Constraints: []*structs.Constraint{
  1929  						{
  1930  							Operand: "=",
  1931  							LTarget: "${device.model}",
  1932  							RTarget: "1080ti",
  1933  						},
  1934  						{
  1935  							Operand: ">",
  1936  							LTarget: "${device.attr.memory}",
  1937  							RTarget: "1320.5 MB",
  1938  						},
  1939  						{
  1940  							Operand: "<=",
  1941  							LTarget: "${device.attr.pci_bandwidth}",
  1942  							RTarget: ".98   GiB/s",
  1943  						},
  1944  						{
  1945  							Operand: "=",
  1946  							LTarget: "${device.attr.cores_clock}",
  1947  							RTarget: "800MHz",
  1948  						},
  1949  					},
  1950  				},
  1951  			},
  1952  		},
  1953  		{
  1954  			Name:        "does not meet first constraint",
  1955  			Result:      false,
  1956  			NodeDevices: []*structs.NodeDeviceResource{nvidia},
  1957  			RequestedDevices: []*structs.RequestedDevice{
  1958  				{
  1959  					Name:  "nvidia/gpu",
  1960  					Count: 1,
  1961  					Constraints: []*structs.Constraint{
  1962  						{
  1963  							Operand: "=",
  1964  							LTarget: "${device.model}",
  1965  							RTarget: "2080ti",
  1966  						},
  1967  						{
  1968  							Operand: ">",
  1969  							LTarget: "${device.attr.memory}",
  1970  							RTarget: "1320.5 MB",
  1971  						},
  1972  						{
  1973  							Operand: "<=",
  1974  							LTarget: "${device.attr.pci_bandwidth}",
  1975  							RTarget: ".98   GiB/s",
  1976  						},
  1977  						{
  1978  							Operand: "=",
  1979  							LTarget: "${device.attr.cores_clock}",
  1980  							RTarget: "800MHz",
  1981  						},
  1982  					},
  1983  				},
  1984  			},
  1985  		},
  1986  		{
  1987  			Name:        "does not meet second constraint",
  1988  			Result:      false,
  1989  			NodeDevices: []*structs.NodeDeviceResource{nvidia},
  1990  			RequestedDevices: []*structs.RequestedDevice{
  1991  				{
  1992  					Name:  "nvidia/gpu",
  1993  					Count: 1,
  1994  					Constraints: []*structs.Constraint{
  1995  						{
  1996  							Operand: "=",
  1997  							LTarget: "${device.model}",
  1998  							RTarget: "1080ti",
  1999  						},
  2000  						{
  2001  							Operand: "<",
  2002  							LTarget: "${device.attr.memory}",
  2003  							RTarget: "1320.5 MB",
  2004  						},
  2005  						{
  2006  							Operand: "<=",
  2007  							LTarget: "${device.attr.pci_bandwidth}",
  2008  							RTarget: ".98   GiB/s",
  2009  						},
  2010  						{
  2011  							Operand: "=",
  2012  							LTarget: "${device.attr.cores_clock}",
  2013  							RTarget: "800MHz",
  2014  						},
  2015  					},
  2016  				},
  2017  			},
  2018  		},
  2019  	}
  2020  
  2021  	for _, c := range cases {
  2022  		t.Run(c.Name, func(t *testing.T) {
  2023  			_, ctx := testContext(t)
  2024  			checker := NewDeviceChecker(ctx)
  2025  			checker.SetTaskGroup(getTg(c.RequestedDevices...))
  2026  			if act := checker.Feasible(getNode(c.NodeDevices...)); act != c.Result {
  2027  				t.Fatalf("got %v; want %v", act, c.Result)
  2028  			}
  2029  		})
  2030  	}
  2031  }
  2032  
  2033  func TestCheckAttributeConstraint(t *testing.T) {
  2034  	type tcase struct {
  2035  		op         string
  2036  		lVal, rVal *psstructs.Attribute
  2037  		result     bool
  2038  	}
  2039  	cases := []tcase{
  2040  		{
  2041  			op:     "=",
  2042  			lVal:   psstructs.NewStringAttribute("foo"),
  2043  			rVal:   psstructs.NewStringAttribute("foo"),
  2044  			result: true,
  2045  		},
  2046  		{
  2047  			op:     "=",
  2048  			lVal:   nil,
  2049  			rVal:   nil,
  2050  			result: false,
  2051  		},
  2052  		{
  2053  			op:     "is",
  2054  			lVal:   psstructs.NewStringAttribute("foo"),
  2055  			rVal:   psstructs.NewStringAttribute("foo"),
  2056  			result: true,
  2057  		},
  2058  		{
  2059  			op:     "==",
  2060  			lVal:   psstructs.NewStringAttribute("foo"),
  2061  			rVal:   psstructs.NewStringAttribute("foo"),
  2062  			result: true,
  2063  		},
  2064  		{
  2065  			op:     "!=",
  2066  			lVal:   psstructs.NewStringAttribute("foo"),
  2067  			rVal:   psstructs.NewStringAttribute("foo"),
  2068  			result: false,
  2069  		},
  2070  		{
  2071  			op:     "!=",
  2072  			lVal:   nil,
  2073  			rVal:   psstructs.NewStringAttribute("foo"),
  2074  			result: true,
  2075  		},
  2076  		{
  2077  			op:     "!=",
  2078  			lVal:   psstructs.NewStringAttribute("foo"),
  2079  			rVal:   nil,
  2080  			result: true,
  2081  		},
  2082  		{
  2083  			op:     "!=",
  2084  			lVal:   psstructs.NewStringAttribute("foo"),
  2085  			rVal:   psstructs.NewStringAttribute("bar"),
  2086  			result: true,
  2087  		},
  2088  		{
  2089  			op:     "not",
  2090  			lVal:   psstructs.NewStringAttribute("foo"),
  2091  			rVal:   psstructs.NewStringAttribute("bar"),
  2092  			result: true,
  2093  		},
  2094  		{
  2095  			op:     structs.ConstraintVersion,
  2096  			lVal:   psstructs.NewStringAttribute("1.2.3"),
  2097  			rVal:   psstructs.NewStringAttribute("~> 1.0"),
  2098  			result: true,
  2099  		},
  2100  		{
  2101  			op:     structs.ConstraintRegex,
  2102  			lVal:   psstructs.NewStringAttribute("foobarbaz"),
  2103  			rVal:   psstructs.NewStringAttribute("[\\w]+"),
  2104  			result: true,
  2105  		},
  2106  		{
  2107  			op:     "<",
  2108  			lVal:   psstructs.NewStringAttribute("foo"),
  2109  			rVal:   psstructs.NewStringAttribute("bar"),
  2110  			result: false,
  2111  		},
  2112  		{
  2113  			op:     structs.ConstraintSetContains,
  2114  			lVal:   psstructs.NewStringAttribute("foo,bar,baz"),
  2115  			rVal:   psstructs.NewStringAttribute("foo,  bar  "),
  2116  			result: true,
  2117  		},
  2118  		{
  2119  			op:     structs.ConstraintSetContainsAll,
  2120  			lVal:   psstructs.NewStringAttribute("foo,bar,baz"),
  2121  			rVal:   psstructs.NewStringAttribute("foo,  bar  "),
  2122  			result: true,
  2123  		},
  2124  		{
  2125  			op:     structs.ConstraintSetContains,
  2126  			lVal:   psstructs.NewStringAttribute("foo,bar,baz"),
  2127  			rVal:   psstructs.NewStringAttribute("foo,bam"),
  2128  			result: false,
  2129  		},
  2130  		{
  2131  			op:     structs.ConstraintSetContainsAny,
  2132  			lVal:   psstructs.NewStringAttribute("foo,bar,baz"),
  2133  			rVal:   psstructs.NewStringAttribute("foo,bam"),
  2134  			result: true,
  2135  		},
  2136  		{
  2137  			op:     structs.ConstraintAttributeIsSet,
  2138  			lVal:   psstructs.NewStringAttribute("foo,bar,baz"),
  2139  			result: true,
  2140  		},
  2141  		{
  2142  			op:     structs.ConstraintAttributeIsSet,
  2143  			lVal:   nil,
  2144  			result: false,
  2145  		},
  2146  		{
  2147  			op:     structs.ConstraintAttributeIsNotSet,
  2148  			lVal:   psstructs.NewStringAttribute("foo,bar,baz"),
  2149  			result: false,
  2150  		},
  2151  		{
  2152  			op:     structs.ConstraintAttributeIsNotSet,
  2153  			lVal:   nil,
  2154  			result: true,
  2155  		},
  2156  	}
  2157  
  2158  	for _, tc := range cases {
  2159  		_, ctx := testContext(t)
  2160  		if res := checkAttributeConstraint(ctx, tc.op, tc.lVal, tc.rVal, tc.lVal != nil, tc.rVal != nil); res != tc.result {
  2161  			t.Fatalf("TC: %#v, Result: %v", tc, res)
  2162  		}
  2163  	}
  2164  }