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 }