github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/model/project_matrix_test.go (about)

     1  package model
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/evergreen-ci/evergreen/command"
     8  	. "github.com/smartystreets/goconvey/convey"
     9  )
    10  
    11  func TestMatrixIntermediateParsing(t *testing.T) {
    12  	Convey("Testing different project files with matrix definitions", t, func() {
    13  		Convey("a set of axes should parse", func() {
    14  			axes := `
    15  axes:
    16  - id: os
    17    display_name: Operating System
    18    values:
    19    - id: ubuntu
    20      display_name: Ubuntu
    21      tags: "linux"
    22      variables:
    23        user: root
    24      run_on: ubuntu_small
    25    - id: rhel
    26      display_name: Red Hat
    27      tags: ["linux", "enterprise"]
    28      run_on:
    29      - rhel55
    30      - rhel62
    31  `
    32  			p, errs := createIntermediateProject([]byte(axes))
    33  			So(errs, ShouldBeNil)
    34  			axis := p.Axes[0]
    35  			So(axis.Id, ShouldEqual, "os")
    36  			So(axis.DisplayName, ShouldEqual, "Operating System")
    37  			So(len(axis.Values), ShouldEqual, 2)
    38  			So(axis.Values[0], ShouldResemble, axisValue{
    39  				Id:          "ubuntu",
    40  				DisplayName: "Ubuntu",
    41  				Tags:        []string{"linux"},
    42  				Variables:   map[string]string{"user": "root"},
    43  				RunOn:       []string{"ubuntu_small"},
    44  			})
    45  			So(axis.Values[1], ShouldResemble, axisValue{
    46  				Id:          "rhel",
    47  				DisplayName: "Red Hat",
    48  				Tags:        []string{"linux", "enterprise"},
    49  				RunOn:       []string{"rhel55", "rhel62"},
    50  			})
    51  		})
    52  		Convey("a barebones matrix definition should parse", func() {
    53  			simple := `
    54  buildvariants:
    55  - matrix_name: "test"
    56    matrix_spec: {"os": ".linux", "bits":["32", "64"]}
    57    exclude_spec: [{"os":"ubuntu", "bits":"32"}]
    58  - matrix_name: "test2"
    59    matrix_spec:
    60      os: "windows95"
    61      color:
    62      - red
    63      - blue
    64      - green
    65  `
    66  			p, errs := createIntermediateProject([]byte(simple))
    67  			So(errs, ShouldBeNil)
    68  			So(len(p.BuildVariants), ShouldEqual, 2)
    69  			m1 := *p.BuildVariants[0].matrix
    70  			So(m1, ShouldResemble, matrix{
    71  				Id: "test",
    72  				Spec: matrixDefinition{
    73  					"os":   []string{".linux"},
    74  					"bits": []string{"32", "64"},
    75  				},
    76  				Exclude: []matrixDefinition{
    77  					{"os": []string{"ubuntu"}, "bits": []string{"32"}},
    78  				},
    79  			})
    80  			m2 := *p.BuildVariants[1].matrix
    81  			So(m2, ShouldResemble, matrix{
    82  				Id: "test2",
    83  				Spec: matrixDefinition{
    84  					"os":    []string{"windows95"},
    85  					"color": []string{"red", "blue", "green"},
    86  				},
    87  			})
    88  		})
    89  		Convey("a mixed definition should parse", func() {
    90  			simple := `
    91  buildvariants:
    92  - matrix_name: "test"
    93    matrix_spec: {"os": "*", "bits": "*"}
    94  - name: "single_variant"
    95    tasks: "*"
    96  `
    97  			p, errs := createIntermediateProject([]byte(simple))
    98  			So(errs, ShouldBeNil)
    99  			So(len(p.BuildVariants), ShouldEqual, 2)
   100  			m1 := *p.BuildVariants[0].matrix
   101  			So(m1.Id, ShouldEqual, "test")
   102  			So(p.BuildVariants[1].Name, ShouldEqual, "single_variant")
   103  			So(p.BuildVariants[1].Tasks, ShouldResemble, parserBVTasks{parserBVTask{Name: "*"}})
   104  		})
   105  	})
   106  }
   107  
   108  func TestMatrixDefinitionAllCells(t *testing.T) {
   109  	Convey("With a set of test definitions", t, func() {
   110  		Convey("an empty definition should return an empty list", func() {
   111  			a := matrixDefinition{}
   112  			cells := a.allCells()
   113  			So(len(cells), ShouldEqual, 0)
   114  		})
   115  		Convey("an empty axis should cause a panic", func() {
   116  			a := matrixDefinition{
   117  				"a": []string{},
   118  				"b": []string{"1"},
   119  			}
   120  			So(func() { a.allCells() }, ShouldPanic)
   121  		})
   122  		Convey("a one-cell matrix should return a one-item list", func() {
   123  			a := matrixDefinition{
   124  				"a": []string{"0"},
   125  			}
   126  			cells := a.allCells()
   127  			So(len(cells), ShouldEqual, 1)
   128  			So(cells, ShouldContainResembling, matrixValue{"a": "0"})
   129  			b := matrixDefinition{
   130  				"a": []string{"0"},
   131  				"b": []string{"1"},
   132  				"c": []string{"2"},
   133  			}
   134  			cells = b.allCells()
   135  			So(len(cells), ShouldEqual, 1)
   136  			So(cells, ShouldContainResembling, matrixValue{"a": "0", "b": "1", "c": "2"})
   137  		})
   138  		Convey("a one-axis matrix should return an equivalent list", func() {
   139  			a := matrixDefinition{
   140  				"a": []string{"0", "1", "2"},
   141  			}
   142  			cells := a.allCells()
   143  			So(len(cells), ShouldEqual, 3)
   144  			So(cells, ShouldContainResembling, matrixValue{"a": "0"})
   145  			So(cells, ShouldContainResembling, matrixValue{"a": "1"})
   146  			So(cells, ShouldContainResembling, matrixValue{"a": "2"})
   147  			b := matrixDefinition{
   148  				"a": []string{"0"},
   149  				"b": []string{"0", "1", "2"},
   150  			}
   151  			cells = b.allCells()
   152  			So(len(cells), ShouldEqual, 3)
   153  			So(cells, ShouldContainResembling, matrixValue{"b": "0", "a": "0"})
   154  			So(cells, ShouldContainResembling, matrixValue{"b": "1", "a": "0"})
   155  			So(cells, ShouldContainResembling, matrixValue{"b": "2", "a": "0"})
   156  			c := matrixDefinition{
   157  				"c": []string{"0", "1", "2"},
   158  				"d": []string{"0"},
   159  			}
   160  			cells = c.allCells()
   161  			So(len(cells), ShouldEqual, 3)
   162  			So(cells, ShouldContainResembling, matrixValue{"c": "0", "d": "0"})
   163  			So(cells, ShouldContainResembling, matrixValue{"c": "1", "d": "0"})
   164  			So(cells, ShouldContainResembling, matrixValue{"c": "2", "d": "0"})
   165  		})
   166  		Convey("a 2x2 matrix should expand properly", func() {
   167  			a := matrixDefinition{
   168  				"a": []string{"0", "1"},
   169  				"b": []string{"0", "1"},
   170  			}
   171  			cells := a.allCells()
   172  			So(len(cells), ShouldEqual, 4)
   173  			So(cells, ShouldContainResembling, matrixValue{"a": "0", "b": "0"})
   174  			So(cells, ShouldContainResembling, matrixValue{"a": "1", "b": "0"})
   175  			So(cells, ShouldContainResembling, matrixValue{"a": "0", "b": "1"})
   176  			So(cells, ShouldContainResembling, matrixValue{"a": "1", "b": "1"})
   177  		})
   178  		Convey("a disgustingly large matrix should expand properly", func() {
   179  			bigList := func(max int) []string {
   180  				out := []string{}
   181  				for i := 0; i < max; i++ {
   182  					out = append(out, fmt.Sprint(i))
   183  				}
   184  				return out
   185  			}
   186  
   187  			huge := matrixDefinition{
   188  				"a": bigList(15),
   189  				"b": bigList(290),
   190  				"c": bigList(20),
   191  			}
   192  			cells := huge.allCells()
   193  			So(len(cells), ShouldEqual, 15*290*20)
   194  			So(cells, ShouldContainResembling, matrixValue{"a": "0", "b": "0", "c": "0"})
   195  			So(cells, ShouldContainResembling, matrixValue{"a": "14", "b": "289", "c": "19"})
   196  			// some random guesses just for fun
   197  			So(cells, ShouldContainResembling, matrixValue{"a": "10", "b": "29", "c": "1"})
   198  			So(cells, ShouldContainResembling, matrixValue{"a": "1", "b": "2", "c": "17"})
   199  			So(cells, ShouldContainResembling, matrixValue{"a": "8", "b": "100", "c": "5"})
   200  		})
   201  	})
   202  }
   203  
   204  func TestMatrixDefinitionContains(t *testing.T) {
   205  	Convey("With a set of test definitions", t, func() {
   206  		Convey("an empty definition should match nothing", func() {
   207  			a := matrixDefinition{}
   208  			So(a.contains(matrixValue{"a": "0"}), ShouldBeFalse)
   209  		})
   210  		Convey("all definitions contain the empty value", func() {
   211  			a := matrixDefinition{}
   212  			So(a.contains(matrixValue{}), ShouldBeTrue)
   213  			b := matrixDefinition{
   214  				"a": []string{"0", "1"},
   215  				"b": []string{"0", "1"},
   216  			}
   217  			So(b.contains(matrixValue{}), ShouldBeTrue)
   218  		})
   219  		Convey("a one-axis matrix should match all of its elements", func() {
   220  			a := matrixDefinition{
   221  				"a": []string{"0", "1", "2"},
   222  			}
   223  			So(a.contains(matrixValue{"a": "0"}), ShouldBeTrue)
   224  			So(a.contains(matrixValue{"a": "1"}), ShouldBeTrue)
   225  			So(a.contains(matrixValue{"a": "2"}), ShouldBeTrue)
   226  			So(a.contains(matrixValue{"a": "3"}), ShouldBeFalse)
   227  		})
   228  		Convey("a 2x2 matrix should match all of its elements", func() {
   229  			a := matrixDefinition{
   230  				"a": []string{"0", "1"},
   231  				"b": []string{"0", "1"},
   232  			}
   233  			cells := a.allCells()
   234  			So(len(cells), ShouldEqual, 4)
   235  			So(a.contains(matrixValue{"a": "0", "b": "0"}), ShouldBeTrue)
   236  			So(a.contains(matrixValue{"a": "1", "b": "0"}), ShouldBeTrue)
   237  			So(a.contains(matrixValue{"a": "0", "b": "1"}), ShouldBeTrue)
   238  			So(a.contains(matrixValue{"a": "1", "b": "1"}), ShouldBeTrue)
   239  			So(a.contains(matrixValue{"a": "1", "b": "2"}), ShouldBeFalse)
   240  			Convey("and sub-match all of its individual axis values", func() {
   241  				So(a.contains(matrixValue{"a": "0"}), ShouldBeTrue)
   242  				So(a.contains(matrixValue{"a": "1"}), ShouldBeTrue)
   243  				So(a.contains(matrixValue{"b": "0"}), ShouldBeTrue)
   244  				So(a.contains(matrixValue{"b": "1"}), ShouldBeTrue)
   245  				So(a.contains(matrixValue{"b": "7"}), ShouldBeFalse)
   246  				So(a.contains(matrixValue{"c": "1"}), ShouldBeFalse)
   247  				So(a.contains(matrixValue{"a": "1", "b": "1", "c": "1"}), ShouldBeFalse)
   248  			})
   249  		})
   250  	})
   251  }
   252  
   253  func TestBuildMatrixVariantSimple(t *testing.T) {
   254  	testMatrix := &matrix{Id: "test"}
   255  	Convey("With a set of test axes", t, func() {
   256  		axes := []matrixAxis{
   257  			{
   258  				Id: "a",
   259  				Values: []axisValue{
   260  					{Id: "0", Tags: []string{"zero"}},
   261  					{Id: "1", Tags: []string{"odd"}},
   262  					{Id: "2", Tags: []string{"even", "prime"}},
   263  					{Id: "3", Tags: []string{"odd", "prime"}},
   264  				},
   265  			},
   266  			{
   267  				Id: "b",
   268  				Values: []axisValue{
   269  					{Id: "0", Tags: []string{"zero"}},
   270  					{Id: "1", Tags: []string{"odd"}},
   271  					{Id: "2", Tags: []string{"even", "prime"}},
   272  					{Id: "3", Tags: []string{"odd", "prime"}},
   273  				},
   274  			},
   275  		}
   276  		Convey("and matrix value test:{a:0, b:0}", func() {
   277  			mv := matrixValue{"a": "0", "b": "0"}
   278  			Convey("the variant should build without error", func() {
   279  				v, err := buildMatrixVariant(axes, mv, testMatrix, nil)
   280  				So(err, ShouldBeNil)
   281  				Convey("with id='test__a~0_b~0', tags=[zero]", func() {
   282  					So(v.Name, ShouldEqual, "test__a~0_b~0")
   283  					So(v.matrixVal, ShouldResemble, mv)
   284  					So(v.Tags, ShouldContain, "zero")
   285  					So(v.matrixId, ShouldEqual, "test")
   286  				})
   287  			})
   288  		})
   289  		Convey("and matrix value test:{a:1, b:3}", func() {
   290  			mv := matrixValue{"b": "3", "a": "1"}
   291  			Convey("the variant should build without error", func() {
   292  				v, err := buildMatrixVariant(axes, mv, testMatrix, nil)
   293  				So(err, ShouldBeNil)
   294  				Convey("with id='test__a~1_b~3', tags=[odd, prime]", func() {
   295  					So(v.Name, ShouldEqual, "test__a~1_b~3")
   296  					So(v.Tags, ShouldContain, "odd")
   297  					So(v.Tags, ShouldContain, "prime")
   298  				})
   299  			})
   300  		})
   301  		Convey("and a matrix value that references non-existent axis values", func() {
   302  			mv := matrixValue{"b": "2", "a": "4"}
   303  			Convey("should return an error", func() {
   304  				_, err := buildMatrixVariant(axes, mv, testMatrix, nil)
   305  				So(err, ShouldNotBeNil)
   306  			})
   307  		})
   308  		Convey("and a matrix value that references non-existent axis names", func() {
   309  			mv := matrixValue{"b": "2", "coolfun": "4"}
   310  			Convey("should return an error", func() {
   311  				_, err := buildMatrixVariant(axes, mv, testMatrix, nil)
   312  				So(err, ShouldNotBeNil)
   313  			})
   314  		})
   315  	})
   316  }
   317  
   318  // helper for pulling variants out of a list
   319  func findVariant(vs []parserBV, id string) parserBV {
   320  	for _, v := range vs {
   321  		if v.Name == id {
   322  			return v
   323  		}
   324  	}
   325  	panic("not found")
   326  }
   327  
   328  func TestMatrixVariantsSimple(t *testing.T) {
   329  	Convey("With a delicious set of test axes", t, func() {
   330  		// These tests are structured around a magical project that tests
   331  		// colorful candies. We will be testing M&Ms, Skittles, and Necco Wafers
   332  		// (all candies copyright their respective holders). We need to test
   333  		// each color of each candy individually, so we've decided to simplify
   334  		// our variant definitions with a matrix! The colors are as follows:
   335  		//  M&Ms:     red, orange, yellow, green, blue, brown (6)
   336  		//  Skittles: red, orange, yellow, green, purple (5)
   337  		//  Necco:    orange, yellow, green, purple, pink, brown, black, white (8)
   338  		// TODO: maybe move this up top for multiple tests
   339  		axes := []matrixAxis{
   340  			{
   341  				Id: "color",
   342  				Values: []axisValue{
   343  					{Id: "red", Tags: []string{"hot_color"}},
   344  					{Id: "pink", Tags: []string{"hot_color"}},
   345  					{Id: "orange", Tags: []string{"hot_color"}},
   346  					{Id: "yellow", Tags: []string{"hot_color"}},
   347  					{Id: "brown", Tags: []string{"hot_color"}},
   348  					{Id: "green", Tags: []string{"cool_color"}},
   349  					{Id: "blue", Tags: []string{"cool_color"}},
   350  					{Id: "purple", Tags: []string{"cool_color"}},
   351  					{Id: "black"},
   352  					{Id: "white"},
   353  				},
   354  			},
   355  			{
   356  				Id: "brand",
   357  				Values: []axisValue{
   358  					{Id: "m&ms", Tags: []string{"chocolate"}},
   359  					{Id: "skittles", Tags: []string{"chewy"}},
   360  					{Id: "necco", Tags: []string{"chalk"}},
   361  				},
   362  			},
   363  		}
   364  		ase := NewAxisSelectorEvaluator(axes)
   365  		So(ase, ShouldNotBeNil)
   366  		Convey("and a valid matrix", func() {
   367  			m := matrix{
   368  				Id: "candy",
   369  				Spec: matrixDefinition{
   370  					"color": []string{
   371  						"red", "orange", "yellow", "brown", "green",
   372  						"blue", "purple", "black", "white", "pink",
   373  					},
   374  					"brand": []string{"m&ms", "skittles", "necco"},
   375  				},
   376  				Exclude: []matrixDefinition{
   377  					{"brand": []string{"skittles"}, "color": []string{"brown", "blue"}},
   378  					{"brand": []string{"m&ms"}, "color": []string{"purple"}},
   379  					{"brand": []string{"m&ms", "skittles"},
   380  						"color": []string{"pink", "black", "white"}},
   381  					{"brand": []string{"necco"}, "color": []string{"red", "blue"}},
   382  				},
   383  			}
   384  			Convey("building a list of variants should succeed", func() {
   385  				vs, errs := buildMatrixVariants(axes, ase, []matrix{m})
   386  				So(errs, ShouldBeNil)
   387  				Convey("and return the correct list of combinations", func() {
   388  					So(len(vs), ShouldEqual, 19)
   389  					// check a couple random samples
   390  					d1 := findVariant(vs, "candy__color~yellow_brand~skittles")
   391  					So(d1.Tags, ShouldContain, "hot_color")
   392  					So(d1.Tags, ShouldContain, "chewy")
   393  					d2 := findVariant(vs, "candy__color~black_brand~necco")
   394  					So(len(d2.Tags), ShouldEqual, 1)
   395  					So(d2.Tags, ShouldContain, "chalk")
   396  					// ensure all values are in there...
   397  					vals := []matrixValue{}
   398  					for _, v := range vs {
   399  						vals = append(vals, v.matrixVal)
   400  					}
   401  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "red"})
   402  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "orange"})
   403  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "yellow"})
   404  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "green"})
   405  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "blue"})
   406  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "brown"})
   407  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "red"})
   408  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "orange"})
   409  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "yellow"})
   410  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "green"})
   411  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "purple"})
   412  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "orange"})
   413  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "yellow"})
   414  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "green"})
   415  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "purple"})
   416  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "pink"})
   417  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "white"})
   418  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "black"})
   419  				})
   420  			})
   421  		})
   422  		Convey("and a valid matrix using tag selectors", func() {
   423  			m := matrix{
   424  				Id: "candy",
   425  				Spec: matrixDefinition{
   426  					"color": []string{".hot_color", ".cool_color"}, // all but white and black
   427  					"brand": []string{"*"},
   428  				},
   429  				Exclude: []matrixDefinition{
   430  					{"brand": []string{".chewy"}, "color": []string{"brown", "blue"}},
   431  					{"brand": []string{".chocolate"}, "color": []string{"purple"}},
   432  					{"brand": []string{"!.chewy", "skittles"}, "color": []string{"pink"}},
   433  					{"brand": []string{"!skittles !m&ms"}, "color": []string{"red", "blue"}},
   434  				},
   435  			}
   436  			Convey("building a list of variations should succeed", func() {
   437  				vs, errs := buildMatrixVariants(axes, ase, []matrix{m})
   438  				So(errs, ShouldBeNil)
   439  				Convey("and return the correct list of combinations", func() {
   440  					// ensure all values are in there...
   441  					So(len(vs), ShouldEqual, 16)
   442  					vals := []matrixValue{}
   443  					for _, d := range vs {
   444  						vals = append(vals, d.matrixVal)
   445  					}
   446  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "red"})
   447  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "orange"})
   448  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "yellow"})
   449  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "green"})
   450  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "blue"})
   451  					So(vals, ShouldContainResembling, matrixValue{"brand": "m&ms", "color": "brown"})
   452  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "red"})
   453  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "orange"})
   454  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "yellow"})
   455  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "green"})
   456  					So(vals, ShouldContainResembling, matrixValue{"brand": "skittles", "color": "purple"})
   457  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "orange"})
   458  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "yellow"})
   459  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "green"})
   460  					So(vals, ShouldContainResembling, matrixValue{"brand": "necco", "color": "purple"})
   461  				})
   462  			})
   463  		})
   464  		Convey("and a matrix that uses wrong axes", func() {
   465  			m := matrix{
   466  				Id: "candy",
   467  				Spec: matrixDefinition{
   468  					"strength": []string{"weak", "middle", "big-n-tough"},
   469  				},
   470  			}
   471  			Convey("should fail to build", func() {
   472  				vs, errs := buildMatrixVariants(axes, ase, []matrix{m})
   473  				So(len(vs), ShouldEqual, 0)
   474  				So(len(errs), ShouldEqual, 3)
   475  			})
   476  		})
   477  		Convey("and a matrix that uses wrong axis values", func() {
   478  			m := matrix{
   479  				Id: "candy",
   480  				Spec: matrixDefinition{
   481  					"color": []string{"salmon", "infrared"},
   482  				},
   483  			}
   484  			Convey("should fail to build", func() {
   485  				vs, errs := buildMatrixVariants(axes, ase, []matrix{m})
   486  				So(len(vs), ShouldEqual, 0)
   487  				So(len(errs), ShouldEqual, 2)
   488  			})
   489  		})
   490  	})
   491  }
   492  
   493  func TestMergeAxisValue(t *testing.T) {
   494  	Convey("With a parserBV", t, func() {
   495  		pbv := parserBV{
   496  			RunOn:     []string{"basic_distro"},
   497  			Modules:   []string{"basic_module"},
   498  			Tags:      []string{"basic"},
   499  			BatchTime: nil,
   500  			Stepback:  nil,
   501  			Expansions: map[string]string{
   502  				"v1": "test",
   503  			},
   504  		}
   505  		Convey("a valid axis value should merge successfully", func() {
   506  			av := axisValue{
   507  				RunOn:     []string{"special_distro"},
   508  				Modules:   []string{"module++"},
   509  				Tags:      []string{"enterprise"},
   510  				BatchTime: new(int),
   511  				Stepback:  new(bool),
   512  				Variables: map[string]string{
   513  					"v2": "new",
   514  				},
   515  			}
   516  			So(pbv.mergeAxisValue(av), ShouldBeNil)
   517  			So(pbv.RunOn, ShouldResemble, av.RunOn)
   518  			So(pbv.Modules, ShouldResemble, av.Modules)
   519  			So(pbv.Tags, ShouldContain, "basic")
   520  			So(pbv.Tags, ShouldContain, "enterprise")
   521  			So(pbv.Stepback, ShouldNotBeNil)
   522  			So(pbv.BatchTime, ShouldNotBeNil)
   523  			So(pbv.Expansions, ShouldResemble, command.Expansions{
   524  				"v1": "test",
   525  				"v2": "new",
   526  			})
   527  		})
   528  		Convey("a valid axis value full of expansions should merge successfully", func() {
   529  			av := axisValue{
   530  				RunOn:   []string{"${v1}", "${v2}"},
   531  				Modules: []string{"${v1}__"},
   532  				Tags:    []string{"fat${v2}"},
   533  				Variables: map[string]string{
   534  					"v2": "${v1}!",
   535  				},
   536  			}
   537  			So(pbv.mergeAxisValue(av), ShouldBeNil)
   538  			So(pbv.RunOn, ShouldResemble, parserStringSlice{"test", "test!"})
   539  			So(pbv.Modules, ShouldResemble, parserStringSlice{"test__"})
   540  			So(pbv.Tags, ShouldContain, "basic")
   541  			So(pbv.Tags, ShouldContain, "fattest!")
   542  			So(pbv.Expansions, ShouldResemble, command.Expansions{
   543  				"v1": "test",
   544  				"v2": "test!",
   545  			})
   546  		})
   547  		Convey("an axis value with a bad tag expansion should fail", func() {
   548  			av := axisValue{
   549  				Tags: []string{"fat${"},
   550  			}
   551  			So(pbv.mergeAxisValue(av), ShouldNotBeNil)
   552  		})
   553  		Convey("an axis value with a bad variables expansion should fail", func() {
   554  			av := axisValue{
   555  				Variables: map[string]string{
   556  					"v2": "${sdsad",
   557  				},
   558  			}
   559  			So(pbv.mergeAxisValue(av), ShouldNotBeNil)
   560  		})
   561  	})
   562  }
   563  
   564  func TestRulesEvaluation(t *testing.T) {
   565  	Convey("With a series of test parserBVs and tasks", t, func() {
   566  		taskDefs := []parserTask{
   567  			{Name: "red", Tags: []string{"primary", "warm"}},
   568  			{Name: "orange", Tags: []string{"secondary", "warm"}},
   569  			{Name: "yellow", Tags: []string{"primary", "warm"}},
   570  			{Name: "green", Tags: []string{"secondary", "cool"}},
   571  			{Name: "blue", Tags: []string{"primary", "cool"}},
   572  			{Name: "purple", Tags: []string{"secondary", "cool"}},
   573  			{Name: "brown", Tags: []string{"tertiary"}},
   574  			{Name: "black", Tags: []string{"special"}},
   575  			{Name: "white", Tags: []string{"special"}},
   576  		}
   577  		tse := NewParserTaskSelectorEvaluator(taskDefs)
   578  		Convey("a variant with a 'remove' rule should remove the given tasks", func() {
   579  			bvs := []parserBV{{
   580  				Name: "test",
   581  				Tasks: parserBVTasks{
   582  					{Name: "blue"},
   583  					{Name: ".special"},
   584  					{Name: ".tertiary"},
   585  				},
   586  				matrixRules: []ruleAction{
   587  					{RemoveTasks: []string{".primary !.warm"}}, //remove blue
   588  					{RemoveTasks: []string{"brown"}},
   589  				},
   590  			}}
   591  			evaluated, errs := evaluateBuildVariants(tse, nil, bvs)
   592  			So(errs, ShouldBeNil)
   593  			v1 := evaluated[0]
   594  			So(v1.Name, ShouldEqual, "test")
   595  			So(len(v1.Tasks), ShouldEqual, 2)
   596  		})
   597  		Convey("a variant with an 'add' rule should add the given tasks", func() {
   598  			bvs := []parserBV{{
   599  				Name: "test",
   600  				Tasks: parserBVTasks{
   601  					{Name: ".special"},
   602  				},
   603  				matrixRules: []ruleAction{
   604  					{AddTasks: []parserBVTask{{Name: ".primary"}}},
   605  					{AddTasks: []parserBVTask{{Name: ".warm"}}},
   606  					{AddTasks: []parserBVTask{{Name: "green", DependsOn: []parserDependency{{
   607  						taskSelector: taskSelector{Name: ".warm"},
   608  					}}}}},
   609  				},
   610  			}}
   611  			evaluated, errs := evaluateBuildVariants(tse, nil, bvs)
   612  			So(errs, ShouldBeNil)
   613  			v1 := evaluated[0]
   614  			So(v1.Name, ShouldEqual, "test")
   615  			So(len(v1.Tasks), ShouldEqual, 7)
   616  		})
   617  		Convey("a series of add and remove rules should execute in order", func() {
   618  			bvs := []parserBV{{
   619  				Name: "test",
   620  				Tasks: parserBVTasks{
   621  					{Name: ".secondary"},
   622  				},
   623  				matrixRules: []ruleAction{
   624  					{AddTasks: []parserBVTask{{Name: ".primary"}}},
   625  					{RemoveTasks: []string{".secondary"}},
   626  					{AddTasks: []parserBVTask{{Name: ".warm"}}},
   627  					{RemoveTasks: []string{"orange"}},
   628  					{AddTasks: []parserBVTask{{Name: "orange", DependsOn: []parserDependency{{
   629  						taskSelector: taskSelector{Name: ".warm"},
   630  					}}}}},
   631  				},
   632  			}}
   633  			evaluated, errs := evaluateBuildVariants(tse, nil, bvs)
   634  			So(errs, ShouldBeNil)
   635  			v1 := evaluated[0]
   636  			So(v1.Name, ShouldEqual, "test")
   637  			So(len(v1.Tasks), ShouldEqual, 4)
   638  		})
   639  		Convey("conflicting added tasks should fail", func() {
   640  			bvs := []parserBV{{
   641  				// case where conflicts take place against existing tasks
   642  				Name: "test1",
   643  				Tasks: parserBVTasks{
   644  					{Name: ".warm"},
   645  				},
   646  				matrixRules: []ruleAction{
   647  					{AddTasks: []parserBVTask{{Name: "orange", DependsOn: []parserDependency{{
   648  						taskSelector: taskSelector{Name: ".warm"},
   649  					}}}}},
   650  				},
   651  			}, {
   652  				// case where conflicts are within the same rule
   653  				Name:  "test2",
   654  				Tasks: parserBVTasks{},
   655  				matrixRules: []ruleAction{
   656  					{AddTasks: []parserBVTask{{Name: ".warm"}, {Name: "orange", DependsOn: []parserDependency{{
   657  						taskSelector: taskSelector{Name: ".warm"},
   658  					}}}}},
   659  				},
   660  			}}
   661  			_, errs := evaluateBuildVariants(tse, nil, bvs)
   662  			So(errs, ShouldNotBeNil)
   663  			So(len(errs), ShouldEqual, 3)
   664  		})
   665  		Convey("a 'remove' rule for an unknown task should fail", func() {
   666  			bvs := []parserBV{{
   667  				Name: "test",
   668  				Tasks: parserBVTasks{
   669  					{Name: "blue"},
   670  					{Name: ".special"},
   671  					{Name: ".tertiary"},
   672  				},
   673  				matrixRules: []ruleAction{
   674  					{RemoveTasks: []string{".amazing"}}, //remove blue
   675  					{RemoveTasks: []string{"rainbow"}},
   676  				},
   677  			}}
   678  			_, errs := evaluateBuildVariants(tse, nil, bvs)
   679  			So(errs, ShouldNotBeNil)
   680  			So(len(errs), ShouldEqual, 2)
   681  		})
   682  	})
   683  }