blob: f9d346a691e02cb67343b51e4b18acfa41da1190 [file] [log] [blame]
// Copyright 2018 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"testing"
"time"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/appengine/gaetesting"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/gologger"
"go.chromium.org/luci/config"
"go.chromium.org/luci/config/cfgclient"
"go.chromium.org/luci/config/impl/memory"
notifypb "go.chromium.org/luci/luci_notify/api/config"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestConfigIngestion(t *testing.T) {
t.Parallel()
Convey(`updateProjects`, t, func() {
c := gaetesting.TestingContextWithAppID("luci-notify")
c = gologger.StdConfig.Use(c)
c = logging.SetLevel(c, logging.Debug)
cfg := map[config.Set]memory.Files{
"projects/chromium": {
"luci-notify.cfg": `
notifiers {
name: "chromium-notifier"
notifications {
on_occurrence: SUCCESS
email {
recipients: "johndoe@chromium.org"
recipients: "janedoe@chromium.org"
}
}
tree_closers {
tree_status_host: "chromium-status.appspot.com"
failed_step_regexp: "test"
failed_step_regexp_exclude: "experimental_test"
}
builders {
bucket: "ci"
name: "linux"
repository: "https://chromium.googlesource.com/chromium/src"
}
}`,
"luci-notify/email-templates/a.template": "a\n\nchromium",
"luci-notify/email-templates/b.template": "b\n\nchromium",
},
"projects/v8": {
"luci-notify.cfg": `
tree_closing_enabled: true
notifiers {
name: "v8-notifier"
notifications {
on_new_status: SUCCESS
on_new_status: FAILURE
on_new_status: INFRA_FAILURE
email {
recipients: "johndoe@v8.org"
recipients: "janedoe@v8.org"
}
}
builders {
bucket: "ci"
name: "win"
}
}
`,
"luci-notify/email-templates/a.template": "a\n\nv8",
"luci-notify/email-templates/b.template": "b\n\nv8",
},
}
c = cfgclient.Use(c, memory.New(cfg))
err := updateProjects(c)
So(err, ShouldBeNil)
datastore.GetTestable(c).CatchupIndexes()
var projects []*Project
So(datastore.GetAll(c, datastore.NewQuery("Project"), &projects), ShouldBeNil)
So(len(projects), ShouldEqual, 2)
So(projects[0].Name, ShouldEqual, "chromium")
So(projects[0].TreeClosingEnabled, ShouldBeFalse)
So(projects[1].Name, ShouldEqual, "v8")
So(projects[1].TreeClosingEnabled, ShouldBeTrue)
var builders []*Builder
So(datastore.GetAll(c, datastore.NewQuery("Builder"), &builders), ShouldBeNil)
// Can't test 'builders' using ShouldResembleProto, as the base object
// isn't a proto, it just contains one. Can't test it using
// ShouldResemble either, as it has a bug where it doesn't terminate
// for protos. So we have to manually pull out the elements of the
// array and test the different parts individually.
So(builders, ShouldHaveLength, 2)
b := builders[0]
So(b.ProjectKey, ShouldResemble, datastore.MakeKey(c, "Project", "chromium"))
So(b.ID, ShouldEqual, "ci/linux")
So(b.Repository, ShouldEqual, "https://chromium.googlesource.com/chromium/src")
So(&b.Notifications, ShouldResembleProto, &notifypb.Notifications{
Notifications: []*notifypb.Notification{
{
OnOccurrence: []buildbucketpb.Status{
buildbucketpb.Status_SUCCESS,
},
Email: &notifypb.Notification_Email{
Recipients: []string{"johndoe@chromium.org", "janedoe@chromium.org"},
},
},
},
})
b = builders[1]
So(b.ProjectKey, ShouldResemble, datastore.MakeKey(c, "Project", "v8"))
So(b.ID, ShouldEqual, "ci/win")
So(&b.Notifications, ShouldResembleProto, &notifypb.Notifications{
Notifications: []*notifypb.Notification{
{
OnNewStatus: []buildbucketpb.Status{
buildbucketpb.Status_SUCCESS,
buildbucketpb.Status_FAILURE,
buildbucketpb.Status_INFRA_FAILURE,
},
Email: &notifypb.Notification_Email{
Recipients: []string{"johndoe@v8.org", "janedoe@v8.org"},
},
},
},
})
var emailTemplates []*EmailTemplate
So(datastore.GetAll(c, datastore.NewQuery("EmailTemplate"), &emailTemplates), ShouldBeNil)
So(emailTemplates, ShouldResemble, []*EmailTemplate{
{
ProjectKey: datastore.MakeKey(c, "Project", "chromium"),
Name: "a",
SubjectTextTemplate: "a",
BodyHTMLTemplate: "chromium",
DefinitionURL: "https://example.com/view/here/luci-notify/email-templates/a.template",
},
{
ProjectKey: datastore.MakeKey(c, "Project", "chromium"),
Name: "b",
SubjectTextTemplate: "b",
BodyHTMLTemplate: "chromium",
DefinitionURL: "https://example.com/view/here/luci-notify/email-templates/b.template",
},
{
ProjectKey: datastore.MakeKey(c, "Project", "v8"),
Name: "a",
SubjectTextTemplate: "a",
BodyHTMLTemplate: "v8",
DefinitionURL: "https://example.com/view/here/luci-notify/email-templates/a.template",
},
{
ProjectKey: datastore.MakeKey(c, "Project", "v8"),
Name: "b",
SubjectTextTemplate: "b",
BodyHTMLTemplate: "v8",
DefinitionURL: "https://example.com/view/here/luci-notify/email-templates/b.template",
},
})
var treeClosers []*TreeCloser
So(datastore.GetAll(c, datastore.NewQuery("TreeCloser"), &treeClosers), ShouldBeNil)
// As above, can't use ShouldResemble or ShouldResembleProto directly.
So(treeClosers, ShouldHaveLength, 1)
t := treeClosers[0]
So(t.BuilderKey, ShouldResemble, datastore.MakeKey(c, "Project", "chromium", "Builder", "ci/linux"))
So(t.TreeStatusHost, ShouldEqual, "chromium-status.appspot.com")
So(t.Status, ShouldEqual, Open)
So(&t.TreeCloser, ShouldResembleProto, &notifypb.TreeCloser{
TreeStatusHost: "chromium-status.appspot.com",
FailedStepRegexp: "test",
FailedStepRegexpExclude: "experimental_test",
})
// Regression test for a bug where we would incorrectly delete entities
// that are still live in the config.
Convey("Entities remain after no-op update", func() {
// Add a space - this won't change the contents of the config, but
// it will update the hash, hence forcing a reingestion of the same
// config, which should be a no-op.
cfg["projects/chromium"]["luci-notify.cfg"] += " "
cfg["projects/v8"]["luci-notify.cfg"] += " "
c := cfgclient.Use(c, memory.New(cfg))
err := updateProjects(c)
So(err, ShouldBeNil)
datastore.GetTestable(c).CatchupIndexes()
var builders []*Builder
So(datastore.GetAll(c, datastore.NewQuery("Builder"), &builders), ShouldBeNil)
So(builders, ShouldHaveLength, 2)
var emailTemplates []*EmailTemplate
So(datastore.GetAll(c, datastore.NewQuery("EmailTemplate"), &emailTemplates), ShouldBeNil)
So(emailTemplates, ShouldHaveLength, 4)
var treeClosers []*TreeCloser
So(datastore.GetAll(c, datastore.NewQuery("TreeCloser"), &treeClosers), ShouldBeNil)
So(treeClosers, ShouldHaveLength, 1)
})
Convey("preserve updated fields", func() {
// Update the Chromium builder in the datastore, simulating that some request was handled.
chromiumBuilder := builders[0]
chromiumBuilder.Status = buildbucketpb.Status_FAILURE
chromiumBuilder.Revision = "abc123"
chromiumBuilder.GitilesCommits = notifypb.GitilesCommits{
Commits: []*buildbucketpb.GitilesCommit{
{
Host: "chromium.googlesource.com",
Project: "chromium/src",
Id: "deadbeefdeadbeefdeadbeef",
},
},
}
So(datastore.Put(c, chromiumBuilder), ShouldBeNil)
// Similar with the TreeCloser
treeCloser := treeClosers[0]
treeCloser.Status = Closed
treeCloser.Timestamp = time.Now().UTC()
So(datastore.Put(c, treeCloser), ShouldBeNil)
datastore.GetTestable(c).CatchupIndexes()
So(updateProjects(c), ShouldBeNil)
chromium := &Project{Name: "chromium"}
chromiumKey := datastore.KeyForObj(c, chromium)
var newBuilders []*Builder
So(datastore.GetAll(c, datastore.NewQuery("Builder").Ancestor(chromiumKey), &newBuilders), ShouldBeNil)
So(newBuilders, ShouldHaveLength, 1)
// Check the fields we care about explicitly, because generated proto structs may have
// size caches which are updated.
So(newBuilders[0].Status, ShouldEqual, chromiumBuilder.Status)
So(newBuilders[0].Revision, ShouldResemble, chromiumBuilder.Revision)
So(&newBuilders[0].GitilesCommits, ShouldResembleProto, &chromiumBuilder.GitilesCommits)
var newTreeClosers []*TreeCloser
So(datastore.GetAll(c, datastore.NewQuery("TreeCloser").Ancestor(chromiumKey), &newTreeClosers), ShouldBeNil)
So(newTreeClosers, ShouldHaveLength, 1)
So(newTreeClosers[0].Status, ShouldEqual, treeCloser.Status)
// The returned Timestamp field is rounded to the microsecond, as datastore only stores times to µs precision.
µs, _ := time.ParseDuration("1µs")
So(newTreeClosers[0].Timestamp, ShouldEqual, treeCloser.Timestamp.Round(µs))
})
Convey("delete project", func() {
delete(cfg, "projects/v8")
So(updateProjects(c), ShouldBeNil)
datastore.GetTestable(c).CatchupIndexes()
v8 := &Project{Name: "v8"}
So(datastore.Get(c, v8), ShouldEqual, datastore.ErrNoSuchEntity)
v8Key := datastore.KeyForObj(c, v8)
var builders []*Builder
So(datastore.GetAll(c, datastore.NewQuery("Builder").Ancestor(v8Key), &builders), ShouldBeNil)
So(builders, ShouldBeEmpty)
var emailTemplates []*EmailTemplate
So(datastore.GetAll(c, datastore.NewQuery("EmailTemplate").Ancestor(v8Key), &emailTemplates), ShouldBeNil)
So(emailTemplates, ShouldBeEmpty)
})
Convey("rename email template", func() {
oldName := "luci-notify/email-templates/a.template"
newName := "luci-notify/email-templates/c.template"
chromiumCfg := cfg["projects/chromium"]
chromiumCfg[newName] = chromiumCfg[oldName]
delete(chromiumCfg, oldName)
So(updateProjects(c), ShouldBeNil)
datastore.GetTestable(c).CatchupIndexes()
var emailTemplates []*EmailTemplate
q := datastore.NewQuery("EmailTemplate").Ancestor(datastore.MakeKey(c, "Project", "chromium"))
So(datastore.GetAll(c, q, &emailTemplates), ShouldBeNil)
So(len(emailTemplates), ShouldEqual, 2)
So(emailTemplates[0].Name, ShouldEqual, "b")
So(emailTemplates[1].Name, ShouldEqual, "c")
})
})
}