go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/task/gitiles/gitiles_test.go (about) 1 // Copyright 2016 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 gitiles 16 17 import ( 18 "context" 19 "fmt" 20 "net/http" 21 "strings" 22 "testing" 23 "time" 24 25 "github.com/golang/mock/gomock" 26 "github.com/golang/protobuf/proto" 27 28 "google.golang.org/grpc/codes" 29 "google.golang.org/grpc/status" 30 "google.golang.org/protobuf/types/known/timestamppb" 31 32 "go.chromium.org/luci/common/errors" 33 commonpb "go.chromium.org/luci/common/proto" 34 "go.chromium.org/luci/common/proto/git" 35 gitilespb "go.chromium.org/luci/common/proto/gitiles" 36 "go.chromium.org/luci/common/proto/gitiles/mock_gitiles" 37 "go.chromium.org/luci/common/retry/transient" 38 "go.chromium.org/luci/config/validation" 39 "go.chromium.org/luci/gae/impl/memory" 40 api "go.chromium.org/luci/scheduler/api/scheduler/v1" 41 "go.chromium.org/luci/scheduler/appengine/messages" 42 "go.chromium.org/luci/scheduler/appengine/task" 43 "go.chromium.org/luci/scheduler/appengine/task/utils/tasktest" 44 45 . "github.com/smartystreets/goconvey/convey" 46 47 . "go.chromium.org/luci/common/testing/assertions" 48 ) 49 50 var _ task.Manager = (*TaskManager)(nil) 51 52 func TestTriggerBuild(t *testing.T) { 53 t.Parallel() 54 55 Convey("LaunchTask Triggers Jobs", t, func() { 56 c := memory.Use(context.Background()) 57 cfg := &messages.GitilesTask{ 58 Repo: "https://a.googlesource.com/b.git", 59 } 60 jobID := "proj/gitiles" 61 62 type strmap map[string]string 63 64 loadNoError := func() strmap { 65 state, err := loadState(c, jobID, cfg.Repo) 66 if err != nil { 67 panic(err) 68 } 69 return state 70 } 71 72 ctl := &tasktest.TestController{ 73 TaskMessage: cfg, 74 Client: http.DefaultClient, 75 SaveCallback: func() error { return nil }, 76 OverrideJobID: jobID, 77 } 78 79 mockCtrl := gomock.NewController(t) 80 defer mockCtrl.Finish() 81 gitilesMock := mock_gitiles.NewMockGitilesClient(mockCtrl) 82 83 m := TaskManager{mockGitilesClient: gitilesMock} 84 85 expectRefs := func(refsPath string, tips strmap) *gomock.Call { 86 req := &gitilespb.RefsRequest{ 87 Project: "b", 88 RefsPath: refsPath, 89 } 90 res := &gitilespb.RefsResponse{ 91 Revisions: tips, 92 } 93 return gitilesMock.EXPECT().Refs(gomock.Any(), commonpb.MatcherEqual(req)).Return(res, nil) 94 } 95 // expCommits is for readability of expectLog calls. 96 log := func(ids ...string) []string { return ids } 97 var epoch = time.Unix(1442270520, 0).UTC() 98 expectLog := func(new, old string, pageSize int, ids []string, errs ...error) *gomock.Call { 99 req := &gitilespb.LogRequest{ 100 Project: "b", 101 Committish: new, 102 ExcludeAncestorsOf: old, 103 PageSize: int32(pageSize), 104 } 105 if len(errs) > 0 { 106 return gitilesMock.EXPECT().Log(gomock.Any(), commonpb.MatcherEqual(req)).Return(nil, errs[0]) 107 } 108 res := &gitilespb.LogResponse{} 109 committedAt := epoch 110 for _, id := range ids { 111 // Ids go backwards in time, just as in `git log`. 112 committedAt = committedAt.Add(-time.Minute) 113 res.Log = append(res.Log, &git.Commit{ 114 Id: id, 115 Committer: &git.Commit_User{Time: timestamppb.New(committedAt)}, 116 }) 117 } 118 return gitilesMock.EXPECT().Log(gomock.Any(), commonpb.MatcherEqual(req)).Return(res, nil) 119 } 120 121 // expectLogWithDiff mocks Log call with result containing Tree Diff. 122 // commitsWithFiles must be in the form of "sha1:comma,separated,files" and 123 // if several must go backwards in time, just like git log. 124 expectLogWithDiff := func(new, old string, pageSize int, project string, commitsWithFiles ...string) *gomock.Call { 125 req := &gitilespb.LogRequest{ 126 Project: project, 127 Committish: new, 128 ExcludeAncestorsOf: old, 129 PageSize: int32(pageSize), 130 TreeDiff: true, 131 } 132 res := &gitilespb.LogResponse{} 133 committedAt := epoch 134 for _, cfs := range commitsWithFiles { 135 parts := strings.SplitN(cfs, ":", 2) 136 if len(parts) != 2 { 137 panic(fmt.Errorf(`commitWithFiles must be in the form of "sha1:comma,separated,files", but given %q`, cfs)) 138 } 139 id := parts[0] 140 fileNames := strings.Split(parts[1], ",") 141 diff := make([]*git.Commit_TreeDiff, len(fileNames)) 142 for i, f := range fileNames { 143 diff[i] = &git.Commit_TreeDiff{NewPath: f} 144 } 145 committedAt = committedAt.Add(-time.Minute) 146 res.Log = append(res.Log, &git.Commit{ 147 Id: id, 148 Committer: &git.Commit_User{Time: timestamppb.New(committedAt)}, 149 TreeDiff: diff, 150 }) 151 } 152 return gitilesMock.EXPECT().Log(gomock.Any(), commonpb.MatcherEqual(req)).Return(res, nil) 153 } 154 155 Convey("each configured ref must match resolved ref", func() { 156 cfg.Refs = []string{"refs/heads/master", `regexp:refs/branch-heads/\d+`} 157 expectRefs("refs/heads", strmap{"refs/heads/not-master": "deadbeef00"}) 158 expectRefs("refs/branch-heads", strmap{"refs/branch-heads/not-digits": "deadbeef00"}) 159 So(m.LaunchTask(c, ctl), ShouldErrLike, "2 unresolved refs") 160 So(ctl.Triggers, ShouldHaveLength, 0) 161 So(ctl.Log[len(ctl.Log)-2], ShouldContainSubstring, 162 "following configured refs didn't match a single actual ref:") 163 }) 164 165 Convey("new refs are discovered", func() { 166 cfg.Refs = []string{"refs/heads/master"} 167 expectRefs("refs/heads", strmap{"refs/heads/master": "deadbeef00", "refs/weird": "123456"}) 168 expectLog("deadbeef00", "", 1, log("deadbeef00")) 169 So(m.LaunchTask(c, ctl), ShouldBeNil) 170 So(loadNoError(), ShouldResemble, strmap{ 171 "refs/heads/master": "deadbeef00", 172 }) 173 So(ctl.Triggers, ShouldHaveLength, 1) 174 So(ctl.Triggers[0].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/heads/master@deadbeef00") 175 So(ctl.Triggers[0].GetGitiles(), ShouldResemble, &api.GitilesTrigger{ 176 Repo: "https://a.googlesource.com/b.git", 177 Ref: "refs/heads/master", 178 Revision: "deadbeef00", 179 }) 180 }) 181 182 Convey("regexp refs are matched correctly", func() { 183 cfg.Refs = []string{`regexp:refs/branch-heads/1\.\d+`} 184 So(saveState(c, jobID, cfg.Repo, strmap{ 185 "refs/branch-heads/1.0": "deadcafe00", 186 "refs/branch-heads/1.1": "beefcafe02", 187 }), ShouldBeNil) 188 expectRefs("refs/branch-heads", strmap{ 189 "refs/branch-heads/1.1": "beefcafe00", 190 "refs/branch-heads/1.2": "deadbeef00", 191 "refs/branch-heads/1.2.3": "deadbeef01", 192 }) 193 expectLog("beefcafe00", "beefcafe02", 50, log("beefcafe00", "beefcafe01")) 194 expectLog("deadbeef00", "", 1, log("deadbeef00")) 195 So(m.LaunchTask(c, ctl), ShouldBeNil) 196 197 So(loadNoError(), ShouldResemble, strmap{ 198 "refs/branch-heads/1.2": "deadbeef00", 199 "refs/branch-heads/1.1": "beefcafe00", 200 }) 201 So(ctl.Triggers, ShouldHaveLength, 3) 202 So(ctl.Triggers[0].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/branch-heads/1.1@beefcafe01") 203 So(ctl.Triggers[0].GetGitiles(), ShouldResemble, &api.GitilesTrigger{ 204 Repo: "https://a.googlesource.com/b.git", 205 Ref: "refs/branch-heads/1.1", 206 Revision: "beefcafe01", 207 }) 208 So(ctl.Triggers[1].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/branch-heads/1.1@beefcafe00") 209 So(ctl.Triggers[2].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/branch-heads/1.2@deadbeef00") 210 }) 211 212 Convey("do not trigger if there are no new commits", func() { 213 cfg.Refs = []string{"regexp:refs/branch-heads/[^/]+"} 214 So(saveState(c, jobID, cfg.Repo, strmap{ 215 "refs/branch-heads/beta": "deadbeef00", 216 }), ShouldBeNil) 217 expectRefs("refs/branch-heads", strmap{"refs/branch-heads/beta": "deadbeef00"}) 218 So(m.LaunchTask(c, ctl), ShouldBeNil) 219 So(ctl.Triggers, ShouldBeNil) 220 So(loadNoError(), ShouldResemble, strmap{ 221 "refs/branch-heads/beta": "deadbeef00", 222 }) 223 }) 224 225 Convey("New, updated, and deleted refs", func() { 226 cfg.Refs = []string{"refs/heads/master", "regexp:refs/branch-heads/[^/]+"} 227 So(saveState(c, jobID, cfg.Repo, strmap{ 228 "refs/heads/master": "deadbeef03", 229 "refs/branch-heads/x": "1234567890", 230 "refs/was/watched": "0987654321", 231 }), ShouldBeNil) 232 expectRefs("refs/heads", strmap{ 233 "refs/heads/master": "deadbeef00", 234 }) 235 expectRefs("refs/branch-heads", strmap{ 236 "refs/branch-heads/1.2.3": "baadcafe00", 237 }) 238 expectLog("deadbeef00", "deadbeef03", 50, log("deadbeef00", "deadbeef01", "deadbeef02")) 239 expectLog("baadcafe00", "", 1, log("baadcafe00")) 240 241 So(m.LaunchTask(c, ctl), ShouldBeNil) 242 So(loadNoError(), ShouldResemble, strmap{ 243 "refs/heads/master": "deadbeef00", 244 "refs/branch-heads/1.2.3": "baadcafe00", 245 }) 246 So(ctl.Triggers, ShouldHaveLength, 4) 247 // Ordered by ref, then by timestamp. 248 So(ctl.Triggers[0].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/branch-heads/1.2.3@baadcafe00") 249 So(ctl.Triggers[1].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/heads/master@deadbeef02") 250 So(ctl.Triggers[2].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/heads/master@deadbeef01") 251 So(ctl.Triggers[3].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/heads/master@deadbeef00") 252 for i, t := range ctl.Triggers { 253 So(t.OrderInBatch, ShouldEqual, i) 254 } 255 So(ctl.Triggers[0].Created.AsTime(), ShouldEqual, epoch.Add(-1*time.Minute)) 256 So(ctl.Triggers[1].Created.AsTime(), ShouldEqual, epoch.Add(-3*time.Minute)) // oldest on master 257 So(ctl.Triggers[2].Created.AsTime(), ShouldEqual, epoch.Add(-2*time.Minute)) 258 So(ctl.Triggers[3].Created.AsTime(), ShouldEqual, epoch.Add(-1*time.Minute)) // newest on master 259 }) 260 261 Convey("Updated ref with pathfilters", func() { 262 cfg.Refs = []string{"refs/heads/master"} 263 cfg.PathRegexps = []string{`.+\.emit`} 264 cfg.PathRegexpsExclude = []string{`skip/.+`} 265 So(saveState(c, jobID, cfg.Repo, strmap{"refs/heads/master": "deadbeef04"}), ShouldBeNil) 266 expectRefs("refs/heads", strmap{"refs/heads/master": "deadbeef00"}) 267 expectLogWithDiff("deadbeef00", "deadbeef04", 50, "b", 268 "deadbeef00:skip/commit", 269 "deadbeef01:yup.emit", 270 "deadbeef02:skip/this-file,not-matched-file,but-still.emit", 271 "deadbeef03:nothing-matched-means-skipped") 272 273 So(m.LaunchTask(c, ctl), ShouldBeNil) 274 So(loadNoError(), ShouldResemble, strmap{ 275 "refs/heads/master": "deadbeef00", 276 }) 277 So(ctl.Triggers, ShouldHaveLength, 2) 278 So(ctl.Triggers[0].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/heads/master@deadbeef02") 279 So(ctl.Triggers[1].Id, ShouldEqual, "https://a.googlesource.com/b.git/+/refs/heads/master@deadbeef01") 280 }) 281 282 Convey("Updated ref without matched commits", func() { 283 cfg.Refs = []string{"refs/heads/master"} 284 cfg.PathRegexps = []string{`must-match`} 285 So(saveState(c, jobID, cfg.Repo, strmap{"refs/heads/master": "deadbeef04"}), ShouldBeNil) 286 expectRefs("refs/heads", strmap{"refs/heads/master": "deadbeef00"}) 287 288 expectLogWithDiff("deadbeef00", "deadbeef04", 50, "b", 289 "deadbeef00:nope0", 290 "deadbeef01:nope1", 291 "deadbeef02:nope2", 292 "deadbeef03:nope3") 293 So(m.LaunchTask(c, ctl), ShouldBeNil) 294 So(loadNoError(), ShouldResemble, strmap{ 295 "refs/heads/master": "deadbeef00", 296 }) 297 So(ctl.Triggers, ShouldHaveLength, 0) 298 }) 299 300 Convey("do nothing at all if there are no changes", func() { 301 cfg.Refs = []string{"refs/heads/master"} 302 So(saveState(c, jobID, cfg.Repo, strmap{ 303 "refs/heads/master": "deadbeef", 304 }), ShouldBeNil) 305 expectRefs("refs/heads", strmap{ 306 "refs/heads/master": "deadbeef", 307 }) 308 So(m.LaunchTask(c, ctl), ShouldBeNil) 309 So(ctl.Triggers, ShouldBeNil) 310 So(ctl.Log, ShouldNotContain, "Saved 1 known refs") 311 So(ctl.Log, ShouldContain, "No changes detected") 312 So(loadNoError(), ShouldResemble, strmap{ 313 "refs/heads/master": "deadbeef", 314 }) 315 }) 316 317 Convey("Avoid choking on too many refs", func() { 318 cfg.Refs = []string{"refs/heads/master", "regexp:refs/branch-heads/[^/]+"} 319 So(saveState(c, jobID, cfg.Repo, strmap{ 320 "refs/heads/master": "deadbeef", 321 }), ShouldBeNil) 322 expectRefs("refs/heads", strmap{"refs/heads/master": "deadbeef"}).AnyTimes() 323 expectRefs("refs/branch-heads", strmap{ 324 "refs/branch-heads/1": "cafee1", 325 "refs/branch-heads/2": "cafee2", 326 "refs/branch-heads/3": "cafee3", 327 "refs/branch-heads/4": "cafee4", 328 "refs/branch-heads/5": "cafee5", 329 }).AnyTimes() 330 expectLog("cafee1", "", 1, log("cafee1")) 331 expectLog("cafee2", "", 1, log("cafee2")) 332 expectLog("cafee3", "", 1, log("cafee3")) 333 expectLog("cafee4", "", 1, log("cafee4")) 334 expectLog("cafee5", "", 1, log("cafee5")) 335 m.maxTriggersPerInvocation = 2 336 m.maxCommitsPerRefUpdate = 1 337 338 // First run, refs/branch-heads/{1,2} updated, refs/heads/master preserved. 339 So(m.LaunchTask(c, ctl), ShouldBeNil) 340 So(ctl.Triggers, ShouldHaveLength, 2) 341 So(loadNoError(), ShouldResemble, strmap{ 342 "refs/heads/master": "deadbeef", 343 "refs/branch-heads/1": "cafee1", 344 "refs/branch-heads/2": "cafee2", 345 }) 346 ctl.Triggers = nil 347 348 // Second run, refs/branch-heads/{3,4} updated. 349 So(m.LaunchTask(c, ctl), ShouldBeNil) 350 So(ctl.Triggers, ShouldHaveLength, 2) 351 So(loadNoError(), ShouldResemble, strmap{ 352 "refs/heads/master": "deadbeef", 353 "refs/branch-heads/1": "cafee1", 354 "refs/branch-heads/2": "cafee2", 355 "refs/branch-heads/3": "cafee3", 356 "refs/branch-heads/4": "cafee4", 357 }) 358 ctl.Triggers = nil 359 360 // Final run, refs/branch-heads/5 updated. 361 So(m.LaunchTask(c, ctl), ShouldBeNil) 362 So(ctl.Triggers, ShouldHaveLength, 1) 363 So(loadNoError(), ShouldResemble, strmap{ 364 "refs/heads/master": "deadbeef", 365 "refs/branch-heads/1": "cafee1", 366 "refs/branch-heads/2": "cafee2", 367 "refs/branch-heads/3": "cafee3", 368 "refs/branch-heads/4": "cafee4", 369 "refs/branch-heads/5": "cafee5", 370 }) 371 }) 372 373 Convey("Ensure progress", func() { 374 cfg.Refs = []string{"regexp:refs/branch-heads/[^/]+"} 375 expectRefs("refs/branch-heads", strmap{ 376 "refs/branch-heads/1": "cafee1", 377 "refs/branch-heads/2": "cafee2", 378 "refs/branch-heads/3": "cafee3", 379 }).AnyTimes() 380 m.maxTriggersPerInvocation = 2 381 m.maxCommitsPerRefUpdate = 1 382 383 Convey("no progress is an error", func() { 384 expectLog("cafee1", "", 1, log(), errors.New("flake")) 385 So(m.LaunchTask(c, ctl), ShouldErrLike, "flake") 386 }) 387 388 expectLog("cafee1", "", 1, log("cafee1")) 389 expectLog("cafee2", "", 1, log(), errors.New("flake")) 390 So(m.LaunchTask(c, ctl), ShouldBeNil) 391 So(ctl.Triggers, ShouldHaveLength, 1) 392 So(loadNoError(), ShouldResemble, strmap{ 393 "refs/branch-heads/1": "cafee1", 394 }) 395 ctl.Triggers = nil 396 So(loadNoError(), ShouldResemble, strmap{ 397 "refs/branch-heads/1": "cafee1", 398 }) 399 400 // Second run. 401 expectLog("cafee2", "", 1, log("cafee2")) 402 expectLog("cafee3", "", 1, log("cafee3")) 403 So(m.LaunchTask(c, ctl), ShouldBeNil) 404 So(ctl.Triggers, ShouldHaveLength, 2) 405 So(loadNoError(), ShouldResemble, strmap{ 406 "refs/branch-heads/1": "cafee1", 407 "refs/branch-heads/2": "cafee2", 408 "refs/branch-heads/3": "cafee3", 409 }) 410 }) 411 412 Convey("distinguish force push from transient weirdness", func() { 413 cfg.Refs = []string{"refs/heads/master"} 414 So(saveState(c, jobID, cfg.Repo, strmap{ 415 "refs/heads/master": "001d", // old. 416 }), ShouldBeNil) 417 expectRefs("refs/heads", strmap{"refs/heads/master": "1111"}) 418 419 Convey("force push going backwards", func() { 420 expectLog("1111", "001d", 50, log()) 421 So(m.LaunchTask(c, ctl), ShouldBeNil) 422 // Changes state 423 So(loadNoError(), ShouldResemble, strmap{ 424 "refs/heads/master": "1111", 425 }) 426 // .. but no triggers, since there are no new commits. 427 So(ctl.Triggers, ShouldHaveLength, 0) 428 }) 429 430 Convey("force push wiping out prior HEAD", func() { 431 expectLog("1111", "001d", 50, nil, status.Errorf(codes.NotFound, "not found")) 432 expectLog("1111", "", 1, log("1111")) 433 expectLog("001d", "", 1, nil, status.Errorf(codes.NotFound, "not found")) 434 So(m.LaunchTask(c, ctl), ShouldBeNil) 435 So(loadNoError(), ShouldResemble, strmap{ 436 "refs/heads/master": "1111", 437 }) 438 So(ctl.Triggers, ShouldHaveLength, 1) 439 }) 440 441 Convey("race 1", func() { 442 expectLog("1111", "001d", 50, nil, status.Errorf(codes.NotFound, "not found")) 443 expectLog("1111", "", 1, nil, status.Errorf(codes.NotFound, "not found")) 444 So(transient.Tag.In(m.LaunchTask(c, ctl)), ShouldBeTrue) 445 So(loadNoError(), ShouldResemble, strmap{ 446 "refs/heads/master": "001d", // no change. 447 }) 448 }) 449 450 Convey("race or fluke", func() { 451 expectLog("1111", "001d", 50, nil, status.Errorf(codes.NotFound, "not found")) 452 expectLog("1111", "", 1, nil, status.Errorf(codes.NotFound, "not found")) 453 So(m.LaunchTask(c, ctl), ShouldNotBeNil) 454 So(loadNoError(), ShouldResemble, strmap{ 455 "refs/heads/master": "001d", 456 }) 457 }) 458 }) 459 }) 460 } 461 462 func TestPathFilterHelpers(t *testing.T) { 463 t.Parallel() 464 465 Convey("PathFilter helpers work", t, func() { 466 Convey("disjunctiveOfRegexps works", func() { 467 So(disjunctiveOfRegexps([]string{`.+\.cpp`}), ShouldEqual, `^((.+\.cpp))$`) 468 So(disjunctiveOfRegexps([]string{`.+\.cpp`, `?a`}), ShouldEqual, `^((.+\.cpp)|(?a))$`) 469 }) 470 Convey("pathFilter works", func() { 471 Convey("simple", func() { 472 empty, err := newPathFilter(&messages.GitilesTask{}) 473 So(err, ShouldBeNil) 474 So(empty.active(), ShouldBeFalse) 475 _, err = newPathFilter(&messages.GitilesTask{PathRegexps: []string{`\K`}}) 476 So(err, ShouldNotBeNil) 477 _, err = newPathFilter(&messages.GitilesTask{PathRegexps: []string{`a?`}, PathRegexpsExclude: []string{`\K`}}) 478 So(err, ShouldNotBeNil) 479 480 }) 481 Convey("just negative ignored", func() { 482 v, err := newPathFilter(&messages.GitilesTask{PathRegexpsExclude: []string{`.+\.cpp`}}) 483 So(err, ShouldBeNil) 484 So(v.active(), ShouldBeFalse) 485 }) 486 487 Convey("just positive", func() { 488 v, err := newPathFilter(&messages.GitilesTask{PathRegexps: []string{`.+`}}) 489 So(err, ShouldBeNil) 490 So(v.active(), ShouldBeTrue) 491 Convey("empty commit is not interesting", func() { 492 So(v.isInteresting([]*git.Commit_TreeDiff{}), ShouldBeFalse) 493 }) 494 Convey("new or old paths are taken into account", func() { 495 So(v.isInteresting([]*git.Commit_TreeDiff{{OldPath: "old"}}), ShouldBeTrue) 496 So(v.isInteresting([]*git.Commit_TreeDiff{{NewPath: "new"}}), ShouldBeTrue) 497 }) 498 }) 499 500 genDiff := func(files ...string) []*git.Commit_TreeDiff { 501 r := make([]*git.Commit_TreeDiff, len(files)) 502 for i, f := range files { 503 if i&1 == 0 { 504 r[i] = &git.Commit_TreeDiff{OldPath: f} 505 } else { 506 r[i] = &git.Commit_TreeDiff{NewPath: f} 507 } 508 } 509 return r 510 } 511 512 Convey("many positives", func() { 513 v, err := newPathFilter(&messages.GitilesTask{PathRegexps: []string{`.+\.cpp`, "exact"}}) 514 So(err, ShouldBeNil) 515 So(v.isInteresting(genDiff("not.matched")), ShouldBeFalse) 516 517 So(v.isInteresting(genDiff("matched.cpp")), ShouldBeTrue) 518 So(v.isInteresting(genDiff("exact")), ShouldBeTrue) 519 So(v.isInteresting(genDiff("at least", "one", "matched.cpp")), ShouldBeTrue) 520 }) 521 522 Convey("many negatives", func() { 523 v, err := newPathFilter(&messages.GitilesTask{ 524 PathRegexps: []string{`.+`}, 525 PathRegexpsExclude: []string{`.+\.cpp`, `excluded`}, 526 }) 527 So(err, ShouldBeNil) 528 So(v.isInteresting(genDiff("not excluded")), ShouldBeTrue) 529 So(v.isInteresting(genDiff("excluded/is/a/dir/not/a/file")), ShouldBeTrue) 530 So(v.isInteresting(genDiff("excluded", "also.excluded.cpp", "but this file isn't")), ShouldBeTrue) 531 532 So(v.isInteresting(genDiff("excluded.cpp")), ShouldBeFalse) 533 So(v.isInteresting(genDiff("excluded")), ShouldBeFalse) 534 So(v.isInteresting(genDiff()), ShouldBeFalse) 535 }) 536 537 Convey("smoke test for complexity", func() { 538 v, err := newPathFilter(&messages.GitilesTask{ 539 PathRegexps: []string{`.+/\d\.py`, `included/.+`}, 540 PathRegexpsExclude: []string{`.+\.cpp`, `excluded/.*`}, 541 }) 542 So(err, ShouldBeNil) 543 So(v.isInteresting(genDiff("excluded/1", "also.cpp", "included/one-is-enough")), ShouldBeTrue) 544 So(v.isInteresting(genDiff("included/but-also-excluded.cpp", "one-still-enough/1.py")), ShouldBeTrue) 545 546 So(v.isInteresting(genDiff("included/but-also-excluded.cpp", "excluded/2.py")), ShouldBeFalse) 547 So(v.isInteresting(genDiff("matches nothing", "")), ShouldBeFalse) 548 }) 549 }) 550 }) 551 } 552 553 func TestValidateConfig(t *testing.T) { 554 t.Parallel() 555 c := context.Background() 556 557 Convey("ValidateProtoMessage works", t, func() { 558 ctx := &validation.Context{Context: c} 559 m := TaskManager{} 560 validate := func(msg proto.Message) error { 561 m.ValidateProtoMessage(ctx, msg, "some-project:some-realm") 562 return ctx.Finalize() 563 } 564 Convey("refNamespace works", func() { 565 cfg := &messages.GitilesTask{ 566 Repo: "https://a.googlesource.com/b.git", 567 Refs: []string{"refs/heads/master", "refs/heads/branch", "regexp:refs/branch-heads/[^/]+"}, 568 } 569 Convey("proper refs", func() { 570 So(validate(cfg), ShouldBeNil) 571 }) 572 Convey("invalid ref", func() { 573 cfg.Refs = []string{"wtf/not/a/ref"} 574 So(validate(cfg), ShouldNotBeNil) 575 }) 576 }) 577 578 Convey("refRegexp works", func() { 579 cfg := &messages.GitilesTask{ 580 Repo: "https://a.googlesource.com/b.git", 581 Refs: []string{ 582 `regexp:refs/heads/\d+`, 583 `regexp:refs/actually/exact`, 584 `refs/heads/master`, 585 }, 586 } 587 Convey("valid", func() { 588 So(validate(cfg), ShouldBeNil) 589 }) 590 Convey("invalid regexp", func() { 591 cfg.Refs = []string{`regexp:a++`} 592 So(validate(cfg), ShouldNotBeNil) 593 }) 594 }) 595 596 Convey("pathRegexs works", func() { 597 cfg := &messages.GitilesTask{ 598 Repo: "https://a.googlesource.com/b.git", 599 Refs: []string{"refs/heads/master"}, 600 PathRegexps: []string{`.+\.cpp`}, 601 PathRegexpsExclude: []string{`.+\.py`}, 602 } 603 Convey("valid", func() { 604 So(validate(cfg), ShouldBeNil) 605 }) 606 Convey("can't even parse", func() { 607 cfg.PathRegexpsExclude = []string{`\K`} 608 So(validate(cfg), ShouldNotBeNil) 609 }) 610 Convey("redundant", func() { 611 cfg.PathRegexps = []string{``} 612 So(validate(cfg), ShouldNotBeNil) 613 cfg.PathRegexps = []string{`^file`} 614 So(validate(cfg), ShouldNotBeNil) 615 cfg.PathRegexps = []string{`file$`} 616 So(validate(cfg), ShouldNotBeNil) 617 }) 618 Convey("excludes require includes", func() { 619 cfg.PathRegexps = nil 620 So(validate(cfg), ShouldNotBeNil) 621 }) 622 }) 623 }) 624 }