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 }