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() }