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  }