blob: 1dd2c7912df113dbcf63f38bd16623894af0ab88 [file] [log] [blame]
// Copyright 2019 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 exe
import (
"bytes"
"compress/zlib"
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
bbpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/system/environ"
"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
"go.chromium.org/luci/luciexe"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestExe(t *testing.T) {
t.Parallel()
Convey(`test exe`, t, func() {
scFake := streamclient.NewFake()
defer scFake.Unregister()
env := environ.Env{}
env.Set(bootstrap.EnvCoordinatorHost, "test.example.com")
env.Set(bootstrap.EnvStreamProject, "test_project")
env.Set(bootstrap.EnvStreamPrefix, "test_prefix")
env.Set(bootstrap.EnvNamespace, "test_namespace")
env.Set(bootstrap.EnvStreamServerPath, scFake.StreamServerPath())
ctx := env.SetInCtx(context.Background())
getBuilds := func(decompress bool) []*bbpb.Build {
fakeData := scFake.Data()["test_namespace/build.proto"]
if decompress {
So(fakeData.GetFlags().ContentType, ShouldEqual, luciexe.BuildProtoZlibContentType)
} else {
So(fakeData.GetFlags().ContentType, ShouldEqual, luciexe.BuildProtoContentType)
}
dgs := fakeData.GetDatagrams()
So(len(dgs), ShouldBeGreaterThanOrEqualTo, 1)
ret := make([]*bbpb.Build, len(dgs))
for i, dg := range dgs {
ret[i] = &bbpb.Build{}
var data []byte
if decompress {
r, err := zlib.NewReader(bytes.NewBufferString(dg))
So(err, ShouldBeNil)
data, err = ioutil.ReadAll(r)
So(err, ShouldBeNil)
} else {
data = []byte(dg)
}
So(proto.Unmarshal(data, ret[i]), ShouldBeNil)
}
return ret
}
lastBuild := func() *bbpb.Build {
builds := getBuilds(false)
return builds[len(builds)-1]
}
args := []string{"fake_test_executable"}
Convey(`basic`, func() {
Convey(`success`, func() {
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
build.Status = bbpb.Status_SCHEDULED
return nil
})
So(exitCode, ShouldEqual, 0)
So(lastBuild(), ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_SUCCESS,
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
})
Convey(`failure`, func() {
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
return errors.New("bad stuff")
})
So(exitCode, ShouldEqual, 1)
So(lastBuild(), ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_FAILURE,
SummaryMarkdown: "Final error: bad stuff",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
})
Convey(`infra failure`, func() {
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
return errors.New("bad stuff", InfraErrorTag)
})
So(exitCode, ShouldEqual, 1)
So(lastBuild(), ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_INFRA_FAILURE,
SummaryMarkdown: "Final infra error: bad stuff",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
})
Convey(`panic`, func() {
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
panic(errors.New("bad stuff"))
})
So(exitCode, ShouldEqual, 2)
So(lastBuild(), ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_INFRA_FAILURE,
SummaryMarkdown: "Final panic: bad stuff",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
})
Convey(`respect user program status`, func() {
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
build.Status = bbpb.Status_INFRA_FAILURE
build.SummaryMarkdown = "status set inside"
return nil
})
So(exitCode, ShouldEqual, 0)
So(lastBuild(), ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_INFRA_FAILURE,
SummaryMarkdown: "status set inside",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
})
})
Convey(`send`, func() {
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
build.SummaryMarkdown = "Hi. I did stuff."
bs()
return errors.New("oh no i failed")
})
So(exitCode, ShouldEqual, 1)
builds := getBuilds(false)
So(len(builds), ShouldEqual, 2)
So(builds[0], ShouldResembleProto, &bbpb.Build{
SummaryMarkdown: "Hi. I did stuff.",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
So(builds[len(builds)-1], ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_FAILURE,
SummaryMarkdown: "Hi. I did stuff.\n\nFinal error: oh no i failed",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
})
Convey(`send (zlib)`, func() {
exitCode := runCtx(ctx, args, []Option{WithZlibCompression(5)}, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
build.SummaryMarkdown = "Hi. I did stuff."
bs()
return errors.New("oh no i failed")
})
So(exitCode, ShouldEqual, 1)
builds := getBuilds(true)
So(len(builds), ShouldEqual, 2)
So(builds[0], ShouldResembleProto, &bbpb.Build{
SummaryMarkdown: "Hi. I did stuff.",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
So(builds[len(builds)-1], ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_FAILURE,
SummaryMarkdown: "Hi. I did stuff.\n\nFinal error: oh no i failed",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
})
Convey(`output`, func() {
tdir, err := ioutil.TempDir("", "luciexe-exe-test")
So(err, ShouldBeNil)
defer os.RemoveAll(tdir)
Convey(`binary`, func() {
outFile := filepath.Join(tdir, "out.pb")
args = append(args, luciexe.OutputCLIArg, outFile)
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
build.SummaryMarkdown = "Hi."
err := WriteProperties(build.Output.Properties, map[string]interface{}{
"some": "thing",
})
if err != nil {
panic(err)
}
return nil
})
So(exitCode, ShouldEqual, 0)
So(lastBuild(), ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_SUCCESS,
SummaryMarkdown: "Hi.",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{
Fields: map[string]*structpb.Value{
"some": {Kind: &structpb.Value_StringValue{
StringValue: "thing",
}},
},
},
},
})
data, err := ioutil.ReadFile(outFile)
So(err, ShouldBeNil)
So(string(data), ShouldResemble,
"`\f\x82\x01\x13\n\x11\n\x0f\n\x04some\x12\a\x1a\x05thing\xa2\x01\x03Hi.")
})
Convey(`textpb`, func() {
outFile := filepath.Join(tdir, "out.textpb")
args = append(args, luciexe.OutputCLIArg, outFile)
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
build.SummaryMarkdown = "Hi."
return nil
})
So(exitCode, ShouldEqual, 0)
So(lastBuild(), ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_SUCCESS,
SummaryMarkdown: "Hi.",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
data, err := ioutil.ReadFile(outFile)
So(err, ShouldBeNil)
So(string(data), ShouldResemble,
"status: SUCCESS\nsummary_markdown: \"Hi.\"\noutput: <\n properties: <\n >\n>\n")
})
Convey(`jsonpb`, func() {
outFile := filepath.Join(tdir, "out.json")
args = append(args, luciexe.OutputCLIArg, outFile)
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
build.SummaryMarkdown = "Hi."
return nil
})
So(exitCode, ShouldEqual, 0)
So(lastBuild(), ShouldResembleProto, &bbpb.Build{
Status: bbpb.Status_SUCCESS,
SummaryMarkdown: "Hi.",
Output: &bbpb.Build_Output{
Properties: &structpb.Struct{},
},
})
data, err := ioutil.ReadFile(outFile)
So(err, ShouldBeNil)
So(string(data), ShouldResemble,
"{\n \"status\": \"SUCCESS\",\n \"summary_markdown\": \"Hi.\",\n \"output\": {\n \"properties\": {\n }\n }\n}")
})
Convey(`pass through user args`, func() {
// Delimiter inside user args should also be passed through
expectedUserArgs := []string{"foo", "bar", ArgsDelim, "baz"}
Convey(`when output is not specified`, func() {
args = append(args, ArgsDelim)
args = append(args, expectedUserArgs...)
exitcode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
So(userArgs, ShouldResemble, expectedUserArgs)
return nil
})
So(exitcode, ShouldEqual, 0)
})
Convey(`when output is specified`, func() {
tdir, err := ioutil.TempDir("", "luciexe-exe-test")
So(err, ShouldBeNil)
defer os.RemoveAll(tdir)
args = append(args, luciexe.OutputCLIArg, filepath.Join(tdir, "out.pb"), ArgsDelim)
args = append(args, expectedUserArgs...)
exitcode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
So(userArgs, ShouldResemble, expectedUserArgs)
return nil
})
So(exitcode, ShouldEqual, 0)
})
})
Convey(`write output on error`, func() {
outFile := filepath.Join(tdir, "out.json")
args = append(args, luciexe.OutputCLIArg, outFile)
exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
build.SummaryMarkdown = "Hi."
return errors.New("bad stuff")
})
So(exitCode, ShouldEqual, 1)
data, err := ioutil.ReadFile(outFile)
So(err, ShouldBeNil)
So(string(data), ShouldResemble,
"{\n \"status\": \"FAILURE\",\n \"summary_markdown\": \"Hi.\\n\\nFinal error: bad stuff\",\n \"output\": {\n \"properties\": {\n }\n }\n}")
})
})
})
}