github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/constraints/constraints_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package constraints_test
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"strings"
    10  
    11  	jc "github.com/juju/testing/checkers"
    12  	gc "gopkg.in/check.v1"
    13  	goyaml "gopkg.in/yaml.v2"
    14  
    15  	"github.com/juju/juju/core/constraints"
    16  	"github.com/juju/juju/core/instance"
    17  )
    18  
    19  type ConstraintsSuite struct{}
    20  
    21  var _ = gc.Suite(&ConstraintsSuite{})
    22  
    23  var parseConstraintsTests = []struct {
    24  	summary string
    25  	args    []string
    26  	err     string
    27  	result  *constraints.Value
    28  }{
    29  	// Simple errors.
    30  	{
    31  		summary: "nothing at all",
    32  	}, {
    33  		summary: "empty",
    34  		args:    []string{"     "},
    35  	}, {
    36  		summary: "complete nonsense",
    37  		args:    []string{"cheese"},
    38  		err:     `malformed constraint "cheese"`,
    39  	}, {
    40  		summary: "missing name",
    41  		args:    []string{"=cheese"},
    42  		err:     `malformed constraint "=cheese"`,
    43  	}, {
    44  		summary: "unknown constraint",
    45  		args:    []string{"cheese=edam"},
    46  		err:     `unknown constraint "cheese"`,
    47  	},
    48  
    49  	// "container" in detail.
    50  	{
    51  		summary: "set container empty",
    52  		args:    []string{"container="},
    53  	}, {
    54  		summary: "set container to none",
    55  		args:    []string{"container=none"},
    56  	}, {
    57  		summary: "set container lxd",
    58  		args:    []string{"container=lxd"},
    59  	}, {
    60  		summary: "set nonsense container",
    61  		args:    []string{"container=foo"},
    62  		err:     `bad "container" constraint: invalid container type "foo"`,
    63  	}, {
    64  		summary: "double set container together",
    65  		args:    []string{"container=lxd container=lxd"},
    66  		err:     `bad "container" constraint: already set`,
    67  	}, {
    68  		summary: "double set container separately",
    69  		args:    []string{"container=lxd", "container="},
    70  		err:     `bad "container" constraint: already set`,
    71  	},
    72  
    73  	// "arch" in detail.
    74  	{
    75  		summary: "set arch empty",
    76  		args:    []string{"arch="},
    77  	}, {
    78  		summary: "set arch amd64",
    79  		args:    []string{"arch=amd64"},
    80  	}, {
    81  		summary: "set arch arm64",
    82  		args:    []string{"arch=arm64"},
    83  	}, {
    84  		summary: "set nonsense arch 1",
    85  		args:    []string{"arch=cheese"},
    86  		err:     `bad "arch" constraint: "cheese" not recognized`,
    87  	}, {
    88  		summary: "set nonsense arch 2",
    89  		args:    []string{"arch=123.45"},
    90  		err:     `bad "arch" constraint: "123.45" not recognized`,
    91  	}, {
    92  		summary: "double set arch together",
    93  		args:    []string{"arch=amd64 arch=amd64"},
    94  		err:     `bad "arch" constraint: already set`,
    95  	}, {
    96  		summary: "double set arch separately",
    97  		args:    []string{"arch=arm64", "arch="},
    98  		err:     `bad "arch" constraint: already set`,
    99  	},
   100  
   101  	// "cores" in detail.
   102  	{
   103  		summary: "set cores empty",
   104  		args:    []string{"cores="},
   105  	}, {
   106  		summary: "set cores zero",
   107  		args:    []string{"cores=0"},
   108  	}, {
   109  		summary: "set cores",
   110  		args:    []string{"cores=4"},
   111  	}, {
   112  		summary: "set nonsense cores 1",
   113  		args:    []string{"cores=cheese"},
   114  		err:     `bad "cores" constraint: must be a non-negative integer`,
   115  	}, {
   116  		summary: "set nonsense cores 2",
   117  		args:    []string{"cores=-1"},
   118  		err:     `bad "cores" constraint: must be a non-negative integer`,
   119  	}, {
   120  		summary: "set nonsense cores 3",
   121  		args:    []string{"cores=123.45"},
   122  		err:     `bad "cores" constraint: must be a non-negative integer`,
   123  	}, {
   124  		summary: "double set cores together",
   125  		args:    []string{"cores=128 cores=1"},
   126  		err:     `bad "cores" constraint: already set`,
   127  	}, {
   128  		summary: "double set cores separately",
   129  		args:    []string{"cores=128", "cores=1"},
   130  		err:     `bad "cores" constraint: already set`,
   131  	},
   132  
   133  	// "cpu-cores"
   134  	{
   135  		summary: "set cpu-cores",
   136  		args:    []string{"cpu-cores=4"},
   137  	},
   138  
   139  	// "cpu-power" in detail.
   140  	{
   141  		summary: "set cpu-power empty",
   142  		args:    []string{"cpu-power="},
   143  	}, {
   144  		summary: "set cpu-power zero",
   145  		args:    []string{"cpu-power=0"},
   146  	}, {
   147  		summary: "set cpu-power",
   148  		args:    []string{"cpu-power=44"},
   149  	}, {
   150  		summary: "set nonsense cpu-power 1",
   151  		args:    []string{"cpu-power=cheese"},
   152  		err:     `bad "cpu-power" constraint: must be a non-negative integer`,
   153  	}, {
   154  		summary: "set nonsense cpu-power 2",
   155  		args:    []string{"cpu-power=-1"},
   156  		err:     `bad "cpu-power" constraint: must be a non-negative integer`,
   157  	}, {
   158  		summary: "double set cpu-power together",
   159  		args:    []string{"  cpu-power=300 cpu-power=1700 "},
   160  		err:     `bad "cpu-power" constraint: already set`,
   161  	}, {
   162  		summary: "double set cpu-power separately",
   163  		args:    []string{"cpu-power=300  ", "  cpu-power=1700"},
   164  		err:     `bad "cpu-power" constraint: already set`,
   165  	},
   166  
   167  	// "mem" in detail.
   168  	{
   169  		summary: "set mem empty",
   170  		args:    []string{"mem="},
   171  	}, {
   172  		summary: "set mem zero",
   173  		args:    []string{"mem=0"},
   174  	}, {
   175  		summary: "set mem without suffix",
   176  		args:    []string{"mem=512"},
   177  	}, {
   178  		summary: "set mem with M suffix",
   179  		args:    []string{"mem=512M"},
   180  	}, {
   181  		summary: "set mem with G suffix",
   182  		args:    []string{"mem=1.5G"},
   183  	}, {
   184  		summary: "set mem with T suffix",
   185  		args:    []string{"mem=36.2T"},
   186  	}, {
   187  		summary: "set mem with P suffix",
   188  		args:    []string{"mem=18.9P"},
   189  	}, {
   190  		summary: "set nonsense mem 1",
   191  		args:    []string{"mem=cheese"},
   192  		err:     `bad "mem" constraint: must be a non-negative float with optional M/G/T/P suffix`,
   193  	}, {
   194  		summary: "set nonsense mem 2",
   195  		args:    []string{"mem=-1"},
   196  		err:     `bad "mem" constraint: must be a non-negative float with optional M/G/T/P suffix`,
   197  	}, {
   198  		summary: "set nonsense mem 3",
   199  		args:    []string{"mem=32Y"},
   200  		err:     `bad "mem" constraint: must be a non-negative float with optional M/G/T/P suffix`,
   201  	}, {
   202  		summary: "double set mem together",
   203  		args:    []string{"mem=1G  mem=2G"},
   204  		err:     `bad "mem" constraint: already set`,
   205  	}, {
   206  		summary: "double set mem separately",
   207  		args:    []string{"mem=1G", "mem=2G"},
   208  		err:     `bad "mem" constraint: already set`,
   209  	},
   210  
   211  	// "root-disk" in detail.
   212  	{
   213  		summary: "set root-disk empty",
   214  		args:    []string{"root-disk="},
   215  	}, {
   216  		summary: "set root-disk zero",
   217  		args:    []string{"root-disk=0"},
   218  	}, {
   219  		summary: "set root-disk without suffix",
   220  		args:    []string{"root-disk=512"},
   221  	}, {
   222  		summary: "set root-disk with M suffix",
   223  		args:    []string{"root-disk=512M"},
   224  	}, {
   225  		summary: "set root-disk with G suffix",
   226  		args:    []string{"root-disk=1.5G"},
   227  	}, {
   228  		summary: "set root-disk with T suffix",
   229  		args:    []string{"root-disk=36.2T"},
   230  	}, {
   231  		summary: "set root-disk with P suffix",
   232  		args:    []string{"root-disk=18.9P"},
   233  	}, {
   234  		summary: "set nonsense root-disk 1",
   235  		args:    []string{"root-disk=cheese"},
   236  		err:     `bad "root-disk" constraint: must be a non-negative float with optional M/G/T/P suffix`,
   237  	}, {
   238  		summary: "set nonsense root-disk 2",
   239  		args:    []string{"root-disk=-1"},
   240  		err:     `bad "root-disk" constraint: must be a non-negative float with optional M/G/T/P suffix`,
   241  	}, {
   242  		summary: "set nonsense root-disk 3",
   243  		args:    []string{"root-disk=32Y"},
   244  		err:     `bad "root-disk" constraint: must be a non-negative float with optional M/G/T/P suffix`,
   245  	}, {
   246  		summary: "double set root-disk together",
   247  		args:    []string{"root-disk=1G  root-disk=2G"},
   248  		err:     `bad "root-disk" constraint: already set`,
   249  	}, {
   250  		summary: "double set root-disk separately",
   251  		args:    []string{"root-disk=1G", "root-disk=2G"},
   252  		err:     `bad "root-disk" constraint: already set`,
   253  	},
   254  
   255  	// root-disk-source in detail.
   256  	{
   257  		summary: "set root-disk-source empty",
   258  		args:    []string{"root-disk-source="},
   259  	}, {
   260  		summary: "set root-disk-source to a value",
   261  		args:    []string{"root-disk-source=sourcename"},
   262  	}, {
   263  		summary: "double set root-disk-source together",
   264  		args:    []string{"root-disk-source=sourcename root-disk-source=somethingelse"},
   265  		err:     `bad "root-disk-source" constraint: already set`,
   266  	}, {
   267  		summary: "double set root-disk-source separately",
   268  		args:    []string{"root-disk-source=sourcename", "root-disk-source=somethingelse"},
   269  		err:     `bad "root-disk-source" constraint: already set`,
   270  	},
   271  
   272  	// tags
   273  	{
   274  		summary: "single tag",
   275  		args:    []string{"tags=foo"},
   276  	}, {
   277  		summary: "multiple tags",
   278  		args:    []string{"tags=foo,bar"},
   279  	}, {
   280  		summary: "no tags",
   281  		args:    []string{"tags="},
   282  	},
   283  
   284  	// spaces
   285  	{
   286  		summary: "single space",
   287  		args:    []string{"spaces=space1"},
   288  	}, {
   289  		summary: "multiple spaces - positive",
   290  		args:    []string{"spaces=space1,space2"},
   291  	}, {
   292  		summary: "multiple spaces - negative",
   293  		args:    []string{"spaces=^dmz,^public"},
   294  	}, {
   295  		summary: "multiple spaces - positive and negative",
   296  		args:    []string{"spaces=admin,^area52,dmz,^public"},
   297  	}, {
   298  		summary: "no spaces",
   299  		args:    []string{"spaces="},
   300  	},
   301  
   302  	// instance roles
   303  	{
   304  		summary: "set instance role",
   305  		args:    []string{"instance-role=foobarir"},
   306  	}, {
   307  		summary: "instance role empty",
   308  		args:    []string{"instance-role="},
   309  	}, {
   310  		summary: "instance role auto",
   311  		args:    []string{"instance-role=auto"},
   312  	},
   313  
   314  	// instance type
   315  	{
   316  		summary: "set instance type",
   317  		args:    []string{"instance-type=foo"},
   318  	}, {
   319  		summary: "instance type empty",
   320  		args:    []string{"instance-type="},
   321  	}, {
   322  		summary: "instance type with slash-escaped spaces",
   323  		args:    []string{`instance-type=something\ with\ spaces`},
   324  	},
   325  
   326  	// "virt-type" in detail.
   327  	{
   328  		summary: "set virt-type empty",
   329  		args:    []string{"virt-type="},
   330  	}, {
   331  		summary: "set virt-type kvm",
   332  		args:    []string{"virt-type=kvm"},
   333  	}, {
   334  		summary: "set virt-type lxd",
   335  		args:    []string{"virt-type=lxd"},
   336  	}, {
   337  		summary: "double set virt-type together",
   338  		args:    []string{"virt-type=kvm virt-type=kvm"},
   339  		err:     `bad "virt-type" constraint: already set`,
   340  	}, {
   341  		summary: "double set virt-type separately",
   342  		args:    []string{"virt-type=kvm", "virt-type="},
   343  		err:     `bad "virt-type" constraint: already set`,
   344  	},
   345  
   346  	// Zones
   347  	{
   348  		summary: "single zone",
   349  		args:    []string{"zones=az1"},
   350  	}, {
   351  		summary: "multiple zones",
   352  		args:    []string{"zones=az1,az2"},
   353  	}, {
   354  		summary: "zones with slash-escaped spaces",
   355  		args:    []string{`zones=Availability\ zone\ 1`},
   356  	}, {
   357  		summary: "Multiple zones with slash-escaped spaces",
   358  		args:    []string{`zones=Availability\ zone\ 1,Availability\ zone\ 2,az2`},
   359  	}, {
   360  		summary: "no zones",
   361  		args:    []string{"zones="},
   362  	},
   363  
   364  	// AllocatePublicIP
   365  	{
   366  		summary: "set allocate-public-ip",
   367  		args:    []string{"allocate-public-ip=true"},
   368  	}, {
   369  		summary: "set nonsense allocate-public-ip",
   370  		args:    []string{"allocate-public-ip=fred"},
   371  		err:     `bad "allocate-public-ip" constraint: must be 'true' or 'false'`,
   372  	}, {
   373  		summary: "try to set allocate-public-ip twice",
   374  		args:    []string{"allocate-public-ip=true allocate-public-ip=false"},
   375  		err:     `bad "allocate-public-ip" constraint: already set`,
   376  	},
   377  
   378  	// ImageID
   379  	{
   380  		summary: "set image-id",
   381  		args:    []string{"image-id=ubuntu-bf2"},
   382  	},
   383  	{
   384  		summary: "set image-id",
   385  		args:    []string{"image-id="},
   386  	},
   387  	{
   388  		summary: "set image-id",
   389  		args:    []string{"image-id=ubuntu-bf2 image-id=ubuntu-bf1"},
   390  		err:     `bad "image-id" constraint: already set`,
   391  	},
   392  
   393  	// Everything at once.
   394  	{
   395  		summary: "kitchen sink together",
   396  		args: []string{
   397  			"root-disk=8G mem=2T  arch=arm64  cores=4096 cpu-power=9001 container=lxd " +
   398  				"tags=foo,bar spaces=space1,^space2 instance-type=foo " +
   399  				"instance-role=foo1",
   400  			"virt-type=kvm zones=az1,az2 allocate-public-ip=true root-disk-source=sourcename image-id=ubuntu-bf2"},
   401  		result: &constraints.Value{
   402  			Arch:             strp("arm64"),
   403  			Container:        (*instance.ContainerType)(strp("lxd")),
   404  			CpuCores:         uint64p(4096),
   405  			CpuPower:         uint64p(9001),
   406  			Mem:              uint64p(2 * 1024 * 1024),
   407  			RootDisk:         uint64p(8192),
   408  			RootDiskSource:   strp("sourcename"),
   409  			Tags:             &[]string{"foo", "bar"},
   410  			Spaces:           &[]string{"space1", "^space2"},
   411  			InstanceRole:     strp("foo1"),
   412  			InstanceType:     strp("foo"),
   413  			VirtType:         strp("kvm"),
   414  			Zones:            &[]string{"az1", "az2"},
   415  			AllocatePublicIP: boolp(true),
   416  			ImageID:          strp("ubuntu-bf2"),
   417  		},
   418  	}, {
   419  		summary: "kitchen sink separately",
   420  		args: []string{
   421  			"root-disk=8G", "mem=2T", "cores=4096", "cpu-power=9001", "arch=arm64",
   422  			"container=lxd", "tags=foo,bar", "spaces=space1,^space2",
   423  			"instance-type=foo", "virt-type=kvm", "zones=az1,az2", "allocate-public-ip=false",
   424  			"instance-role=foo2"},
   425  		result: &constraints.Value{
   426  			Arch:             strp("arm64"),
   427  			Container:        (*instance.ContainerType)(strp("lxd")),
   428  			CpuCores:         uint64p(4096),
   429  			CpuPower:         uint64p(9001),
   430  			Mem:              uint64p(2 * 1024 * 1024),
   431  			RootDisk:         uint64p(8192),
   432  			Tags:             &[]string{"foo", "bar"},
   433  			Spaces:           &[]string{"space1", "^space2"},
   434  			InstanceRole:     strp("foo2"),
   435  			InstanceType:     strp("foo"),
   436  			VirtType:         strp("kvm"),
   437  			Zones:            &[]string{"az1", "az2"},
   438  			AllocatePublicIP: boolp(false),
   439  		},
   440  	}, {
   441  		summary: "kitchen sink together with spaced zones",
   442  		args: []string{
   443  			`root-disk=8G mem=2T  arch=arm64  cores=4096 zones=Availability\ zone\ 1 cpu-power=9001 container=lxd ` +
   444  				"tags=foo,bar spaces=space1,^space2 instance-type=foo instance-role=foo3",
   445  			"virt-type=kvm"},
   446  		result: &constraints.Value{
   447  			Arch:         strp("arm64"),
   448  			Container:    (*instance.ContainerType)(strp("lxd")),
   449  			CpuCores:     uint64p(4096),
   450  			CpuPower:     uint64p(9001),
   451  			Mem:          uint64p(2 * 1024 * 1024),
   452  			RootDisk:     uint64p(8192),
   453  			Tags:         &[]string{"foo", "bar"},
   454  			Spaces:       &[]string{"space1", "^space2"},
   455  			InstanceRole: strp("foo3"),
   456  			InstanceType: strp("foo"),
   457  			VirtType:     strp("kvm"),
   458  			Zones:        &[]string{"Availability zone 1"},
   459  		},
   460  	},
   461  }
   462  
   463  func (s *ConstraintsSuite) TestParseConstraints(c *gc.C) {
   464  	// TODO(dimitern): This test is inadequate and needs to check for
   465  	// more than just the reparsed output of String() matches the
   466  	// expected.
   467  	for i, t := range parseConstraintsTests {
   468  		c.Logf("test %d: %s", i, t.summary)
   469  		cons0, err := constraints.Parse(t.args...)
   470  		if t.err == "" {
   471  			c.Assert(err, jc.ErrorIsNil)
   472  		} else {
   473  			c.Assert(err, gc.ErrorMatches, t.err)
   474  			continue
   475  		}
   476  		if t.result != nil {
   477  			c.Check(cons0, gc.DeepEquals, *t.result)
   478  		}
   479  		cons1, err := constraints.Parse(cons0.String())
   480  		c.Check(err, jc.ErrorIsNil)
   481  		c.Check(cons1, gc.DeepEquals, cons0)
   482  	}
   483  }
   484  
   485  func (s *ConstraintsSuite) TestParseAliases(c *gc.C) {
   486  	v, aliases, err := constraints.ParseWithAliases("cpu-cores=5 arch=amd64")
   487  	c.Assert(err, jc.ErrorIsNil)
   488  	c.Assert(v, gc.DeepEquals, constraints.Value{
   489  		CpuCores: uint64p(5),
   490  		Arch:     strp("amd64"),
   491  	})
   492  	c.Assert(aliases, gc.DeepEquals, map[string]string{
   493  		"cpu-cores": "cores",
   494  	})
   495  }
   496  
   497  func (s *ConstraintsSuite) TestMerge(c *gc.C) {
   498  	con1 := constraints.MustParse("arch=amd64 mem=4G")
   499  	con2 := constraints.MustParse("cores=42")
   500  	con3 := constraints.MustParse(
   501  		"root-disk=8G container=lxd spaces=space1,^space2 image-id=ubuntu-bf2",
   502  	)
   503  	merged, err := constraints.Merge(con1, con2)
   504  	c.Assert(err, jc.ErrorIsNil)
   505  	c.Assert(merged, jc.DeepEquals, constraints.MustParse("arch=amd64 mem=4G cores=42"))
   506  	merged, err = constraints.Merge(con1)
   507  	c.Assert(err, jc.ErrorIsNil)
   508  	c.Assert(merged, jc.DeepEquals, con1)
   509  	merged, err = constraints.Merge(con1, con2, con3)
   510  	c.Assert(err, jc.ErrorIsNil)
   511  	c.Assert(merged, jc.DeepEquals, constraints.
   512  		MustParse("arch=amd64 mem=4G cores=42 root-disk=8G container=lxd spaces=space1,^space2 image-id=ubuntu-bf2"),
   513  	)
   514  	merged, err = constraints.Merge()
   515  	c.Assert(err, jc.ErrorIsNil)
   516  	c.Assert(merged, jc.DeepEquals, constraints.Value{})
   517  	foo := "foo"
   518  	merged, err = constraints.Merge(constraints.Value{Arch: &foo}, con2)
   519  	c.Assert(err, gc.NotNil)
   520  	c.Assert(err, gc.ErrorMatches, `bad "arch" constraint: "foo" not recognized`)
   521  	c.Assert(merged, jc.DeepEquals, constraints.Value{})
   522  	merged, err = constraints.Merge(con1, con1)
   523  	c.Assert(err, gc.NotNil)
   524  	c.Assert(err, gc.ErrorMatches, `bad "arch" constraint: already set`)
   525  	c.Assert(merged, jc.DeepEquals, constraints.Value{})
   526  }
   527  
   528  func (s *ConstraintsSuite) TestParseInstanceTypeWithSpaces(c *gc.C) {
   529  	con := constraints.MustParse(
   530  		`arch=amd64 instance-type=with\ spaces cores=1`,
   531  	)
   532  	c.Assert(con.Arch, gc.Not(gc.IsNil))
   533  	c.Assert(con.InstanceType, gc.Not(gc.IsNil))
   534  	c.Assert(con.CpuCores, gc.Not(gc.IsNil))
   535  	c.Check(*con.Arch, gc.Equals, "amd64")
   536  	c.Check(*con.InstanceType, gc.Equals, "with spaces")
   537  	c.Check(*con.CpuCores, gc.Equals, uint64(1))
   538  }
   539  
   540  func (s *ConstraintsSuite) TestParseMissingTagsAndSpaces(c *gc.C) {
   541  	con := constraints.MustParse("arch=amd64 mem=4G cores=1 root-disk=8G")
   542  	c.Check(con.Tags, gc.IsNil)
   543  	c.Check(con.Spaces, gc.IsNil)
   544  }
   545  
   546  func (s *ConstraintsSuite) TestParseNoTagsNoSpaces(c *gc.C) {
   547  	con := constraints.MustParse(
   548  		"arch=amd64 mem=4G cores=1 root-disk=8G tags= spaces=",
   549  	)
   550  	c.Assert(con.Tags, gc.Not(gc.IsNil))
   551  	c.Assert(con.Spaces, gc.Not(gc.IsNil))
   552  	c.Check(*con.Tags, gc.HasLen, 0)
   553  	c.Check(*con.Spaces, gc.HasLen, 0)
   554  }
   555  
   556  func (s *ConstraintsSuite) TestIncludeExcludeAndHasSpaces(c *gc.C) {
   557  	con := constraints.MustParse("spaces=space1,^space2,space3,^space4")
   558  	c.Assert(con.Spaces, gc.Not(gc.IsNil))
   559  	c.Check(*con.Spaces, gc.HasLen, 4)
   560  	c.Check(con.IncludeSpaces(), jc.SameContents, []string{"space1", "space3"})
   561  	c.Check(con.ExcludeSpaces(), jc.SameContents, []string{"space2", "space4"})
   562  	c.Check(con.HasSpaces(), jc.IsTrue)
   563  	con = constraints.MustParse("mem=4G")
   564  	c.Check(con.HasSpaces(), jc.IsFalse)
   565  	con = constraints.MustParse("mem=4G spaces=space-foo,^space-bar")
   566  	c.Check(con.IncludeSpaces(), jc.SameContents, []string{"space-foo"})
   567  	c.Check(con.ExcludeSpaces(), jc.SameContents, []string{"space-bar"})
   568  	c.Check(con.HasSpaces(), jc.IsTrue)
   569  }
   570  
   571  func (s *ConstraintsSuite) TestInvalidSpaces(c *gc.C) {
   572  	invalidNames := []string{
   573  		"%$pace", "^foo#2", "+", "tcp:ip",
   574  		"^^myspace", "^^^^^^^^", "space^x",
   575  		"&-foo", "space/3", "^bar=4", "&#!",
   576  	}
   577  	for _, name := range invalidNames {
   578  		con, err := constraints.Parse("spaces=" + name)
   579  		expectName := strings.TrimPrefix(name, "^")
   580  		expectErr := fmt.Sprintf(`bad "spaces" constraint: %q is not a valid space name`, expectName)
   581  		c.Check(err, gc.NotNil)
   582  		c.Check(err.Error(), gc.Equals, expectErr)
   583  		c.Check(con, jc.DeepEquals, constraints.Value{})
   584  	}
   585  }
   586  
   587  func (s *ConstraintsSuite) TestHasZones(c *gc.C) {
   588  	con := constraints.MustParse("zones=az1,az2,az3")
   589  	c.Assert(con.Zones, gc.Not(gc.IsNil))
   590  	c.Check(*con.Zones, gc.HasLen, 3)
   591  	c.Check(con.HasZones(), jc.IsTrue)
   592  
   593  	con = constraints.MustParse("zones=")
   594  	c.Check(con.HasZones(), jc.IsFalse)
   595  
   596  	con = constraints.MustParse("spaces=space1,^space2")
   597  	c.Check(con.HasZones(), jc.IsFalse)
   598  }
   599  
   600  func (s *ConstraintsSuite) TestHasAllocatePublicIP(c *gc.C) {
   601  	con := constraints.MustParse("allocate-public-ip=true")
   602  	c.Assert(con.AllocatePublicIP, gc.Not(gc.IsNil))
   603  	c.Check(con.HasAllocatePublicIP(), jc.IsTrue)
   604  
   605  	con = constraints.MustParse("allocate-public-ip=")
   606  	c.Check(con.HasAllocatePublicIP(), jc.IsFalse)
   607  
   608  	con = constraints.MustParse("spaces=space1,^space2")
   609  	c.Check(con.HasAllocatePublicIP(), jc.IsFalse)
   610  }
   611  
   612  func (s *ConstraintsSuite) TestHasRootDiskSource(c *gc.C) {
   613  	con := constraints.MustParse("root-disk-source=pilgrim")
   614  	c.Check(con.HasRootDiskSource(), jc.IsTrue)
   615  	con = constraints.MustParse("root-disk-source=")
   616  	c.Check(con.HasRootDiskSource(), jc.IsFalse)
   617  	con = constraints.MustParse("root-disk=32G")
   618  	c.Check(con.HasRootDiskSource(), jc.IsFalse)
   619  }
   620  
   621  func (s *ConstraintsSuite) TestHasRootDisk(c *gc.C) {
   622  	con := constraints.MustParse("root-disk=32G")
   623  	c.Check(con.HasRootDisk(), jc.IsTrue)
   624  	con = constraints.MustParse("root-disk=")
   625  	c.Check(con.HasRootDisk(), jc.IsFalse)
   626  	con = constraints.MustParse("root-disk-source=pilgrim")
   627  	c.Check(con.HasRootDisk(), jc.IsFalse)
   628  }
   629  
   630  func (s *ConstraintsSuite) TestHasImageID(c *gc.C) {
   631  	con := constraints.MustParse("image-id=ubuntu-bf2")
   632  	c.Check(con.HasImageID(), jc.IsTrue)
   633  	con = constraints.MustParse("image-id=")
   634  	c.Check(con.HasImageID(), jc.IsFalse)
   635  	con = constraints.MustParse("spaces=space1,^space2")
   636  	c.Check(con.HasImageID(), jc.IsFalse)
   637  }
   638  
   639  func (s *ConstraintsSuite) TestIsEmpty(c *gc.C) {
   640  	con := constraints.Value{}
   641  	c.Check(&con, jc.Satisfies, constraints.IsEmpty)
   642  	con = constraints.MustParse("arch=amd64")
   643  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   644  	con = constraints.MustParse("")
   645  	c.Check(&con, jc.Satisfies, constraints.IsEmpty)
   646  	con = constraints.MustParse("tags=")
   647  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   648  	con = constraints.MustParse("spaces=")
   649  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   650  	con = constraints.MustParse("mem=")
   651  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   652  	con = constraints.MustParse("arch=")
   653  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   654  	con = constraints.MustParse("root-disk=")
   655  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   656  	con = constraints.MustParse("cpu-power=")
   657  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   658  	con = constraints.MustParse("cores=")
   659  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   660  	con = constraints.MustParse("container=")
   661  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   662  	con = constraints.MustParse("instance-role=")
   663  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   664  	con = constraints.MustParse("instance-type=")
   665  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   666  	con = constraints.MustParse("zones=")
   667  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   668  	con = constraints.MustParse("allocate-public-ip=")
   669  	c.Check(&con, jc.Satisfies, constraints.IsEmpty)
   670  	con = constraints.MustParse("image-id=")
   671  	c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty)
   672  }
   673  
   674  func boolp(b bool) *bool {
   675  	return &b
   676  }
   677  
   678  func uint64p(i uint64) *uint64 {
   679  	return &i
   680  }
   681  
   682  func strp(s string) *string {
   683  	return &s
   684  }
   685  
   686  func ctypep(ctype string) *instance.ContainerType {
   687  	res := instance.ContainerType(ctype)
   688  	return &res
   689  }
   690  
   691  type roundTrip struct {
   692  	Name  string
   693  	Value constraints.Value
   694  }
   695  
   696  var constraintsRoundtripTests = []roundTrip{
   697  	{"empty", constraints.Value{}},
   698  	{"Arch1", constraints.Value{Arch: strp("")}},
   699  	{"Arch2", constraints.Value{Arch: strp("amd64")}},
   700  	{"Container1", constraints.Value{Container: ctypep("")}},
   701  	{"Container2", constraints.Value{Container: ctypep("lxd")}},
   702  	{"Container3", constraints.Value{Container: nil}},
   703  	{"CpuCores1", constraints.Value{CpuCores: nil}},
   704  	{"CpuCores2", constraints.Value{CpuCores: uint64p(0)}},
   705  	{"CpuCores3", constraints.Value{CpuCores: uint64p(128)}},
   706  	{"CpuPower1", constraints.Value{CpuPower: nil}},
   707  	{"CpuPower2", constraints.Value{CpuPower: uint64p(0)}},
   708  	{"CpuPower3", constraints.Value{CpuPower: uint64p(250)}},
   709  	{"Mem1", constraints.Value{Mem: nil}},
   710  	{"Mem2", constraints.Value{Mem: uint64p(0)}},
   711  	{"Mem3", constraints.Value{Mem: uint64p(98765)}},
   712  	{"RootDisk1", constraints.Value{RootDisk: nil}},
   713  	{"RootDisk2", constraints.Value{RootDisk: uint64p(0)}},
   714  	{"RootDisk2", constraints.Value{RootDisk: uint64p(109876)}},
   715  	{"RootDiskSource1", constraints.Value{RootDiskSource: nil}},
   716  	{"RootDiskSource2", constraints.Value{RootDiskSource: strp("identikit")}},
   717  	{"Tags1", constraints.Value{Tags: nil}},
   718  	{"Tags2", constraints.Value{Tags: &[]string{}}},
   719  	{"Tags3", constraints.Value{Tags: &[]string{"foo", "bar"}}},
   720  	{"Spaces1", constraints.Value{Spaces: nil}},
   721  	{"Spaces2", constraints.Value{Spaces: &[]string{}}},
   722  	{"Spaces3", constraints.Value{Spaces: &[]string{"space1", "^space2"}}},
   723  	{"InstanceRole1", constraints.Value{InstanceRole: strp("")}},
   724  	{"InstanceRole2", constraints.Value{InstanceRole: strp("foo")}},
   725  	{"InstanceType1", constraints.Value{InstanceType: strp("")}},
   726  	{"InstanceType2", constraints.Value{InstanceType: strp("foo")}},
   727  	{"Zones1", constraints.Value{Zones: nil}},
   728  	{"Zones2", constraints.Value{Zones: &[]string{}}},
   729  	{"Zones3", constraints.Value{Zones: &[]string{"az1", "az2"}}},
   730  	{"AllocatePublicIP1", constraints.Value{AllocatePublicIP: nil}},
   731  	{"AllocatePublicIP2", constraints.Value{AllocatePublicIP: boolp(true)}},
   732  	{"ImageID1", constraints.Value{ImageID: nil}},
   733  	{"ImageID1", constraints.Value{ImageID: strp("")}},
   734  	{"ImageID1", constraints.Value{ImageID: strp("ubuntu-bf2")}},
   735  	{"All", constraints.Value{
   736  		Arch:             strp("arm64"),
   737  		Container:        ctypep("lxd"),
   738  		CpuCores:         uint64p(4096),
   739  		CpuPower:         uint64p(9001),
   740  		Mem:              uint64p(18000000000),
   741  		RootDisk:         uint64p(24000000000),
   742  		RootDiskSource:   strp("cave"),
   743  		Tags:             &[]string{"foo", "bar"},
   744  		Spaces:           &[]string{"space1", "^space2"},
   745  		InstanceType:     strp("foo"),
   746  		Zones:            &[]string{"az1", "az2"},
   747  		AllocatePublicIP: boolp(true),
   748  		ImageID:          strp("ubuntu-bf2"),
   749  	}},
   750  }
   751  
   752  func (s *ConstraintsSuite) TestRoundtripGnuflagValue(c *gc.C) {
   753  	for _, t := range constraintsRoundtripTests {
   754  		c.Logf("test %s", t.Name)
   755  		var cons constraints.Value
   756  		val := constraints.ConstraintsValue{&cons}
   757  		err := val.Set(t.Value.String())
   758  		c.Check(err, jc.ErrorIsNil)
   759  		c.Check(cons, jc.DeepEquals, t.Value)
   760  	}
   761  }
   762  
   763  func (s *ConstraintsSuite) TestRoundtripString(c *gc.C) {
   764  	for _, t := range constraintsRoundtripTests {
   765  		c.Logf("test %s", t.Name)
   766  		cons, err := constraints.Parse(t.Value.String())
   767  		c.Check(err, jc.ErrorIsNil)
   768  		c.Check(cons, jc.DeepEquals, t.Value)
   769  	}
   770  }
   771  
   772  func (s *ConstraintsSuite) TestRoundtripJson(c *gc.C) {
   773  	for _, t := range constraintsRoundtripTests {
   774  		c.Logf("test %s", t.Name)
   775  		data, err := json.Marshal(t.Value)
   776  		c.Assert(err, jc.ErrorIsNil)
   777  		var cons constraints.Value
   778  		err = json.Unmarshal(data, &cons)
   779  		c.Check(err, jc.ErrorIsNil)
   780  		c.Check(cons, jc.DeepEquals, t.Value)
   781  	}
   782  }
   783  
   784  func (s *ConstraintsSuite) TestRoundtripYaml(c *gc.C) {
   785  	for _, t := range constraintsRoundtripTests {
   786  		c.Logf("test %s", t.Name)
   787  		data, err := goyaml.Marshal(t.Value)
   788  		c.Assert(err, jc.ErrorIsNil)
   789  		var cons constraints.Value
   790  		err = goyaml.Unmarshal(data, &cons)
   791  		c.Check(err, jc.ErrorIsNil)
   792  		c.Check(cons, jc.DeepEquals, t.Value)
   793  	}
   794  }
   795  
   796  var hasContainerTests = []struct {
   797  	constraints  string
   798  	hasContainer bool
   799  }{
   800  	{
   801  		hasContainer: false,
   802  	}, {
   803  		constraints:  "container=lxd",
   804  		hasContainer: true,
   805  	}, {
   806  		constraints:  "container=none",
   807  		hasContainer: false,
   808  	},
   809  }
   810  
   811  func (s *ConstraintsSuite) TestHasContainer(c *gc.C) {
   812  	for i, t := range hasContainerTests {
   813  		c.Logf("test %d", i)
   814  		cons := constraints.MustParse(t.constraints)
   815  		c.Check(cons.HasContainer(), gc.Equals, t.hasContainer)
   816  	}
   817  }
   818  
   819  func (s *ConstraintsSuite) TestHasInstanceType(c *gc.C) {
   820  	cons := constraints.MustParse("arch=amd64")
   821  	c.Check(cons.HasInstanceType(), jc.IsFalse)
   822  	cons = constraints.MustParse("arch=amd64 instance-type=foo")
   823  	c.Check(cons.HasInstanceType(), jc.IsTrue)
   824  }
   825  
   826  const initialWithoutCons = "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 spaces=space1,^space2 tags=foo " +
   827  	"container=lxd instance-type=bar zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2"
   828  
   829  var withoutTests = []struct {
   830  	initial string
   831  	without []string
   832  	final   string
   833  }{{
   834  	initial: initialWithoutCons,
   835  	without: []string{"root-disk"},
   836  	final:   "mem=4G arch=amd64 cpu-power=1000 cores=4 tags=foo spaces=space1,^space2 container=lxd instance-type=bar  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   837  }, {
   838  	initial: initialWithoutCons,
   839  	without: []string{"mem"},
   840  	final:   "root-disk=8G arch=amd64 cpu-power=1000 cores=4 tags=foo spaces=space1,^space2 container=lxd instance-type=bar  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   841  }, {
   842  	initial: initialWithoutCons,
   843  	without: []string{"arch"},
   844  	final:   "root-disk=8G mem=4G cpu-power=1000 cores=4 tags=foo spaces=space1,^space2 container=lxd instance-type=bar zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   845  }, {
   846  	initial: initialWithoutCons,
   847  	without: []string{"cpu-power"},
   848  	final:   "root-disk=8G mem=4G arch=amd64 cores=4 tags=foo spaces=space1,^space2 container=lxd instance-type=bar  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   849  }, {
   850  	initial: initialWithoutCons,
   851  	without: []string{"cores"},
   852  	final:   "root-disk=8G mem=4G arch=amd64 cpu-power=1000 tags=foo spaces=space1,^space2 container=lxd instance-type=bar  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   853  }, {
   854  	initial: initialWithoutCons,
   855  	without: []string{"tags"},
   856  	final:   "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 spaces=space1,^space2 container=lxd instance-type=bar  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   857  }, {
   858  	initial: initialWithoutCons,
   859  	without: []string{"spaces"},
   860  	final:   "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 tags=foo container=lxd instance-type=bar  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   861  }, {
   862  	initial: initialWithoutCons,
   863  	without: []string{"container"},
   864  	final:   "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 tags=foo spaces=space1,^space2 instance-type=bar  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   865  }, {
   866  	initial: initialWithoutCons,
   867  	without: []string{"instance-type"},
   868  	final:   "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 tags=foo spaces=space1,^space2 container=lxd  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   869  }, {
   870  	initial: initialWithoutCons,
   871  	without: []string{"root-disk", "mem", "arch"},
   872  	final:   "cpu-power=1000 cores=4 tags=foo spaces=space1,^space2 container=lxd instance-type=bar  zones=az1,az2 allocate-public-ip=true image-id=ubuntu-bf2",
   873  }, {
   874  	initial: initialWithoutCons,
   875  	without: []string{"zones"},
   876  	final:   "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 spaces=space1,^space2 tags=foo container=lxd instance-type=bar allocate-public-ip=true image-id=ubuntu-bf2",
   877  }, {
   878  	initial: initialWithoutCons,
   879  	without: []string{"allocate-public-ip"},
   880  	final:   "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 spaces=space1,^space2 tags=foo container=lxd instance-type=bar zones=az1,az2 image-id=ubuntu-bf2",
   881  }, {
   882  	initial: initialWithoutCons,
   883  	without: []string{"image-id"},
   884  	final:   "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 spaces=space1,^space2 tags=foo container=lxd instance-type=bar zones=az1,az2 allocate-public-ip=true",
   885  }}
   886  
   887  func (s *ConstraintsSuite) TestWithout(c *gc.C) {
   888  	for i, t := range withoutTests {
   889  		c.Logf("test %d", i)
   890  		initial := constraints.MustParse(t.initial)
   891  		final := constraints.Without(initial, t.without...)
   892  		c.Check(final, jc.DeepEquals, constraints.MustParse(t.final))
   893  	}
   894  }
   895  
   896  var hasAnyTests = []struct {
   897  	cons     string
   898  	attrs    []string
   899  	expected []string
   900  }{
   901  	{
   902  		cons:     "root-disk=8G mem=4G arch=amd64 cpu-power=1000 spaces=space1,^space2 cores=4",
   903  		attrs:    []string{"root-disk", "tags", "mem", "spaces"},
   904  		expected: []string{"root-disk", "mem", "spaces"},
   905  	},
   906  	{
   907  		cons:     "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4 image-id=ubuntu-bf2",
   908  		attrs:    []string{"root-disk", "tags", "mem", "image-id"},
   909  		expected: []string{"root-disk", "mem", "image-id"},
   910  	},
   911  	{
   912  		cons:     "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cores=4",
   913  		attrs:    []string{"tags", "spaces"},
   914  		expected: []string{},
   915  	},
   916  }
   917  
   918  func (s *ConstraintsSuite) TestHasAny(c *gc.C) {
   919  	for i, t := range hasAnyTests {
   920  		c.Logf("test %d", i)
   921  		cons := constraints.MustParse(t.cons)
   922  		obtained := constraints.HasAny(cons, t.attrs...)
   923  		c.Check(obtained, jc.DeepEquals, t.expected)
   924  	}
   925  }