go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/job/edit_dimensions_test.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package job
    16  
    17  import (
    18  	"fmt"
    19  	"testing"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/types/known/durationpb"
    23  
    24  	bbpb "go.chromium.org/luci/buildbucket/proto"
    25  	swarmingpb "go.chromium.org/luci/swarming/proto/api_v2"
    26  
    27  	. "github.com/smartystreets/goconvey/convey"
    28  	. "go.chromium.org/luci/common/testing/assertions"
    29  )
    30  
    31  func TestMakeDimensionEditCommands(t *testing.T) {
    32  	t.Parallel()
    33  
    34  	var testCases = []struct {
    35  		name   string
    36  		cmds   []string
    37  		err    string
    38  		expect DimensionEditCommands
    39  	}{
    40  		{name: "empty"},
    41  		{
    42  			name: "err no op",
    43  			cmds: []string{"bad"},
    44  			err:  "op was missing",
    45  		},
    46  		{
    47  			name: "err no value",
    48  			cmds: []string{"bad+="},
    49  			err:  "empty value not allowed for operator",
    50  		},
    51  		{
    52  			name: "err bad expiration",
    53  			cmds: []string{"bad=value@dogs"},
    54  			err:  `parsing expiration "dogs"`,
    55  		},
    56  		{
    57  			name: "err unwanted expiration",
    58  			cmds: []string{"bad-=value@123"},
    59  			err:  "expiration seconds not allowed",
    60  		},
    61  		{
    62  			name: "reset",
    63  			cmds: []string{
    64  				"key=",
    65  			},
    66  			expect: DimensionEditCommands{
    67  				"key": &DimensionEditCommand{
    68  					SetValues: []ExpiringValue{},
    69  				},
    70  			},
    71  		},
    72  		{
    73  			name: "set",
    74  			cmds: []string{
    75  				"key=value",
    76  				"key=other_value@123",
    77  				"something=value", // ignored on 'apply', but will show up here
    78  				"something=value@123",
    79  			},
    80  			expect: DimensionEditCommands{
    81  				"key": &DimensionEditCommand{
    82  					SetValues: []ExpiringValue{
    83  						{Value: "value"},
    84  						{Value: "other_value", Expiration: 123 * time.Second},
    85  					},
    86  				},
    87  				"something": &DimensionEditCommand{
    88  					SetValues: []ExpiringValue{
    89  						{Value: "value"},
    90  						{Value: "value", Expiration: 123 * time.Second},
    91  					},
    92  				},
    93  			},
    94  		},
    95  		{
    96  			name: "add",
    97  			cmds: []string{
    98  				"key+=value",
    99  				"key+=other_value@123",
   100  				"something+=value", // ignored on 'apply', but will show up here
   101  				"something+=value@123",
   102  			},
   103  			expect: DimensionEditCommands{
   104  				"key": &DimensionEditCommand{
   105  					AddValues: []ExpiringValue{
   106  						{Value: "value"},
   107  						{Value: "other_value", Expiration: 123 * time.Second},
   108  					},
   109  				},
   110  				"something": &DimensionEditCommand{
   111  					AddValues: []ExpiringValue{
   112  						{Value: "value"},
   113  						{Value: "value", Expiration: 123 * time.Second},
   114  					},
   115  				},
   116  			},
   117  		},
   118  		{
   119  			name: "remove",
   120  			cmds: []string{
   121  				"key-=value",
   122  				"key-=other_value",
   123  			},
   124  			expect: DimensionEditCommands{
   125  				"key": &DimensionEditCommand{
   126  					RemoveValues: []string{"value", "other_value"},
   127  				},
   128  			},
   129  		},
   130  	}
   131  
   132  	Convey(`MakeDimensionEditCommands`, t, func() {
   133  		for _, tc := range testCases {
   134  			tc := tc
   135  			Convey(tc.name, func() {
   136  				dec, err := MakeDimensionEditCommands(tc.cmds)
   137  				if tc.err == "" {
   138  					So(err, ShouldBeNil)
   139  					So(dec, ShouldResemble, tc.expect)
   140  				} else {
   141  					So(err, ShouldErrLike, tc.err)
   142  				}
   143  			})
   144  		}
   145  	})
   146  }
   147  
   148  func TestSetDimensions(t *testing.T) {
   149  	t.Parallel()
   150  
   151  	runCases(t, "SetDimensions", []testCase{
   152  		{
   153  			name: "nil",
   154  			fn: func(jd *Definition) {
   155  				SoEdit(jd, func(je Editor) {
   156  					je.SetDimensions(nil)
   157  				})
   158  				So(mustGetDimensions(jd), ShouldBeEmpty)
   159  			},
   160  		},
   161  
   162  		{
   163  			name:        "add",
   164  			skipSWEmpty: true,
   165  			fn: func(jd *Definition) {
   166  				baselineDims(jd)
   167  
   168  				So(mustGetDimensions(jd).String(), ShouldResemble, ExpiringDimensions{
   169  					"key": []ExpiringValue{
   170  						{Value: "A", Expiration: swSlice1Exp},
   171  						{Value: "AA", Expiration: swSlice1Exp},
   172  						{Value: "B", Expiration: swSlice2Exp},
   173  						{Value: "C", Expiration: swSlice3Exp},
   174  						{Value: "Z", Expiration: swSlice3Exp},
   175  					},
   176  				}.String())
   177  
   178  				if sw := jd.GetSwarming(); sw != nil {
   179  					// ensure dimensions show up in ALL slices which they ought to.
   180  					So(sw.Task.TaskSlices[0].Properties.Dimensions, ShouldResembleProto, []*swarmingpb.StringPair{
   181  						{
   182  							Key:   "key",
   183  							Value: "A",
   184  						},
   185  						{
   186  							Key:   "key",
   187  							Value: "AA",
   188  						},
   189  						{
   190  							Key:   "key",
   191  							Value: "B",
   192  						},
   193  						{
   194  							Key:   "key",
   195  							Value: "C",
   196  						},
   197  						{
   198  							Key:   "key",
   199  							Value: "Z",
   200  						},
   201  					})
   202  					So(sw.Task.TaskSlices[1].Properties.Dimensions, ShouldResembleProto, []*swarmingpb.StringPair{
   203  						{
   204  							Key:   "key",
   205  							Value: "B",
   206  						},
   207  						{
   208  							Key:   "key",
   209  							Value: "C",
   210  						},
   211  						{
   212  							Key:   "key",
   213  							Value: "Z",
   214  						},
   215  					})
   216  					So(sw.Task.TaskSlices[2].Properties.Dimensions, ShouldResembleProto, []*swarmingpb.StringPair{
   217  						{
   218  							Key:   "key",
   219  							Value: "C",
   220  						},
   221  						{
   222  							Key:   "key",
   223  							Value: "Z",
   224  						},
   225  					})
   226  				} else {
   227  					rdims := jd.GetBuildbucket().BbagentArgs.Build.Infra.Swarming.TaskDimensions
   228  					So(rdims, ShouldResembleProto, []*bbpb.RequestedDimension{
   229  						{Key: "key", Value: "A", Expiration: durationpb.New(swSlice1Exp)},
   230  						{Key: "key", Value: "AA", Expiration: durationpb.New(swSlice1Exp)},
   231  						{Key: "key", Value: "B", Expiration: durationpb.New(swSlice2Exp)},
   232  						{Key: "key", Value: "C", Expiration: durationpb.New(swSlice3Exp)},
   233  						{Key: "key", Value: "Z"},
   234  					})
   235  				}
   236  			},
   237  		},
   238  
   239  		{
   240  			name:        "replace",
   241  			skipSWEmpty: true,
   242  			fn: func(jd *Definition) {
   243  				baselineDims(jd)
   244  
   245  				SoEdit(jd, func(je Editor) {
   246  					je.SetDimensions(ExpiringDimensions{
   247  						"key": []ExpiringValue{
   248  							{Value: "norp", Expiration: swSlice1Exp},
   249  						},
   250  					})
   251  				})
   252  
   253  				So(mustGetDimensions(jd), ShouldResemble, ExpiringDimensions{
   254  					"key": []ExpiringValue{
   255  						{Value: "norp", Expiration: swSlice1Exp},
   256  					},
   257  				})
   258  			},
   259  		},
   260  
   261  		{
   262  			name:        "delete",
   263  			skipSWEmpty: true,
   264  			fn: func(jd *Definition) {
   265  				baselineDims(jd)
   266  
   267  				SoEdit(jd, func(je Editor) {
   268  					je.SetDimensions(nil)
   269  				})
   270  
   271  				So(mustGetDimensions(jd), ShouldResemble, ExpiringDimensions{})
   272  			},
   273  		},
   274  
   275  		{
   276  			name:        "bad expiration",
   277  			skipSWEmpty: true,
   278  			skipBB:      true,
   279  			fn: func(jd *Definition) {
   280  				err := jd.Edit(func(je Editor) {
   281  					je.SetDimensions(ExpiringDimensions{
   282  						"key": []ExpiringValue{
   283  							{Value: "narp", Expiration: time.Second * 10},
   284  						},
   285  					})
   286  				})
   287  				So(err, ShouldErrLike,
   288  					"key=narp@10 has invalid expiration time: "+
   289  						"current slices expire at [0 60 240 600]")
   290  			},
   291  		},
   292  	})
   293  
   294  }
   295  
   296  func editDims(jd *Definition, cmds ...string) {
   297  	editCmds, err := MakeDimensionEditCommands(cmds)
   298  	So(err, ShouldBeNil)
   299  	err = jd.Edit(func(je Editor) {
   300  		je.EditDimensions(editCmds)
   301  	})
   302  	So(err, ShouldBeNil)
   303  }
   304  
   305  func TestEditDimensions(t *testing.T) {
   306  	t.Parallel()
   307  
   308  	runCases(t, "EditDimensions", []testCase{
   309  		{
   310  			name: "nil (empty)",
   311  			fn: func(jd *Definition) {
   312  				editDims(jd) // no edit commands
   313  				So(mustGetDimensions(jd), ShouldResemble, ExpiringDimensions{})
   314  			},
   315  		},
   316  
   317  		{
   318  			name:        "nil (existing)",
   319  			skipSWEmpty: true,
   320  			fn: func(jd *Definition) {
   321  				base := baselineDims(jd)
   322  				So(mustGetDimensions(jd), ShouldResemble, base)
   323  
   324  				editDims(jd) // no edit commands
   325  
   326  				So(mustGetDimensions(jd), ShouldResemble, base)
   327  			},
   328  		},
   329  
   330  		{
   331  			name:        "add",
   332  			skipSWEmpty: true,
   333  			fn: func(jd *Definition) {
   334  				editDims(jd,
   335  					fmt.Sprintf("key+=value@%d", swSlice1ExpSecs),
   336  					fmt.Sprintf("key+=other_value@%d", swSlice3ExpSecs),
   337  					"other-=bogus",
   338  					"reset=everything",
   339  					"reset=else",
   340  				)
   341  
   342  				So(mustGetDimensions(jd), ShouldResemble, ExpiringDimensions{
   343  					"key": []ExpiringValue{
   344  						{Value: "value", Expiration: swSlice1Exp},
   345  						{Value: "other_value", Expiration: swSlice3Exp},
   346  					},
   347  					"reset": []ExpiringValue{
   348  						{Value: "else", Expiration: swSlice3Exp},
   349  						{Value: "everything", Expiration: swSlice3Exp},
   350  					},
   351  				})
   352  
   353  				if sw := jd.GetSwarming(); sw != nil {
   354  					// ensure dimensions show up in ALL slices which they ought to.
   355  					So(sw.Task.TaskSlices[0].Properties.Dimensions, ShouldResembleProto, []*swarmingpb.StringPair{
   356  						{
   357  							Key:   "key",
   358  							Value: "other_value",
   359  						},
   360  						{
   361  							Key:   "key",
   362  							Value: "value",
   363  						},
   364  						{
   365  							Key:   "reset",
   366  							Value: "else",
   367  						},
   368  						{
   369  							Key:   "reset",
   370  							Value: "everything",
   371  						},
   372  					})
   373  					So(sw.Task.TaskSlices[1].Properties.Dimensions, ShouldResembleProto, []*swarmingpb.StringPair{
   374  						{
   375  							Key:   "key",
   376  							Value: "other_value",
   377  						},
   378  						{
   379  							Key:   "reset",
   380  							Value: "else",
   381  						},
   382  						{
   383  							Key:   "reset",
   384  							Value: "everything",
   385  						},
   386  					})
   387  					So(sw.Task.TaskSlices[2].Properties.Dimensions, ShouldResembleProto, []*swarmingpb.StringPair{
   388  						{
   389  							Key:   "key",
   390  							Value: "other_value",
   391  						},
   392  						{
   393  							Key:   "reset",
   394  							Value: "else",
   395  						},
   396  						{
   397  							Key:   "reset",
   398  							Value: "everything",
   399  						},
   400  					})
   401  				} else {
   402  					rdims := jd.GetBuildbucket().BbagentArgs.Build.Infra.Swarming.TaskDimensions
   403  					So(rdims, ShouldResembleProto, []*bbpb.RequestedDimension{
   404  						{Key: "key", Value: "other_value", Expiration: durationpb.New(swSlice3Exp)},
   405  						{Key: "key", Value: "value", Expiration: durationpb.New(swSlice1Exp)},
   406  						{Key: "reset", Value: "else"},
   407  						{Key: "reset", Value: "everything"},
   408  					})
   409  				}
   410  			},
   411  		},
   412  
   413  		{
   414  			name:        "remove",
   415  			skipSWEmpty: true,
   416  			fn: func(jd *Definition) {
   417  				editDims(jd,
   418  					fmt.Sprintf("key+=value@%d", swSlice1ExpSecs),
   419  					fmt.Sprintf("key+=other_value@%d", swSlice3ExpSecs),
   420  					"reset=everything",
   421  					"reset=else",
   422  				)
   423  
   424  				editDims(jd, "key-=other_value")
   425  
   426  				So(mustGetDimensions(jd), ShouldResemble, ExpiringDimensions{
   427  					"key": []ExpiringValue{
   428  						{Value: "value", Expiration: swSlice1Exp},
   429  					},
   430  					"reset": []ExpiringValue{
   431  						{Value: "else", Expiration: swSlice3Exp},
   432  						{Value: "everything", Expiration: swSlice3Exp},
   433  					},
   434  				})
   435  
   436  			},
   437  		},
   438  	})
   439  }