go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/configs/prjcfg/refresher/refresh_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  	"testing"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/encoding/prototext"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/types/known/durationpb"
    25  
    26  	"go.chromium.org/luci/common/clock/testclock"
    27  	"go.chromium.org/luci/common/logging"
    28  	"go.chromium.org/luci/common/logging/gologger"
    29  	"go.chromium.org/luci/config"
    30  	"go.chromium.org/luci/config/cfgclient"
    31  	cfgmemory "go.chromium.org/luci/config/impl/memory"
    32  	"go.chromium.org/luci/gae/filter/txndefer"
    33  	gaememory "go.chromium.org/luci/gae/impl/memory"
    34  	"go.chromium.org/luci/gae/service/datastore"
    35  	"go.chromium.org/luci/server/tq"
    36  	"go.chromium.org/luci/server/tq/tqtesting"
    37  
    38  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    39  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    40  	"go.chromium.org/luci/cv/internal/configs/srvcfg"
    41  	listenerpb "go.chromium.org/luci/cv/settings/listener"
    42  
    43  	. "github.com/smartystreets/goconvey/convey"
    44  	. "go.chromium.org/luci/common/testing/assertions"
    45  )
    46  
    47  var testNow = testclock.TestTimeLocal.Round(1 * time.Millisecond)
    48  var testCfg = &cfgpb.Config{
    49  	DrainingStartTime: "2014-05-11T14:37:57Z",
    50  	SubmitOptions: &cfgpb.SubmitOptions{
    51  		MaxBurst:   50,
    52  		BurstDelay: durationpb.New(2 * time.Second),
    53  	},
    54  	CqStatusHost: "chromium-cq-status.appspot.com",
    55  	ConfigGroups: []*cfgpb.ConfigGroup{
    56  		{
    57  			Name: "group_foo",
    58  			Gerrit: []*cfgpb.ConfigGroup_Gerrit{
    59  				{
    60  					Url: "https://chromium-review.googlesource.com/",
    61  					Projects: []*cfgpb.ConfigGroup_Gerrit_Project{
    62  						{
    63  							Name:      "chromium/src",
    64  							RefRegexp: []string{"refs/heads/main"},
    65  						},
    66  					},
    67  				},
    68  			},
    69  		},
    70  	},
    71  }
    72  
    73  func TestUpdateProject(t *testing.T) {
    74  	Convey("Update Project", t, func() {
    75  		ctx, testClock, _ := mkTestingCtx()
    76  		chromiumConfig := &cfgpb.Config{
    77  			DrainingStartTime: "2017-12-23T15:47:58Z",
    78  			CqStatusHost:      "chromium-cq-status.appspot.com",
    79  			SubmitOptions: &cfgpb.SubmitOptions{
    80  				MaxBurst:   100,
    81  				BurstDelay: durationpb.New(1 * time.Second),
    82  			},
    83  			ConfigGroups: []*cfgpb.ConfigGroup{
    84  				{
    85  					Name: "branch_m100",
    86  					Gerrit: []*cfgpb.ConfigGroup_Gerrit{
    87  						{
    88  							Url: "https://chromium-review.googlesource.com/",
    89  							Projects: []*cfgpb.ConfigGroup_Gerrit_Project{
    90  								{
    91  									Name:      "chromium/src",
    92  									RefRegexp: []string{"refs/heads/branch_m100"},
    93  								},
    94  							},
    95  						},
    96  					},
    97  				},
    98  				{
    99  					Name: "main",
   100  					Gerrit: []*cfgpb.ConfigGroup_Gerrit{
   101  						{
   102  							Url: "https://chromium-review.googlesource.com/",
   103  							Projects: []*cfgpb.ConfigGroup_Gerrit_Project{
   104  								{
   105  									Name:      "chromium/src",
   106  									RefRegexp: []string{"refs/heads/main"},
   107  								},
   108  							},
   109  						},
   110  					},
   111  				},
   112  			},
   113  		}
   114  		verifyEntitiesInDatastore := func(ctx context.Context, expectedEVersion int64) {
   115  			cfg, meta := &cfgpb.Config{}, &config.Meta{}
   116  			err := cfgclient.Get(ctx, config.MustProjectSet("chromium"), ConfigFileName, cfgclient.ProtoText(cfg), meta)
   117  			So(err, ShouldBeNil)
   118  			localHash := prjcfg.ComputeHash(cfg)
   119  			projKey := prjcfg.ProjectConfigKey(ctx, "chromium")
   120  			cgNames := make([]string, len(cfg.GetConfigGroups()))
   121  			// Verify ConfigGroups.
   122  			for i, cgpb := range cfg.GetConfigGroups() {
   123  				cgNames[i] = cgpb.GetName()
   124  				cg := prjcfg.ConfigGroup{
   125  					ID:      prjcfg.MakeConfigGroupID(localHash, cgNames[i]),
   126  					Project: projKey,
   127  				}
   128  				err := datastore.Get(ctx, &cg)
   129  				So(err, ShouldBeNil)
   130  				So(cg.DrainingStartTime, ShouldEqual, cfg.GetDrainingStartTime())
   131  				So(cg.SubmitOptions, ShouldResembleProto, cfg.GetSubmitOptions())
   132  				So(cg.Content, ShouldResembleProto, cfg.GetConfigGroups()[i])
   133  				So(cg.CQStatusHost, ShouldResemble, cfg.GetCqStatusHost())
   134  			}
   135  			// Verify ProjectConfig.
   136  			pc := prjcfg.ProjectConfig{Project: "chromium"}
   137  			err = datastore.Get(ctx, &pc)
   138  			So(err, ShouldBeNil)
   139  			So(pc, ShouldResemble, prjcfg.ProjectConfig{
   140  				Project:          "chromium",
   141  				SchemaVersion:    prjcfg.SchemaVersion,
   142  				Enabled:          true,
   143  				EVersion:         expectedEVersion,
   144  				Hash:             localHash,
   145  				ExternalHash:     meta.ContentHash,
   146  				UpdateTime:       datastore.RoundTime(testClock.Now()).UTC(),
   147  				ConfigGroupNames: cgNames,
   148  			})
   149  			// The revision in the memory-based config fake is a fake
   150  			// 40-character sha256 hash digest. The particular value is
   151  			// internally determined by the memory-based implementation
   152  			// and isn't important here, so just assert that something
   153  			// that looks like a hash digest is filled in.
   154  			hashInfo := prjcfg.ConfigHashInfo{Hash: localHash, Project: projKey}
   155  			err = datastore.Get(ctx, &hashInfo)
   156  			So(err, ShouldBeNil)
   157  			So(len(hashInfo.GitRevision), ShouldEqual, 40)
   158  			hashInfo.GitRevision = ""
   159  			// Verify the rest of ConfigHashInfo.
   160  			So(hashInfo, ShouldResemble, prjcfg.ConfigHashInfo{
   161  				Hash:             localHash,
   162  				Project:          projKey,
   163  				SchemaVersion:    prjcfg.SchemaVersion,
   164  				ProjectEVersion:  expectedEVersion,
   165  				UpdateTime:       datastore.RoundTime(testClock.Now()).UTC(),
   166  				ConfigGroupNames: cgNames,
   167  			})
   168  		}
   169  
   170  		notifyCalled := false
   171  		notify := func(context.Context) error {
   172  			notifyCalled = true
   173  			return nil
   174  		}
   175  
   176  		Convey("Creates new ProjectConfig", func() {
   177  			ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{
   178  				config.MustProjectSet("chromium"): {
   179  					ConfigFileName: toProtoText(chromiumConfig),
   180  				},
   181  			}))
   182  			err := UpdateProject(ctx, "chromium", notify)
   183  			So(err, ShouldBeNil)
   184  			verifyEntitiesInDatastore(ctx, 1)
   185  			So(notifyCalled, ShouldBeTrue)
   186  
   187  			notifyCalled = false
   188  			testClock.Add(10 * time.Minute)
   189  
   190  			Convey("Noop if config is up-to-date", func() {
   191  				err := UpdateProject(ctx, "chromium", notify)
   192  				So(err, ShouldBeNil)
   193  				pc := prjcfg.ProjectConfig{Project: "chromium"}
   194  				So(datastore.Get(ctx, &pc), ShouldBeNil)
   195  				So(pc.EVersion, ShouldEqual, 1)
   196  				prevUpdatedTime := testClock.Now().Add(-10 * time.Minute)
   197  				So(pc.UpdateTime, ShouldResemble, prevUpdatedTime.UTC())
   198  				So(notifyCalled, ShouldBeFalse)
   199  
   200  				Convey("But not noop if SchemaVersion changed", func() {
   201  					old := pc // copy
   202  					old.SchemaVersion--
   203  					So(datastore.Put(ctx, &old), ShouldBeNil)
   204  
   205  					err := UpdateProject(ctx, "chromium", notify)
   206  					So(err, ShouldBeNil)
   207  					So(notifyCalled, ShouldBeTrue)
   208  					So(datastore.Get(ctx, &pc), ShouldBeNil)
   209  					So(pc.EVersion, ShouldEqual, 2)
   210  					So(pc.SchemaVersion, ShouldEqual, prjcfg.SchemaVersion)
   211  				})
   212  			})
   213  
   214  			Convey("Update existing ProjectConfig", func() {
   215  				updatedConfig := proto.Clone(chromiumConfig).(*cfgpb.Config)
   216  				updatedConfig.ConfigGroups = append(updatedConfig.ConfigGroups, &cfgpb.ConfigGroup{
   217  					Name: "experimental",
   218  					Gerrit: []*cfgpb.ConfigGroup_Gerrit{
   219  						{
   220  							Url: "https://chromium-review.googlesource.com/",
   221  							Projects: []*cfgpb.ConfigGroup_Gerrit_Project{
   222  								{
   223  									Name:      "chromium/src/experimental",
   224  									RefRegexp: []string{"refs/heads/main"},
   225  								},
   226  							},
   227  						},
   228  					},
   229  				})
   230  				ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{
   231  					config.MustProjectSet("chromium"): {
   232  						ConfigFileName: toProtoText(updatedConfig),
   233  					},
   234  				}))
   235  				err := UpdateProject(ctx, "chromium", notify)
   236  				So(err, ShouldBeNil)
   237  				verifyEntitiesInDatastore(ctx, 2)
   238  				So(notifyCalled, ShouldBeTrue)
   239  
   240  				notifyCalled = false
   241  				testClock.Add(10 * time.Minute)
   242  
   243  				Convey("Roll back to previous version", func() {
   244  					ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{
   245  						config.MustProjectSet("chromium"): {
   246  							ConfigFileName: toProtoText(chromiumConfig),
   247  						},
   248  					}))
   249  
   250  					err := UpdateProject(ctx, "chromium", notify)
   251  					So(err, ShouldBeNil)
   252  					verifyEntitiesInDatastore(ctx, 3)
   253  					So(notifyCalled, ShouldBeTrue)
   254  				})
   255  
   256  				Convey("Re-enables project even if config hash is the same", func() {
   257  					testClock.Add(10 * time.Minute)
   258  					So(DisableProject(ctx, "chromium", notify), ShouldBeNil)
   259  					before := prjcfg.ProjectConfig{Project: "chromium"}
   260  					So(datastore.Get(ctx, &before), ShouldBeNil)
   261  					// Delete config entities.
   262  					projKey := prjcfg.ProjectConfigKey(ctx, "chromium")
   263  					err := datastore.Delete(ctx,
   264  						&prjcfg.ConfigHashInfo{Hash: before.Hash, Project: projKey},
   265  						&prjcfg.ConfigGroup{
   266  							ID:      prjcfg.MakeConfigGroupID(before.Hash, before.ConfigGroupNames[0]),
   267  							Project: projKey,
   268  						},
   269  					)
   270  					So(err, ShouldBeNil)
   271  
   272  					testClock.Add(10 * time.Minute)
   273  					So(UpdateProject(ctx, "chromium", notify), ShouldBeNil)
   274  					after := prjcfg.ProjectConfig{Project: "chromium"}
   275  					So(datastore.Get(ctx, &after), ShouldBeNil)
   276  
   277  					So(after.Enabled, ShouldBeTrue)
   278  					So(after.EVersion, ShouldEqual, before.EVersion+1)
   279  					So(after.Hash, ShouldResemble, before.Hash)
   280  					// Ensure deleted entities are re-created.
   281  					verifyEntitiesInDatastore(ctx, 4)
   282  					So(notifyCalled, ShouldBeTrue)
   283  				})
   284  			})
   285  		})
   286  	})
   287  }
   288  
   289  func TestDisableProject(t *testing.T) {
   290  	Convey("Disable", t, func() {
   291  		ctx, testClock, _ := mkTestingCtx()
   292  		writeProjectConfig := func(enabled bool) {
   293  			pc := prjcfg.ProjectConfig{
   294  				Project:          "chromium",
   295  				Enabled:          enabled,
   296  				EVersion:         100,
   297  				Hash:             "hash",
   298  				ExternalHash:     "externalHash",
   299  				UpdateTime:       datastore.RoundTime(testClock.Now()).UTC(),
   300  				ConfigGroupNames: []string{"default"},
   301  			}
   302  			So(datastore.Put(ctx, &pc), ShouldBeNil)
   303  			testClock.Add(10 * time.Minute)
   304  		}
   305  
   306  		notifyCalled := false
   307  		notify := func(context.Context) error {
   308  			notifyCalled = true
   309  			return nil
   310  		}
   311  
   312  		Convey("currently enabled Project", func() {
   313  			writeProjectConfig(true)
   314  			err := DisableProject(ctx, "chromium", notify)
   315  			So(err, ShouldBeNil)
   316  			actual := prjcfg.ProjectConfig{Project: "chromium"}
   317  			So(datastore.Get(ctx, &actual), ShouldBeNil)
   318  			So(actual.Enabled, ShouldBeFalse)
   319  			So(actual.EVersion, ShouldEqual, 101)
   320  			So(actual.UpdateTime, ShouldResemble, datastore.RoundTime(testClock.Now()).UTC())
   321  			So(notifyCalled, ShouldBeTrue)
   322  		})
   323  
   324  		Convey("currently disabled Project", func() {
   325  			writeProjectConfig(false)
   326  			err := DisableProject(ctx, "chromium", notify)
   327  			So(err, ShouldBeNil)
   328  			actual := prjcfg.ProjectConfig{Project: "chromium"}
   329  			So(datastore.Get(ctx, &actual), ShouldBeNil)
   330  			So(actual.Enabled, ShouldBeFalse)
   331  			So(actual.EVersion, ShouldEqual, 100)
   332  			So(notifyCalled, ShouldBeFalse)
   333  		})
   334  
   335  		Convey("non-existing Project", func() {
   336  			err := DisableProject(ctx, "non-existing", notify)
   337  			So(err, ShouldBeNil)
   338  			So(datastore.Get(ctx, &prjcfg.ProjectConfig{Project: "non-existing"}), ShouldErrLike, datastore.ErrNoSuchEntity)
   339  			So(notifyCalled, ShouldBeFalse)
   340  		})
   341  	})
   342  }
   343  
   344  func mkTestingCtx() (context.Context, testclock.TestClock, *tqtesting.Scheduler) {
   345  	ctx, clock := testclock.UseTime(context.Background(), testNow)
   346  	ctx = txndefer.FilterRDS(gaememory.Use(ctx))
   347  	datastore.GetTestable(ctx).AutoIndex(true)
   348  	datastore.GetTestable(ctx).Consistent(true)
   349  
   350  	ctx, scheduler := tq.TestingContext(ctx, nil)
   351  	if err := srvcfg.SetTestListenerConfig(ctx, &listenerpb.Settings{}, nil); err != nil {
   352  		panic(err)
   353  	}
   354  	return ctx, clock, scheduler
   355  }
   356  
   357  func toProtoText(msg proto.Message) string {
   358  	bs, err := prototext.Marshal(msg)
   359  	So(err, ShouldBeNil)
   360  	return string(bs)
   361  }
   362  
   363  func TestPutConfigGroups(t *testing.T) {
   364  	t.Parallel()
   365  
   366  	Convey("PutConfigGroups", t, func() {
   367  		ctx := gaememory.Use(context.Background())
   368  		if testing.Verbose() {
   369  			ctx = logging.SetLevel(gologger.StdConfig.Use(ctx), logging.Debug)
   370  		}
   371  
   372  		Convey("New Configs", func() {
   373  			hash := prjcfg.ComputeHash(testCfg)
   374  			err := putConfigGroups(ctx, testCfg, "chromium", hash)
   375  			So(err, ShouldBeNil)
   376  			stored := prjcfg.ConfigGroup{
   377  				ID:      prjcfg.MakeConfigGroupID(hash, "group_foo"),
   378  				Project: prjcfg.ProjectConfigKey(ctx, "chromium"),
   379  			}
   380  			So(datastore.Get(ctx, &stored), ShouldBeNil)
   381  			So(stored.DrainingStartTime, ShouldEqual, testCfg.GetDrainingStartTime())
   382  			So(stored.SubmitOptions, ShouldResembleProto, testCfg.GetSubmitOptions())
   383  			So(stored.Content, ShouldResembleProto, testCfg.GetConfigGroups()[0])
   384  			So(stored.SchemaVersion, ShouldEqual, prjcfg.SchemaVersion)
   385  
   386  			Convey("Skip if already exists", func() {
   387  				ctx := datastore.AddRawFilters(ctx, func(_ context.Context, rds datastore.RawInterface) datastore.RawInterface {
   388  					return readOnlyFilter{rds}
   389  				})
   390  				err := putConfigGroups(ctx, testCfg, "chromium", prjcfg.ComputeHash(testCfg))
   391  				So(err, ShouldBeNil)
   392  			})
   393  
   394  			Convey("Update existing due to SchemaVersion", func() {
   395  				old := stored // copy
   396  				old.SchemaVersion = prjcfg.SchemaVersion - 1
   397  				So(datastore.Put(ctx, &old), ShouldBeNil)
   398  
   399  				err := putConfigGroups(ctx, testCfg, "chromium", prjcfg.ComputeHash(testCfg))
   400  				So(err, ShouldBeNil)
   401  
   402  				So(datastore.Get(ctx, &stored), ShouldBeNil)
   403  				So(stored.SchemaVersion, ShouldEqual, prjcfg.SchemaVersion)
   404  			})
   405  		})
   406  	})
   407  }
   408  
   409  type readOnlyFilter struct{ datastore.RawInterface }
   410  
   411  func (f readOnlyFilter) PutMulti(keys []*datastore.Key, vals []datastore.PropertyMap, cb datastore.NewKeyCB) error {
   412  	panic("write is not supported")
   413  }