blob: f02b6f03703b7a00077e6070f3aa9f4dbbb288d2 [file] [log] [blame]
// Copyright 2016 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 buildbot
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/json"
"io"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/luci/gae/impl/memory"
ds "github.com/luci/gae/service/datastore"
"github.com/luci/luci-go/common/clock/testclock"
memcfg "github.com/luci/luci-go/common/config/impl/memory"
"github.com/luci/luci-go/common/logging/gologger"
"github.com/luci/luci-go/luci_config/server/cfgclient/backend/testconfig"
"github.com/luci/luci-go/milo/common"
"github.com/luci/luci-go/server/auth"
"github.com/luci/luci-go/server/auth/authtest"
"github.com/luci/luci-go/server/auth/identity"
"github.com/luci/luci-go/server/router"
"github.com/julienschmidt/httprouter"
"golang.org/x/net/context"
. "github.com/luci/luci-go/common/testing/assertions"
. "github.com/smartystreets/goconvey/convey"
)
var (
fakeTime = time.Date(2001, time.February, 3, 4, 5, 6, 7, time.UTC)
letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)
func RandStringRunes(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
func buildbotTimesFinished(start, end float64) []*float64 {
return []*float64{&start, &end}
}
func buildbotTimesPending(start float64) []*float64 {
return []*float64{&start, nil}
}
func newCombinedPsBody(bs []*buildbotBuild, m *buildbotMaster, internal bool) io.ReadCloser {
bmsg := buildMasterMsg{
Master: m,
Builds: bs,
}
bm, _ := json.Marshal(bmsg)
var b bytes.Buffer
zw := zlib.NewWriter(&b)
zw.Write(bm)
zw.Close()
sub := "projects/luci-milo/subscriptions/buildbot-public"
if internal {
sub = "projects/luci-milo/subscriptions/buildbot-private"
}
msg := common.PubSubSubscription{
Subscription: sub,
Message: common.PubSubMessage{
Data: base64.StdEncoding.EncodeToString(b.Bytes()),
},
}
jmsg, _ := json.Marshal(msg)
return ioutil.NopCloser(bytes.NewReader(jmsg))
}
func TestPubSub(t *testing.T) {
Convey(`A test Environment`, t, func() {
c := memory.UseWithAppID(context.Background(), "dev~luci-milo")
c = gologger.StdConfig.Use(c)
c, _ = testclock.UseTime(c, fakeTime)
c = testconfig.WithCommonClient(c, memcfg.New(bbAclConfigs))
c = auth.WithState(c, &authtest.FakeState{
Identity: identity.AnonymousIdentity,
IdentityGroups: []string{"all"},
})
// Update the service config so that the settings are loaded.
_, err := common.UpdateServiceConfig(c)
So(err, ShouldBeNil)
rand.Seed(5)
Convey("Remove source changes", func() {
m := &buildbotMaster{
Name: "fake",
Builders: map[string]*buildbotBuilder{
"fake builder": {
PendingBuildStates: []*buildbotPending{
{
Source: buildbotSourceStamp{
Changes: []buildbotChange{{Comments: "foo"}},
},
},
},
},
},
}
So(putDSMasterJSON(c, m, false), ShouldBeNil)
lm, _, _, err := getMasterJSON(c, "fake")
So(err, ShouldBeNil)
So(lm.Builders["fake builder"].PendingBuildStates[0].Source.Changes[0].Comments, ShouldResemble, "")
})
Convey("Save build entry", func() {
build := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1234,
Currentstep: "this is a string",
Finished: true,
}
err := ds.Put(c, build)
ds.GetTestable(c).CatchupIndexes()
So(err, ShouldBeNil)
Convey("Load build entry", func() {
loadB := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1234,
}
err = ds.Get(c, loadB)
So(err, ShouldBeNil)
So(loadB.Master, ShouldEqual, "Fake Master")
So(loadB.Internal, ShouldEqual, false)
So(loadB.Currentstep.(string), ShouldEqual, "this is a string")
So(loadB.Finished, ShouldEqual, true)
})
Convey("Query build entry", func() {
q := ds.NewQuery("buildbotBuild")
buildbots := []*buildbotBuild{}
err = ds.GetAll(c, q, &buildbots)
So(err, ShouldBeNil)
So(len(buildbots), ShouldEqual, 1)
So(buildbots[0].Currentstep.(string), ShouldEqual, "this is a string")
Convey("Query for finished entries should be 1", func() {
q = q.Eq("finished", true)
buildbots = []*buildbotBuild{}
err = ds.GetAll(c, q, &buildbots)
So(err, ShouldBeNil)
So(len(buildbots), ShouldEqual, 1)
})
Convey("Query for unfinished entries should be 0", func() {
q = q.Eq("finished", false)
buildbots = []*buildbotBuild{}
err = ds.GetAll(c, q, &buildbots)
So(err, ShouldBeNil)
So(len(buildbots), ShouldEqual, 0)
})
})
Convey("Save a few more entries", func() {
ds.GetTestable(c).Consistent(true)
ds.GetTestable(c).AutoIndex(true)
for i := 1235; i < 1240; i++ {
build := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: i,
Currentstep: "this is a string",
Finished: i%2 == 0,
}
err := ds.Put(c, build)
So(err, ShouldBeNil)
}
q := ds.NewQuery("buildbotBuild")
q = q.Eq("finished", true)
q = q.Eq("master", "Fake Master")
q = q.Eq("builder", "Fake buildername")
q = q.Order("-number")
buildbots := []*buildbotBuild{}
err = ds.GetAll(c, q, &buildbots)
So(err, ShouldBeNil)
So(len(buildbots), ShouldEqual, 3) // 1235, 1237, 1239
})
})
Convey("Build panics on invalid query", func() {
build := &buildbotBuild{
Master: "Fake Master",
}
So(func() { ds.Put(c, build) }, ShouldPanicLike, "No Master or Builder found")
})
ts := 555
b := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1234,
Currentstep: "this is a string",
Times: buildbotTimesPending(123.0),
TimeStamp: &ts,
}
Convey("Basic master + build pusbsub subscription", func() {
h := httptest.NewRecorder()
slaves := map[string]*buildbotSlave{}
ft := 1234.0
slaves["testslave"] = &buildbotSlave{
Name: "testslave",
Connected: true,
Runningbuilds: []*buildbotBuild{
{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 2222,
Times: []*float64{&ft, nil},
},
},
}
ms := buildbotMaster{
Name: "Fake Master",
Project: buildbotProject{Title: "some title"},
Slaves: slaves,
Builders: map[string]*buildbotBuilder{},
}
ms.Builders["Fake buildername"] = &buildbotBuilder{
CurrentBuilds: []int{1234},
}
r := &http.Request{
Body: newCombinedPsBody([]*buildbotBuild{b}, &ms, false),
}
p := httprouter.Params{}
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
Convey("And stores correctly", func() {
loadB := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1234,
}
err := ds.Get(c, loadB)
So(err, ShouldBeNil)
So(loadB.Master, ShouldEqual, "Fake Master")
So(loadB.Currentstep.(string), ShouldEqual, "this is a string")
m, _, t, err := getMasterJSON(c, "Fake Master")
So(err, ShouldBeNil)
So(t.Unix(), ShouldEqual, 981173106)
So(m.Name, ShouldEqual, "Fake Master")
So(m.Project.Title, ShouldEqual, "some title")
So(m.Slaves["testslave"].Name, ShouldEqual, "testslave")
So(len(m.Slaves["testslave"].Runningbuilds), ShouldEqual, 0)
So(len(m.Slaves["testslave"].RunningbuildsMap), ShouldEqual, 1)
So(m.Slaves["testslave"].RunningbuildsMap["Fake buildername"][0],
ShouldEqual, 2222)
})
Convey("And a new master overwrites", func() {
c, _ = testclock.UseTime(c, fakeTime.Add(time.Duration(1*time.Second)))
ms.Project.Title = "some other title"
h = httptest.NewRecorder()
r := &http.Request{
Body: newCombinedPsBody([]*buildbotBuild{b}, &ms, false)}
p = httprouter.Params{}
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
m, _, t, err := getMasterJSON(c, "Fake Master")
So(err, ShouldBeNil)
So(m.Project.Title, ShouldEqual, "some other title")
So(t.Unix(), ShouldEqual, 981173107)
So(m.Name, ShouldEqual, "Fake Master")
})
Convey("And a new build overwrites", func() {
b.Times = buildbotTimesFinished(123.0, 124.0)
h = httptest.NewRecorder()
r = &http.Request{
Body: newCombinedPsBody([]*buildbotBuild{b}, &ms, false),
}
p = httprouter.Params{}
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
loadB := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1234,
}
err := ds.Get(c, loadB)
So(err, ShouldBeNil)
So(*loadB.Times[0], ShouldEqual, 123.0)
So(*loadB.Times[1], ShouldEqual, 124.0)
Convey("And another pending build is rejected", func() {
b.Times = buildbotTimesPending(123.0)
h = httptest.NewRecorder()
r = &http.Request{
Body: newCombinedPsBody([]*buildbotBuild{b}, &ms, false),
}
p = httprouter.Params{}
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
loadB := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1234,
}
err := ds.Get(c, loadB)
So(err, ShouldBeNil)
So(*loadB.Times[0], ShouldEqual, 123.0)
So(*loadB.Times[1], ShouldEqual, 124.0)
})
})
Convey("Don't Expire non-existant current build under 20 min", func() {
b.Number = 1235
ts := int(fakeTime.Unix()) - 1000
b.TimeStamp = &ts
h = httptest.NewRecorder()
r = &http.Request{
Body: newCombinedPsBody([]*buildbotBuild{b}, &ms, false),
}
p = httprouter.Params{}
ds.GetTestable(c).Consistent(true)
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
loadB := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1235,
}
err := ds.Get(c, loadB)
So(err, ShouldBeNil)
So(loadB.Finished, ShouldEqual, false)
So(*loadB.Times[0], ShouldEqual, 123.0)
So(loadB.Times[1], ShouldBeNil)
})
Convey("Expire non-existant current build", func() {
b.Number = 1235
ts := int(fakeTime.Unix()) - 1201
b.TimeStamp = &ts
h = httptest.NewRecorder()
r = &http.Request{
Body: newCombinedPsBody([]*buildbotBuild{b}, &ms, false),
}
p = httprouter.Params{}
ds.GetTestable(c).Consistent(true)
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
loadB := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1235,
}
err := ds.Get(c, loadB)
So(err, ShouldBeNil)
So(loadB.Finished, ShouldEqual, true)
So(*loadB.Times[0], ShouldEqual, 123.0)
So(loadB.Times[1], ShouldNotEqual, nil)
So(*loadB.Times[1], ShouldEqual, ts)
So(*loadB.Results, ShouldEqual, 4)
})
Convey("Large pubsub message", func() {
// This has to be a random string, so that after gzip compresses it
// it remains larger than 950KB
b.Text = append(b.Text, RandStringRunes(1500000))
h := httptest.NewRecorder()
r := &http.Request{
Body: newCombinedPsBody([]*buildbotBuild{b}, &ms, false),
}
p := httprouter.Params{}
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
})
})
Convey("Empty pubsub message", func() {
h := httptest.NewRecorder()
r := &http.Request{Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}
p := httprouter.Params{}
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
})
Convey("Internal master + build pusbsub subscription", func() {
h := httptest.NewRecorder()
slaves := map[string]*buildbotSlave{}
ft := 1234.0
slaves["testslave"] = &buildbotSlave{
Name: "testslave",
Connected: true,
Runningbuilds: []*buildbotBuild{
{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 2222,
Times: []*float64{&ft, nil},
},
},
}
ms := buildbotMaster{
Name: "Fake Master",
Project: buildbotProject{Title: "some title"},
Slaves: slaves,
}
r := &http.Request{
Body: newCombinedPsBody([]*buildbotBuild{b}, &ms, true),
}
p := httprouter.Params{}
PubSubHandler(&router.Context{
Context: c,
Writer: h,
Request: r,
Params: p,
})
So(h.Code, ShouldEqual, 200)
Convey("And stores correctly", func() {
c = auth.WithState(c, &authtest.FakeState{
Identity: "user:alicebob@google.com",
IdentityGroups: []string{"googlers", "all"},
})
loadB := &buildbotBuild{
Master: "Fake Master",
Buildername: "Fake buildername",
Number: 1234,
}
err = ds.Get(c, loadB)
So(err, ShouldBeNil)
So(loadB.Master, ShouldEqual, "Fake Master")
So(loadB.Internal, ShouldEqual, true)
So(loadB.Currentstep.(string), ShouldEqual, "this is a string")
m, _, t, err := getMasterJSON(c, "Fake Master")
So(err, ShouldBeNil)
So(t.Unix(), ShouldEqual, 981173106)
So(m.Name, ShouldEqual, "Fake Master")
So(m.Project.Title, ShouldEqual, "some title")
So(m.Slaves["testslave"].Name, ShouldEqual, "testslave")
So(len(m.Slaves["testslave"].Runningbuilds), ShouldEqual, 0)
So(len(m.Slaves["testslave"].RunningbuildsMap), ShouldEqual, 1)
So(m.Slaves["testslave"].RunningbuildsMap["Fake buildername"][0],
ShouldEqual, 2222)
})
})
})
}