go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/usertext/cl_error_test.go (about)

     1  // Copyright 2021 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 usertext
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  
    21  	"google.golang.org/protobuf/types/known/timestamppb"
    22  
    23  	"go.chromium.org/luci/common/clock/testclock"
    24  	"go.chromium.org/luci/common/data/text"
    25  	"go.chromium.org/luci/gae/impl/memory"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  
    28  	"go.chromium.org/luci/cv/internal/changelist"
    29  	gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
    30  	"go.chromium.org/luci/cv/internal/run"
    31  
    32  	. "github.com/smartystreets/goconvey/convey"
    33  )
    34  
    35  func TestFormatCLError(t *testing.T) {
    36  	t.Parallel()
    37  
    38  	Convey("CLError formatting works", t, func() {
    39  		ctx := memory.Use(context.Background())
    40  		const gHost = "x-review.googlesource.com"
    41  		ci := gf.CI(
    42  			43, gf.PS(2), gf.Project("re/po"), gf.Ref("refs/heads/main"),
    43  			gf.CQ(+2, testclock.TestRecentTimeUTC, gf.U("user-1")),
    44  			gf.Updated(testclock.TestRecentTimeUTC),
    45  		)
    46  		cl := &changelist.CL{
    47  			Snapshot: &changelist.Snapshot{
    48  				Kind: &changelist.Snapshot_Gerrit{
    49  					Gerrit: &changelist.Gerrit{
    50  						Host: gHost,
    51  						Info: ci,
    52  					},
    53  				},
    54  			},
    55  		}
    56  
    57  		Convey("Single CLError", func() {
    58  			// reason.Kind and mode are set in tests below as needed.
    59  			reason := &changelist.CLError{Kind: nil}
    60  			var mode run.Mode
    61  			mustFormat := func() string {
    62  				s, err := SFormatCLError(ctx, reason, cl, mode)
    63  				So(err, ShouldBeNil)
    64  				So(s, ShouldNotContainSubstring, "<no value>")
    65  				return s
    66  			}
    67  			Convey("Lacks owner email", func() {
    68  				reason.Kind = &changelist.CLError_OwnerLacksEmail{
    69  					OwnerLacksEmail: true,
    70  				}
    71  				So(mustFormat(), ShouldContainSubstring, "set preferred email at https://x-review.googlesource.com/settings/#EmailAddresses")
    72  			})
    73  			Convey("Not yet supported mode", func() {
    74  				reason.Kind = &changelist.CLError_UnsupportedMode{
    75  					UnsupportedMode: "CUSTOM_RUN",
    76  				}
    77  				So(mustFormat(), ShouldContainSubstring, `its mode "CUSTOM_RUN" is not supported`)
    78  			})
    79  			Convey("Depends on itself", func() {
    80  				reason.Kind = &changelist.CLError_SelfCqDepend{SelfCqDepend: true}
    81  				So(mustFormat(), ShouldContainSubstring, `because it depends on itself`)
    82  			})
    83  			Convey("CorruptGerrit", func() {
    84  				reason.Kind = &changelist.CLError_CorruptGerritMetadata{CorruptGerritMetadata: "foo is not bar"}
    85  				So(mustFormat(), ShouldContainSubstring, `foo is not bar`)
    86  			})
    87  			Convey("Watched by many config groups", func() {
    88  				reason.Kind = &changelist.CLError_WatchedByManyConfigGroups_{
    89  					WatchedByManyConfigGroups: &changelist.CLError_WatchedByManyConfigGroups{
    90  						ConfigGroups: []string{"first", "second"},
    91  					},
    92  				}
    93  				s := mustFormat()
    94  				So(s, ShouldContainSubstring, text.Doc(`
    95  				it is watched by more than 1 config group:
    96  				  * first
    97  				  * second
    98  
    99  				Please
   100  			`))
   101  				So(s, ShouldContainSubstring, `current CL target ref is "refs/heads/main"`)
   102  			})
   103  			Convey("Watched by many LUCI projects", func() {
   104  				reason.Kind = &changelist.CLError_WatchedByManyProjects_{
   105  					WatchedByManyProjects: &changelist.CLError_WatchedByManyProjects{
   106  						Projects: []string{"first", "second"},
   107  					},
   108  				}
   109  				s := mustFormat()
   110  				So(s, ShouldContainSubstring, text.Doc(`
   111  				it is watched by more than 1 LUCI project:
   112  				  * first
   113  				  * second
   114  
   115  				Please
   116  			`))
   117  			})
   118  			Convey("Invalid deps", func() {
   119  				// Save a CL snapshot for each dep.
   120  				deps := make(map[int]*changelist.Dep, 3)
   121  				for i := 101; i <= 102; i++ {
   122  					depCL := changelist.MustGobID(gHost, int64(i)).MustCreateIfNotExists(ctx)
   123  					depCL.Snapshot = &changelist.Snapshot{
   124  						LuciProject:           "whatever",
   125  						MinEquivalentPatchset: 1,
   126  						Patchset:              2,
   127  						ExternalUpdateTime:    timestamppb.New(testclock.TestRecentTimeUTC),
   128  						Kind: &changelist.Snapshot_Gerrit{
   129  							Gerrit: &changelist.Gerrit{
   130  								Host: gHost,
   131  								Info: gf.CI(i),
   132  							},
   133  						},
   134  					}
   135  					So(datastore.Put(ctx, depCL), ShouldBeNil)
   136  					deps[i] = &changelist.Dep{Clid: int64(depCL.ID)}
   137  				}
   138  				invalidDeps := &changelist.CLError_InvalidDeps{ /*set below*/ }
   139  				reason.Kind = &changelist.CLError_InvalidDeps_{InvalidDeps: invalidDeps}
   140  
   141  				Convey("Unwatched", func() {
   142  					invalidDeps.Unwatched = []*changelist.Dep{deps[101]}
   143  					s := mustFormat()
   144  					So(s, ShouldContainSubstring, text.Doc(`
   145  				are not watched by the same LUCI project:
   146  				  * https://x-review.googlesource.com/c/101
   147  
   148  				Please check Cq-Depend
   149  			`))
   150  				})
   151  				Convey("WrongConfigGroup", func() {
   152  					invalidDeps.WrongConfigGroup = []*changelist.Dep{deps[101], deps[102]}
   153  					s := mustFormat()
   154  					So(s, ShouldContainSubstring, text.Doc(`
   155  				its deps do not belong to the same config group:
   156  				  * https://x-review.googlesource.com/c/101
   157  				  * https://x-review.googlesource.com/c/102
   158  			`))
   159  				})
   160  				Convey("Singular Full Run with open dependencies", func() {
   161  					mode = run.FullRun
   162  					invalidDeps.SingleFullDeps = []*changelist.Dep{deps[102], deps[101]}
   163  					s := mustFormat()
   164  					So(s, ShouldContainSubstring, text.Doc(`
   165  				in "FULL_RUN" mode because it has not yet submitted dependencies:
   166  				  * https://x-review.googlesource.com/c/101
   167  				  * https://x-review.googlesource.com/c/102
   168  			`))
   169  				})
   170  				Convey("Combinable not triggered deps", func() {
   171  					invalidDeps.CombinableUntriggered = []*changelist.Dep{deps[102], deps[101]}
   172  					s := mustFormat()
   173  					So(s, ShouldContainSubstring, text.Doc(`
   174  				its dependencies weren't CQ-ed at all:
   175  				  * https://x-review.googlesource.com/c/101
   176  				  * https://x-review.googlesource.com/c/102
   177  			`))
   178  				})
   179  				Convey("Combinable mode mismatch", func() {
   180  					mode = run.FullRun
   181  					invalidDeps.CombinableMismatchedMode = []*changelist.Dep{deps[102], deps[101]}
   182  					s := mustFormat()
   183  					So(s, ShouldContainSubstring, text.Doc(`
   184  				its mode "FULL_RUN" does not match mode on its dependencies:
   185  				  * https://x-review.googlesource.com/c/101
   186  				  * https://x-review.googlesource.com/c/102
   187  			`))
   188  				})
   189  				Convey("Too many", func() {
   190  					invalidDeps.TooMany = &changelist.CLError_InvalidDeps_TooMany{Actual: 5, MaxAllowed: 4}
   191  					s := mustFormat()
   192  					So(s, ShouldContainSubstring, "has too many deps: 5 (max supported: 4)")
   193  				})
   194  			})
   195  			Convey("Reuse of triggers", func() {
   196  				reason.Kind = &changelist.CLError_ReusedTrigger_{
   197  					ReusedTrigger: &changelist.CLError_ReusedTrigger{Run: "some/123-1-run"},
   198  				}
   199  				So(mustFormat(), ShouldContainSubstring, `previously completed a Run ("some/123-1-run") triggered by the same vote(s)`)
   200  			})
   201  			Convey("TrighgerDeps", func() {
   202  				tdeps := &changelist.CLError_TriggerDeps{
   203  					PermissionDenied: []*changelist.CLError_TriggerDeps_PermissionDenied{
   204  						{Clid: 1, Email: "voter@example.org"},
   205  						{Clid: 2},
   206  					},
   207  					NotFound:            []int64{3, 4, 5},
   208  					InternalGerritError: []int64{6},
   209  				}
   210  				// Save a CL snapshot for each dep.
   211  				deps := make(map[int]*changelist.Dep, 6)
   212  				for i := 1; i <= 6; i++ {
   213  					depCL := changelist.MustGobID(gHost, int64(i)).MustCreateIfNotExists(ctx)
   214  					depCL.Snapshot = &changelist.Snapshot{
   215  						LuciProject:           "whatever",
   216  						MinEquivalentPatchset: 1,
   217  						Patchset:              2,
   218  						ExternalUpdateTime:    timestamppb.New(testclock.TestRecentTimeUTC),
   219  						Kind: &changelist.Snapshot_Gerrit{
   220  							Gerrit: &changelist.Gerrit{
   221  								Host: gHost,
   222  								Info: gf.CI(i),
   223  							},
   224  						},
   225  					}
   226  					So(datastore.Put(ctx, depCL), ShouldBeNil)
   227  					deps[i] = &changelist.Dep{Clid: int64(depCL.ID)}
   228  				}
   229  				reason.Kind = &changelist.CLError_TriggerDeps_{TriggerDeps: tdeps}
   230  				So(mustFormat(), ShouldContainSubstring, text.Doc(`
   231  					failed to vote the CQ label on the following dependencies.
   232  					  * https://x-review.googlesource.com/c/1 - no permission to vote on behalf of voter@example.org
   233  					  * https://x-review.googlesource.com/c/2 - no permission to vote
   234  					  * https://x-review.googlesource.com/c/3 - the CL no longer exists in Gerrit
   235  					  * https://x-review.googlesource.com/c/4 - the CL no longer exists in Gerrit
   236  					  * https://x-review.googlesource.com/c/5 - the CL no longer exists in Gerrit
   237  					  * https://x-review.googlesource.com/c/6 - internal Gerrit error
   238  				`))
   239  			})
   240  
   241  			Convey("DepRunFailed", func() {
   242  				// Save a CL snapshot for each dep.
   243  				deps := make(map[int]*changelist.Dep, 2)
   244  				for i := 1; i <= 6; i++ {
   245  					depCL := changelist.MustGobID(gHost, int64(i)).MustCreateIfNotExists(ctx)
   246  					depCL.Snapshot = &changelist.Snapshot{
   247  						LuciProject:           "whatever",
   248  						MinEquivalentPatchset: 1,
   249  						Patchset:              2,
   250  						ExternalUpdateTime:    timestamppb.New(testclock.TestRecentTimeUTC),
   251  						Kind: &changelist.Snapshot_Gerrit{
   252  							Gerrit: &changelist.Gerrit{
   253  								Host: gHost,
   254  								Info: gf.CI(i),
   255  							},
   256  						},
   257  					}
   258  					So(datastore.Put(ctx, depCL), ShouldBeNil)
   259  					deps[i] = &changelist.Dep{Clid: int64(depCL.ID)}
   260  				}
   261  				reason.Kind = &changelist.CLError_DepRunFailed{
   262  					DepRunFailed: 2,
   263  				}
   264  				So(mustFormat(), ShouldContainSubstring, text.Doc(`
   265  					a Run failed on [CL](https://x-review.googlesource.com/c/2) that this CL depends on.
   266  				`))
   267  			})
   268  		})
   269  	})
   270  }