go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/configs/prjcfg/refresher/cron_test.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 refresher
    16  
    17  import (
    18  	"context"
    19  	"math/rand"
    20  	"testing"
    21  	"time"
    22  
    23  	"go.chromium.org/luci/common/data/rand/mathrand"
    24  	"go.chromium.org/luci/config"
    25  	"go.chromium.org/luci/config/cfgclient"
    26  	cfgmemory "go.chromium.org/luci/config/impl/memory"
    27  	"go.chromium.org/luci/gae/service/datastore"
    28  	"go.chromium.org/luci/server/quota/quotapb"
    29  	"go.chromium.org/luci/server/tq/tqtesting"
    30  	"google.golang.org/protobuf/reflect/protoreflect"
    31  
    32  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    33  	"go.chromium.org/luci/cv/internal/cvtesting"
    34  
    35  	. "github.com/smartystreets/goconvey/convey"
    36  	. "go.chromium.org/luci/common/testing/assertions"
    37  )
    38  
    39  func TestConfigRefreshCron(t *testing.T) {
    40  	t.Parallel()
    41  
    42  	Convey("Config refresh cron works", t, func() {
    43  		ct := cvtesting.Test{}
    44  		ctx, cancel := ct.SetUp(t)
    45  		defer cancel()
    46  
    47  		pm := mockPM{}
    48  		qm := mockQM{}
    49  		pcr := NewRefresher(ct.TQDispatcher, &pm, &qm, ct.Env)
    50  
    51  		Convey("for a new project", func() {
    52  			ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{
    53  				config.MustProjectSet("chromium"): {ConfigFileName: ""},
    54  			}))
    55  			// Project chromium doesn't exist in datastore.
    56  			err := pcr.SubmitRefreshTasks(ctx)
    57  			So(err, ShouldBeNil)
    58  			So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{
    59  				&RefreshProjectConfigTask{Project: "chromium"},
    60  			})
    61  			ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config"))
    62  			So(pm.updates, ShouldResemble, []string{"chromium"})
    63  			So(qm.writes, ShouldResemble, []string{"chromium"})
    64  		})
    65  
    66  		Convey("for an existing project", func() {
    67  			ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{
    68  				config.MustProjectSet("chromium"): {ConfigFileName: ""},
    69  			}))
    70  			So(datastore.Put(ctx, &prjcfg.ProjectConfig{
    71  				Project: "chromium",
    72  				Enabled: true,
    73  			}), ShouldBeNil)
    74  			So(pcr.SubmitRefreshTasks(ctx), ShouldBeNil)
    75  			So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{
    76  				&RefreshProjectConfigTask{Project: "chromium"},
    77  			})
    78  			ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config"))
    79  			So(pm.updates, ShouldResemble, []string{"chromium"})
    80  			So(qm.writes, ShouldResemble, []string{"chromium"})
    81  			pm.updates = nil
    82  			qm.writes = nil
    83  
    84  			Convey("randomly pokes existing projects even if there are no updates", func() {
    85  				// Simulate cron runs every 1 minute and expect PM to be poked at least
    86  				// once per pokePMInterval.
    87  				ctx = mathrand.Set(ctx, rand.New(rand.NewSource(1234)))
    88  				pokeBefore := ct.Clock.Now().Add(pokePMInterval)
    89  				for ct.Clock.Now().Before(pokeBefore) {
    90  					ct.Clock.Add(time.Minute)
    91  					So(pcr.SubmitRefreshTasks(ctx), ShouldBeNil)
    92  					ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config"))
    93  					So(pm.updates, ShouldBeEmpty)
    94  					So(qm.writes, ShouldResemble, []string{"chromium"})
    95  					if len(pm.pokes) > 0 {
    96  						break
    97  					}
    98  				}
    99  				So(pm.pokes, ShouldResemble, []string{"chromium"})
   100  			})
   101  		})
   102  
   103  		Convey("Disable project", func() {
   104  			Convey("that doesn't have CV config", func() {
   105  				ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{
   106  					config.MustProjectSet("chromium"): {"other.cfg": ""},
   107  				}))
   108  				So(datastore.Put(ctx, &prjcfg.ProjectConfig{
   109  					Project: "chromium",
   110  					Enabled: true,
   111  				}), ShouldBeNil)
   112  				err := pcr.SubmitRefreshTasks(ctx)
   113  				So(err, ShouldBeNil)
   114  				So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{
   115  					&RefreshProjectConfigTask{Project: "chromium", Disable: true},
   116  				})
   117  				ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config"))
   118  				So(pm.updates, ShouldResemble, []string{"chromium"})
   119  				So(qm.writes, ShouldResemble, []string{"chromium"})
   120  			})
   121  			Convey("that doesn't exist in LUCI Config", func() {
   122  				ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{}))
   123  				So(datastore.Put(ctx, &prjcfg.ProjectConfig{
   124  					Project: "chromium",
   125  					Enabled: true,
   126  				}), ShouldBeNil)
   127  				err := pcr.SubmitRefreshTasks(ctx)
   128  				So(err, ShouldBeNil)
   129  				So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{
   130  					&RefreshProjectConfigTask{Project: "chromium", Disable: true},
   131  				})
   132  				ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config"))
   133  				So(pm.updates, ShouldResemble, []string{"chromium"})
   134  				So(qm.writes, ShouldResemble, []string{"chromium"})
   135  			})
   136  			Convey("Skip already disabled Project", func() {
   137  				ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{}))
   138  				So(datastore.Put(ctx, &prjcfg.ProjectConfig{
   139  					Project: "foo",
   140  					Enabled: false,
   141  				}), ShouldBeNil)
   142  				err := pcr.SubmitRefreshTasks(ctx)
   143  				So(err, ShouldBeNil)
   144  				So(ct.TQ.Tasks(), ShouldBeEmpty)
   145  			})
   146  		})
   147  	})
   148  }
   149  
   150  type mockPM struct {
   151  	pokes   []string
   152  	updates []string
   153  }
   154  
   155  func (m *mockPM) Poke(ctx context.Context, luciProject string) error {
   156  	m.pokes = append(m.pokes, luciProject)
   157  	return nil
   158  }
   159  
   160  func (m *mockPM) UpdateConfig(ctx context.Context, luciProject string) error {
   161  	m.updates = append(m.updates, luciProject)
   162  	return nil
   163  }
   164  
   165  type mockQM struct {
   166  	writes []string
   167  }
   168  
   169  func (qm *mockQM) WritePolicy(ctx context.Context, project string) (*quotapb.PolicyConfigID, error) {
   170  	qm.writes = append(qm.writes, project)
   171  	return nil, nil
   172  }