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  }