go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/gitiles/refset_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 gitiles
    16  
    17  import (
    18  	"context"
    19  	"regexp"
    20  	"testing"
    21  
    22  	"github.com/golang/mock/gomock"
    23  	. "github.com/smartystreets/goconvey/convey"
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/proto"
    26  	"go.chromium.org/luci/common/proto/gitiles"
    27  	"go.chromium.org/luci/common/proto/gitiles/mock_gitiles"
    28  	"go.chromium.org/luci/config/validation"
    29  )
    30  
    31  func TestRefSet(t *testing.T) {
    32  	t.Parallel()
    33  
    34  	Convey("RefSet", t, func() {
    35  		wr := NewRefSet([]string{
    36  			`refs/heads/master`,
    37  			`regexp:refs/branch-heads/\d+\.\d+`,
    38  			`regexp:refs/missing/many.+`,
    39  			`refs/missing/exact`,
    40  		})
    41  
    42  		Convey("explicit refs", func() {
    43  			So(wr.Has("refs/heads/master"), ShouldBeTrue)
    44  			So(wr.Has("refs/heads/foo"), ShouldBeFalse)
    45  		})
    46  
    47  		Convey("regexp refs", func() {
    48  			So(wr.Has("refs/branch-heads/1.12"), ShouldBeTrue)
    49  			So(wr.Has("refs/branch-heads/1.12.123"), ShouldBeFalse)
    50  		})
    51  
    52  		Convey("resolve ref tips", func() {
    53  			ctx := context.Background()
    54  			ctl := gomock.NewController(t)
    55  			defer ctl.Finish()
    56  			mockClient := mock_gitiles.NewMockGitilesClient(ctl)
    57  
    58  			mockClient.EXPECT().Refs(gomock.Any(), proto.MatcherEqual(&gitiles.RefsRequest{
    59  				Project: "project", RefsPath: "refs/heads",
    60  			})).Return(
    61  				&gitiles.RefsResponse{Revisions: map[string]string{
    62  					"refs/heads/master": "01234567",
    63  					"refs/heads/foobar": "89abcdef",
    64  				}}, nil,
    65  			)
    66  			mockClient.EXPECT().Refs(gomock.Any(), proto.MatcherEqual(&gitiles.RefsRequest{
    67  				Project: "project", RefsPath: "refs/missing",
    68  			})).Return(&gitiles.RefsResponse{}, nil)
    69  
    70  			Convey("normal", func() {
    71  				mockClient.EXPECT().Refs(gomock.Any(), proto.MatcherEqual(&gitiles.RefsRequest{
    72  					Project: "project", RefsPath: "refs/branch-heads",
    73  				})).Return(
    74  					&gitiles.RefsResponse{Revisions: map[string]string{
    75  						"refs/branch-heads/1.9":      "cafedead",
    76  						"refs/branch-heads/1.10":     "deadcafe",
    77  						"refs/branch-heads/1.11.123": "deadbeef",
    78  					}}, nil,
    79  				)
    80  
    81  				refTips, missing, err := wr.Resolve(ctx, mockClient, "project")
    82  				So(err, ShouldBeNil)
    83  				So(refTips, ShouldResemble, map[string]string{
    84  					"refs/heads/master":      "01234567",
    85  					"refs/branch-heads/1.9":  "cafedead",
    86  					"refs/branch-heads/1.10": "deadcafe",
    87  				})
    88  				So(missing, ShouldResemble, []string{`refs/missing/exact`, `regexp:refs/missing/many.+`})
    89  			})
    90  
    91  			Convey("failed RPCs", func() {
    92  				mockClient.EXPECT().Refs(gomock.Any(), proto.MatcherEqual(&gitiles.RefsRequest{
    93  					Project: "project", RefsPath: "refs/branch-heads",
    94  				})).Return(
    95  					nil, errors.New("foobar"),
    96  				)
    97  
    98  				_, _, err := wr.Resolve(ctx, mockClient, "project")
    99  				So(err.Error(), ShouldContainSubstring, "foobar")
   100  			})
   101  		})
   102  	})
   103  
   104  	Convey("ValidateRefSet", t, func() {
   105  		ctx := &validation.Context{Context: context.Background()}
   106  
   107  		Convey("plain refs", func() {
   108  			Convey("too few slashes", func() {
   109  				ValidateRefSet(ctx, []string{`refs/foo`})
   110  				So(ctx.Finalize().Error(), ShouldContainSubstring,
   111  					`fewer than 2 slashes in ref "refs/foo"`)
   112  			})
   113  			Convey("does not start with refs/", func() {
   114  				ValidateRefSet(ctx, []string{`foo/bar/baz`})
   115  				So(ctx.Finalize().Error(), ShouldContainSubstring,
   116  					`ref must start with 'refs/' not "foo/bar/baz"`)
   117  			})
   118  			Convey("valid", func() {
   119  				ValidateRefSet(ctx, []string{`refs/heads/master`})
   120  				So(ctx.Finalize(), ShouldBeNil)
   121  			})
   122  		})
   123  
   124  		Convey("regexp refs", func() {
   125  			Convey("starts with ^ or ends with $", func() {
   126  				ValidateRefSet(ctx, []string{`regexp:^refs/branch-heads/\d+\.\d+$`})
   127  				So(ctx.Finalize().Error(), ShouldContainSubstring,
   128  					`^ and $ qualifiers are added automatically, please remove them`)
   129  			})
   130  			Convey("invalid regexp", func() {
   131  				ValidateRefSet(ctx, []string{`regexp:([{`})
   132  				So(ctx.Finalize().Error(), ShouldContainSubstring, `invalid regexp`)
   133  			})
   134  			Convey("matches single ref only is fine", func() {
   135  				ValidateRefSet(ctx, []string{`regexp:refs/h[e]ad(s)/m[a]ster`})
   136  				So(ctx.Finalize(), ShouldBeNil)
   137  			})
   138  			Convey("fewer than 2 slashes in literal prefix", func() {
   139  				ValidateRefSet(ctx, []string{`regexp:refs/branch[-_]heads/\d+\/\d+`})
   140  				So(ctx.Finalize().Error(), ShouldContainSubstring,
   141  					`fewer than 2 slashes in literal prefix "refs/branch"`)
   142  			})
   143  			Convey("does not start with refs/", func() {
   144  				ValidateRefSet(ctx, []string{`regexp:foo/branch-heads/\d+\/\d+`})
   145  				So(ctx.Finalize().Error(), ShouldContainSubstring,
   146  					`literal prefix "foo/branch-heads/" must start with "refs/"`)
   147  			})
   148  			Convey("non-trivial ref prefix is supported", func() {
   149  				ValidateRefSet(ctx, []string{`regexp:refs/foo\.bar/\d+`})
   150  				So(ctx.Finalize(), ShouldBeNil)
   151  			})
   152  			Convey("not-trivial literal prefix is supported", func() {
   153  				ValidateRefSet(ctx, []string{`regexp:refs/branch-heads/(6\.8|6\.9)\.\d+`})
   154  				So(ctx.Finalize(), ShouldBeNil)
   155  			})
   156  			Convey("valid", func() {
   157  				ValidateRefSet(ctx, []string{`regexp:refs/branch-heads/\d+\.\d+`})
   158  				So(ctx.Finalize(), ShouldBeNil)
   159  			})
   160  		})
   161  	})
   162  
   163  	Convey("smoke test of LiteralPrefix not working as expected", t, func() {
   164  		r := "refs/heads/\\d+\\.\\d+.\\d"
   165  		l1, _ := regexp.MustCompile(r).LiteralPrefix()
   166  		l2, _ := regexp.MustCompile("^" + r + "$").LiteralPrefix()
   167  		So(l1, ShouldResemble, "refs/heads/")
   168  		So(l2, ShouldResemble, "") // See https://github.com/golang/go/issues/30425
   169  		NewRefSet([]string{"regexp:" + r})
   170  	})
   171  }