blob: 48d4758ad48ca1e082faebe461f054ca9e0e6f57 [file] [log] [blame]
package build
import (
"bytes"
"context"
"io/ioutil"
"path/filepath"
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
"golang.org/x/time/rate"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
bbpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/memlogger"
"go.chromium.org/luci/common/system/environ"
. "go.chromium.org/luci/common/testing/assertions"
"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
"go.chromium.org/luci/luciexe/build/internal/testpb"
)
func init() {
// ensure that send NEVER blocks while testing Main functionality
mainSendRate = rate.Inf
}
func TestMain(t *testing.T) {
// avoid t.Parallel() because this registers property handlers.
Convey(`Main`, t, func() {
ctx := memlogger.Use(context.Background())
logs := logging.Get(ctx).(*memlogger.MemLogger)
ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
nowpb := timestamppb.New(testclock.TestRecentTimeUTC)
scFake := streamclient.NewFake()
defer scFake.Unregister()
env := environ.Env{}
env.Set(bootstrap.EnvStreamServerPath, scFake.StreamServerPath())
env.Set(bootstrap.EnvNamespace, "u")
ctx = env.SetInCtx(ctx)
imsg := &testpb.TopLevel{}
var setOut func(*testpb.TopLevel)
tdir := t.TempDir()
finalBuildPath := filepath.Join(tdir, "finalBuild.json")
args := []string{"myprogram", "--output", finalBuildPath}
stdin := &bytes.Buffer{}
mkStruct := func(dictlike map[string]interface{}) *structpb.Struct {
s, err := structpb.NewStruct(dictlike)
So(err, ShouldBeNil)
return s
}
writeStdinProps := func(dictlike map[string]interface{}) {
b := &bbpb.Build{
Input: &bbpb.Build_Input{
Properties: mkStruct(dictlike),
},
}
data, err := proto.Marshal(b)
So(err, ShouldBeNil)
_, err = stdin.Write(data)
So(err, ShouldBeNil)
}
getFinal := func() *bbpb.Build {
data, err := ioutil.ReadFile(finalBuildPath)
So(err, ShouldBeNil)
ret := &bbpb.Build{}
So(protojson.Unmarshal(data, ret), ShouldBeNil)
// proto module is cute and tries to introduce non-deterministic
// characters into their error messages. This is annoying and unhelpful
// for tests where error messages intentionally can show up in the Build
// output. We manually normalize them here. Replaces non-breaking space
// (U+00a0) with space (U+0020)
ret.SummaryMarkdown = strings.ReplaceAll(ret.SummaryMarkdown, " ", " ")
return ret
}
Convey(`good`, func() {
Convey(`simple`, func() {
err := main(ctx, args, stdin, imsg, nil, nil, func(ctx context.Context, args []string, st *State) error {
So(args, ShouldBeNil)
return nil
})
So(err, ShouldBeNil)
So(getFinal(), ShouldResembleProto, &bbpb.Build{
StartTime: nowpb,
EndTime: nowpb,
Status: bbpb.Status_SUCCESS,
Output: &bbpb.Build_Output{},
Input: &bbpb.Build_Input{},
})
})
Convey(`user args`, func() {
args = append(args, "--", "custom", "stuff")
err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
So(args, ShouldResemble, []string{"custom", "stuff"})
return nil
})
So(err, ShouldBeNil)
So(getFinal(), ShouldResembleProto, &bbpb.Build{
StartTime: nowpb,
EndTime: nowpb,
Status: bbpb.Status_SUCCESS,
Output: &bbpb.Build_Output{},
Input: &bbpb.Build_Input{},
})
})
Convey(`inputProps`, func() {
writeStdinProps(map[string]interface{}{
"field": "something",
"$cool": "blah",
})
err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
So(imsg, ShouldResembleProto, &testpb.TopLevel{
Field: "something",
JsonNameField: "blah",
})
return nil
})
So(err, ShouldBeNil)
})
Convey(`help`, func() {
args = append(args, "--help")
err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
return nil
})
So(err, ShouldBeNil)
So(logs, memlogger.ShouldHaveLog, logging.Info, "`myprogram` is a `luciexe` binary. See go.chromium.org/luci/luciexe.")
So(logs, memlogger.ShouldHaveLog, logging.Info, "======= I/O Proto =======")
// TODO(iannucci): check I/O proto when implemented
})
})
Convey(`errors`, func() {
Convey(`returned`, func() {
err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
So(args, ShouldBeNil)
return errors.New("bad stuff")
})
So(err, ShouldEqual, errNonSuccess)
So(getFinal(), ShouldResembleProto, &bbpb.Build{
StartTime: nowpb,
EndTime: nowpb,
Status: bbpb.Status_FAILURE,
Output: &bbpb.Build_Output{},
Input: &bbpb.Build_Input{},
})
So(logs, memlogger.ShouldHaveLog, logging.Error, "set status: FAILURE: bad stuff")
})
Convey(`panic`, func() {
err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
So(args, ShouldBeNil)
panic("BAD THINGS")
})
So(err, ShouldEqual, errNonSuccess)
So(getFinal(), ShouldResembleProto, &bbpb.Build{
StartTime: nowpb,
EndTime: nowpb,
Status: bbpb.Status_INFRA_FAILURE,
Output: &bbpb.Build_Output{},
Input: &bbpb.Build_Input{},
})
So(logs, memlogger.ShouldHaveLog, logging.Error, "set status: INFRA_FAILURE: PANIC")
So(logs, memlogger.ShouldHaveLog, logging.Error, "recovered panic: BAD THINGS")
})
Convey(`inputProps`, func() {
writeStdinProps(map[string]interface{}{
"bogus": "something",
})
args = append(args, "--strict-input")
err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
return nil
})
So(err, ShouldErrLike, "parsing top-level properties")
So(getFinal(), ShouldResembleProto, &bbpb.Build{
StartTime: nowpb,
EndTime: nowpb,
Status: bbpb.Status_INFRA_FAILURE,
Output: &bbpb.Build_Output{},
SummaryMarkdown: "fatal error starting build: parsing top-level properties: proto: (line 1:2): unknown field \"bogus\"",
Input: &bbpb.Build_Input{
Properties: mkStruct(map[string]interface{}{
"bogus": "something",
}),
},
})
})
})
})
}