go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/pmtest/pm.go (about) 1 // Copyright 2020 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 pmtest 16 17 import ( 18 "context" 19 "sort" 20 "time" 21 22 "google.golang.org/protobuf/proto" 23 24 "go.chromium.org/luci/server/tq/tqtesting" 25 26 "go.chromium.org/luci/cv/internal/changelist" 27 "go.chromium.org/luci/cv/internal/common" 28 "go.chromium.org/luci/cv/internal/common/eventbox" 29 "go.chromium.org/luci/cv/internal/cvtesting" 30 "go.chromium.org/luci/cv/internal/prjmanager" 31 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 32 "go.chromium.org/luci/cv/internal/run" 33 34 . "github.com/smartystreets/goconvey/convey" 35 ) 36 37 // Projects returns list of projects from tasks for PM. 38 func Projects(in tqtesting.TaskList) (projects []string) { 39 for _, t := range in.SortByETA() { 40 switch v := t.Payload.(type) { 41 case *prjpb.ManageProjectTask: 42 projects = append(projects, v.GetLuciProject()) 43 case *prjpb.KickManageProjectTask: 44 projects = append(projects, v.GetLuciProject()) 45 } 46 } 47 return projects 48 } 49 50 func iterEventBox(ctx context.Context, project string, cb func(*prjpb.Event)) { 51 events, err := eventbox.List(ctx, prjmanager.EventboxRecipient(ctx, project)) 52 So(err, ShouldBeNil) 53 for _, item := range events { 54 evt := &prjpb.Event{} 55 So(proto.Unmarshal(item.Value, evt), ShouldBeNil) 56 cb(evt) 57 } 58 } 59 60 // ETAsOf returns sorted list of ETAs for a given project. 61 // 62 // Includes ETAs encoded in KickManageProjectTask tasks. 63 func ETAsOF(in tqtesting.TaskList, luciProject string) []time.Time { 64 var out []time.Time 65 for _, t := range in { 66 switch v := t.Payload.(type) { 67 case *prjpb.ManageProjectTask: 68 if v.GetLuciProject() == luciProject { 69 out = append(out, t.ETA) 70 } 71 case *prjpb.KickManageProjectTask: 72 if v.GetLuciProject() == luciProject { 73 out = append(out, v.GetEta().AsTime()) 74 } 75 } 76 } 77 sort.Slice(out, func(i, j int) bool { return out[i].Before(out[j]) }) 78 return out 79 } 80 81 // LatestETAof returns time.Time of the latest task for a given project. 82 // 83 // Includes ETAs encoded in KickManageProjectTask tasks. 84 // If none, returns Zero time.Time{}. 85 func LatestETAof(in tqtesting.TaskList, luciProject string) time.Time { 86 ts := ETAsOF(in, luciProject) 87 if len(ts) == 0 { 88 return time.Time{} 89 } 90 return ts[len(ts)-1] 91 } 92 93 // ETAsWithin returns sorted list of ETAs for a given project in t+-d range. 94 func ETAsWithin(in tqtesting.TaskList, luciProject string, d time.Duration, t time.Time) []time.Time { 95 out := ETAsOF(in, luciProject) 96 for len(out) > 0 && out[0].Before(t.Add(-d)) { 97 out = out[1:] 98 } 99 for len(out) > 0 && out[len(out)-1].After(t.Add(d)) { 100 out = out[:len(out)-1] 101 } 102 return out 103 } 104 105 func matchEventBox(ctx context.Context, project string, targets []*prjpb.Event) (matched, remaining []*prjpb.Event) { 106 remaining = make([]*prjpb.Event, len(targets)) 107 copy(remaining, targets) 108 iterEventBox(ctx, project, func(evt *prjpb.Event) { 109 for i, r := range remaining { 110 if proto.Equal(evt, r) { 111 matched = append(matched, r) 112 remaining[i] = remaining[len(remaining)-1] 113 remaining[len(remaining)-1] = nil 114 remaining = remaining[:len(remaining)-1] 115 return 116 } 117 } 118 }) 119 return 120 } 121 122 // AssertNotInEventbox asserts none of the events exists in the project 123 // Eventbox. 124 func AssertNotInEventbox(ctx context.Context, project string, targets ...*prjpb.Event) { 125 matched, _ := matchEventBox(ctx, project, targets) 126 So(matched, ShouldBeEmpty) 127 } 128 129 // AssertInEventbox asserts all events exist in the project Eventbox. 130 func AssertInEventbox(ctx context.Context, project string, targets ...*prjpb.Event) { 131 _, remaining := matchEventBox(ctx, project, targets) 132 So(remaining, ShouldBeEmpty) 133 } 134 135 // AssertReceivedRunFinished asserts a RunFinished event has been delivered 136 // tor project's eventbox for the given Run. 137 func AssertReceivedRunFinished(ctx context.Context, runID common.RunID, status run.Status) { 138 AssertInEventbox(ctx, runID.LUCIProject(), &prjpb.Event{ 139 Event: &prjpb.Event_RunFinished{ 140 RunFinished: &prjpb.RunFinished{ 141 RunId: string(runID), 142 Status: status, 143 }, 144 }, 145 }) 146 } 147 148 // AssertCLsUpdated asserts all events exist in the project Eventbox. 149 func AssertReceivedCLsNotified(ctx context.Context, project string, cls []*changelist.CL) { 150 AssertInEventbox(ctx, project, &prjpb.Event{ 151 Event: &prjpb.Event_ClsUpdated{ 152 ClsUpdated: changelist.ToUpdatedEvents(cls...), 153 }, 154 }) 155 } 156 157 // MockDispatch installs and returns MockDispatcher for PM. 158 func MockDispatch(ctx context.Context) (context.Context, MockDispatcher) { 159 m := MockDispatcher{&cvtesting.DispatchRecorder{}} 160 ctx = prjpb.InstallMockDispatcher(ctx, m.Dispatch) 161 return ctx, m 162 } 163 164 // MockDispatcher records in memory what would have resulted in task enqueues 165 // for a PM. 166 type MockDispatcher struct { 167 *cvtesting.DispatchRecorder 168 } 169 170 // Projects returns sorted list of Projects. 171 func (m *MockDispatcher) Projects() []string { return m.Targets() } 172 173 // PopProjects returns sorted list of Projects and resets the state. 174 func (m *MockDispatcher) PopProjects() []string { return m.PopTargets() }