go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/git/combined_logs_test.go (about) 1 // Copyright 2018 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 git 16 17 import ( 18 "context" 19 "encoding/hex" 20 "testing" 21 "time" 22 23 "github.com/golang/mock/gomock" 24 "google.golang.org/protobuf/types/known/timestamppb" 25 26 "go.chromium.org/luci/auth/identity" 27 "go.chromium.org/luci/common/proto" 28 gitpb "go.chromium.org/luci/common/proto/git" 29 gitilespb "go.chromium.org/luci/common/proto/gitiles" 30 "go.chromium.org/luci/common/proto/gitiles/mock_gitiles" 31 "go.chromium.org/luci/gae/impl/memory" 32 "go.chromium.org/luci/milo/internal/git/gitacls" 33 configpb "go.chromium.org/luci/milo/proto/config" 34 "go.chromium.org/luci/server/auth" 35 "go.chromium.org/luci/server/auth/authtest" 36 37 . "github.com/smartystreets/goconvey/convey" 38 . "go.chromium.org/luci/common/testing/assertions" 39 ) 40 41 func TestCombinedLogs(t *testing.T) { 42 t.Parallel() 43 44 Convey("CombinedLogs", t, func() { 45 c := memory.Use(context.Background()) 46 47 ctl := gomock.NewController(t) 48 defer ctl.Finish() 49 gitilesMock := mock_gitiles.NewMockGitilesClient(ctl) 50 51 host := "limited.googlesource.com" 52 acls, err := gitacls.FromConfig(c, []*configpb.Settings_SourceAcls{ 53 {Hosts: []string{host}, Readers: []string{"allowed@example.com"}}, 54 }) 55 So(err, ShouldBeNil) 56 impl := implementation{mockGitiles: gitilesMock, acls: acls} 57 c = Use(c, &impl) 58 cAllowed := auth.WithState(c, &authtest.FakeState{Identity: "user:allowed@example.com"}) 59 cDenied := auth.WithState(c, &authtest.FakeState{Identity: identity.AnonymousIdentity}) 60 61 fakeCommits := make([]*gitpb.Commit, 30) 62 commitID := make([]byte, 20) 63 commitID[0] = 255 64 epoch, err := time.Parse(time.RFC3339, "2018-06-22T19:34:06Z") 65 So(err, ShouldBeNil) 66 for i := range fakeCommits { 67 fakeCommits[i] = &gitpb.Commit{ 68 Id: hex.EncodeToString(commitID), 69 Committer: &gitpb.Commit_User{ 70 Time: timestamppb.New( // each next commit is 1 minute older 71 epoch.Add(-time.Duration(i) * time.Minute)), 72 }, 73 } 74 commitID[0]-- 75 } 76 77 type refTips map[string]string 78 mockRefsCall := func(prefix string, tips refTips) *gomock.Call { 79 return gitilesMock.EXPECT().Refs(gomock.Any(), proto.MatcherEqual(&gitilespb.RefsRequest{ 80 Project: "project", 81 RefsPath: prefix, 82 })).Return(&gitilespb.RefsResponse{Revisions: tips}, nil) 83 } 84 85 mockLogCall := func(reqCommit string, respCommits []*gitpb.Commit) *gomock.Call { 86 return gitilesMock.EXPECT().Log(gomock.Any(), proto.MatcherEqual(&gitilespb.LogRequest{ 87 Project: "project", Committish: reqCommit, 88 PageSize: 100, ExcludeAncestorsOf: "refs/heads/main", 89 })).Return(&gitilespb.LogResponse{Log: respCommits}, nil) 90 } 91 92 Convey("ACLs respected", func() { 93 _, err := impl.CombinedLogs( 94 cDenied, host, "project", "refs/heads/main", 95 []string{`regexp:refs/branch-heads/\d+\.\d+`}, 50) 96 So(err.Error(), ShouldContainSubstring, "not logged in") 97 }) 98 99 Convey("no refs match", func() { 100 mockRefsCall("refs/branch-heads", refTips{}) 101 commits, err := impl.CombinedLogs( 102 cAllowed, host, "project", "refs/heads/main", 103 []string{`regexp:refs/branch-heads/\d+\.\d+`}, 50) 104 So(err, ShouldBeNil) 105 So(len(commits), ShouldEqual, 0) 106 }) 107 108 Convey("one ref matches", func() { 109 mockRefsCall("refs/branch-heads", refTips{ 110 "refs/branch-heads/1.1": fakeCommits[0].Id, 111 }) 112 113 mockLogCall(fakeCommits[0].Id, fakeCommits[0:5]) 114 115 commits, err := impl.CombinedLogs( 116 cAllowed, host, "project", "refs/heads/main", 117 []string{`regexp:refs/branch-heads/\d+\.\d+`}, 50) 118 So(err, ShouldBeNil) 119 So(commits, ShouldResemble, fakeCommits[0:5]) 120 }) 121 122 Convey("multiple refs match and commits are merged correctly", func() { 123 mockRefsCall("refs/branch-heads", refTips{ 124 "refs/branch-heads/1.1": fakeCommits[0].Id, 125 "refs/branch-heads/1.2": fakeCommits[10].Id, 126 }) 127 mockRefsCall("refs/heads", refTips{ 128 "refs/heads/1.3.195": fakeCommits[20].Id, 129 }) 130 131 // Change commit times in order to test merging logic. This still keeps 132 // the order of commits on each ref, but should change the order in the 133 // merged list by moving: 134 // - commit 2 back in time between 22 and 23, 135 // - commit 3 back in time past 23 (should be truncated by limit) and 136 // - commit 20 forward in time between 0 and 1. 137 fakeCommits[2].Committer.Time = timestamppb.New( 138 epoch.Add(-time.Duration(22)*time.Minute - time.Second)) 139 fakeCommits[3].Committer.Time = timestamppb.New( 140 epoch.Add(-time.Duration(23)*time.Minute - time.Second)) 141 fakeCommits[20].Committer.Time = timestamppb.New( 142 epoch.Add(-time.Duration(0)*time.Minute - time.Second)) 143 144 mockLogCall(fakeCommits[0].Id, fakeCommits[0:4]) 145 mockLogCall(fakeCommits[10].Id, fakeCommits[10:10]) // empty list 146 mockLogCall(fakeCommits[20].Id, fakeCommits[20:30]) 147 148 commits, err := impl.CombinedLogs( 149 cAllowed, host, "project", "refs/heads/main", []string{ 150 `regexp:refs/branch-heads/\d+\.\d+`, 151 `regexp:refs/heads/\d+\.\d+\.\d+`, 152 }, 7) 153 So(err, ShouldBeNil) 154 So(commits, ShouldResemble, []*gitpb.Commit{ 155 fakeCommits[0], fakeCommits[20], fakeCommits[1], fakeCommits[21], 156 fakeCommits[22], fakeCommits[2], fakeCommits[23], 157 }) 158 }) 159 160 Convey("multiple refs match and their commits deduped", func() { 161 mockRefsCall("refs/branch-heads", refTips{ 162 "refs/branch-heads/1.1": fakeCommits[0].Id, 163 "refs/branch-heads/1.2": fakeCommits[5].Id, 164 }) 165 166 mockLogCall(fakeCommits[0].Id, fakeCommits[0:10]) 167 mockLogCall(fakeCommits[5].Id, fakeCommits[5:10]) 168 169 commits, err := impl.CombinedLogs( 170 cAllowed, host, "project", "refs/heads/main", 171 []string{`regexp:refs/branch-heads/\d+\.\d+`}, 50) 172 So(err, ShouldBeNil) 173 So(commits, ShouldResemble, fakeCommits[0:10]) 174 }) 175 176 Convey("use result from cache when available", func() { 177 mockRefsCall("refs/branch-heads", refTips{ 178 "refs/branch-heads/1.1": fakeCommits[0].Id, 179 "refs/branch-heads/1.2": fakeCommits[10].Id, 180 }).Times(2) 181 182 mockLogCall(fakeCommits[0].Id, fakeCommits[0:10]).Times(1) 183 mockLogCall(fakeCommits[10].Id, fakeCommits[10:20]).Times(1) 184 185 commits, err := impl.CombinedLogs( 186 cAllowed, host, "project", "refs/heads/main", 187 []string{`regexp:refs/branch-heads/\d+\.\d+`}, 50) 188 So(err, ShouldBeNil) 189 So(commits, ShouldResembleProto, fakeCommits[0:20]) 190 191 // This call should use logs from cache. 192 commits, err = impl.CombinedLogs( 193 cAllowed, host, "project", "refs/heads/main", 194 []string{`regexp:refs/branch-heads/\d+\.\d+`}, 50) 195 So(err, ShouldBeNil) 196 So(commits, ShouldResembleProto, fakeCommits[0:20]) 197 }) 198 199 Convey("invalidate cache when ref moves", func() { 200 firstRefsCall := mockRefsCall("refs/branch-heads", refTips{ 201 "refs/branch-heads/1.1": fakeCommits[0].Id, 202 "refs/branch-heads/1.2": fakeCommits[11].Id, 203 }) 204 205 mockRefsCall("refs/branch-heads", refTips{ 206 "refs/branch-heads/1.1": fakeCommits[0].Id, 207 "refs/branch-heads/1.2": fakeCommits[10].Id, 208 }).After(firstRefsCall) 209 210 mockLogCall(fakeCommits[0].Id, fakeCommits[0:2]) 211 mockLogCall(fakeCommits[11].Id, fakeCommits[11:13]) 212 213 // This call is required due to moved ref. 214 mockLogCall(fakeCommits[10].Id, fakeCommits[10:13]) 215 216 commits, err := impl.CombinedLogs( 217 cAllowed, host, "project", "refs/heads/main", 218 []string{`regexp:refs/branch-heads/\d+\.\d+`}, 50) 219 So(err, ShouldBeNil) 220 So(commits, ShouldResembleProto, []*gitpb.Commit{ 221 fakeCommits[0], fakeCommits[1], fakeCommits[11], fakeCommits[12]}) 222 223 commits, err = impl.CombinedLogs( 224 cAllowed, host, "project", "refs/heads/main", 225 []string{`regexp:refs/branch-heads/\d+\.\d+`}, 50) 226 So(err, ShouldBeNil) 227 So(commits, ShouldResembleProto, []*gitpb.Commit{ 228 fakeCommits[0], fakeCommits[1], fakeCommits[10], fakeCommits[11], 229 fakeCommits[12]}) 230 }) 231 }) 232 }