go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/exe/exe_test.go (about)

     1  // Copyright 2019 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 exe
    16  
    17  import (
    18  	"bytes"
    19  	"compress/zlib"
    20  	"context"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"path/filepath"
    25  	"testing"
    26  
    27  	"google.golang.org/protobuf/proto"
    28  	"google.golang.org/protobuf/types/known/structpb"
    29  
    30  	bbpb "go.chromium.org/luci/buildbucket/proto"
    31  	"go.chromium.org/luci/common/errors"
    32  	"go.chromium.org/luci/common/system/environ"
    33  	"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
    34  	"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
    35  	"go.chromium.org/luci/luciexe"
    36  
    37  	. "github.com/smartystreets/goconvey/convey"
    38  
    39  	. "go.chromium.org/luci/common/testing/assertions"
    40  )
    41  
    42  func TestExe(t *testing.T) {
    43  	t.Parallel()
    44  
    45  	Convey(`test exe`, t, func() {
    46  		scFake := streamclient.NewFake()
    47  		defer scFake.Unregister()
    48  
    49  		env := environ.New(nil)
    50  		env.Set(bootstrap.EnvCoordinatorHost, "test.example.com")
    51  		env.Set(bootstrap.EnvStreamProject, "test_project")
    52  		env.Set(bootstrap.EnvStreamPrefix, "test_prefix")
    53  		env.Set(bootstrap.EnvNamespace, "test_namespace")
    54  		env.Set(bootstrap.EnvStreamServerPath, scFake.StreamServerPath())
    55  		ctx := env.SetInCtx(context.Background())
    56  
    57  		getBuilds := func(decompress bool) []*bbpb.Build {
    58  			fakeData := scFake.Data()["test_namespace/build.proto"]
    59  			if decompress {
    60  				So(fakeData.GetFlags().ContentType, ShouldEqual, luciexe.BuildProtoZlibContentType)
    61  			} else {
    62  				So(fakeData.GetFlags().ContentType, ShouldEqual, luciexe.BuildProtoContentType)
    63  			}
    64  
    65  			dgs := fakeData.GetDatagrams()
    66  			So(len(dgs), ShouldBeGreaterThanOrEqualTo, 1)
    67  
    68  			ret := make([]*bbpb.Build, len(dgs))
    69  			for i, dg := range dgs {
    70  				ret[i] = &bbpb.Build{}
    71  
    72  				var data []byte
    73  
    74  				if decompress {
    75  					r, err := zlib.NewReader(bytes.NewBufferString(dg))
    76  					So(err, ShouldBeNil)
    77  					data, err = io.ReadAll(r)
    78  					So(err, ShouldBeNil)
    79  				} else {
    80  					data = []byte(dg)
    81  				}
    82  
    83  				So(proto.Unmarshal(data, ret[i]), ShouldBeNil)
    84  			}
    85  			return ret
    86  		}
    87  		lastBuild := func() *bbpb.Build {
    88  			builds := getBuilds(false)
    89  			return builds[len(builds)-1]
    90  		}
    91  
    92  		args := []string{"fake_test_executable"}
    93  
    94  		Convey(`basic`, func() {
    95  			Convey(`success`, func() {
    96  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
    97  					build.Status = bbpb.Status_SCHEDULED
    98  					return nil
    99  				})
   100  				So(exitCode, ShouldEqual, 0)
   101  				So(lastBuild(), ShouldResembleProto, &bbpb.Build{
   102  					Status: bbpb.Status_SUCCESS,
   103  					Output: &bbpb.Build_Output{
   104  						Properties: &structpb.Struct{},
   105  						Status:     bbpb.Status_SUCCESS,
   106  					},
   107  				})
   108  			})
   109  
   110  			Convey(`failure`, func() {
   111  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   112  					return errors.New("bad stuff")
   113  				})
   114  				So(exitCode, ShouldEqual, 1)
   115  				So(lastBuild(), ShouldResembleProto, &bbpb.Build{
   116  					Status:          bbpb.Status_FAILURE,
   117  					SummaryMarkdown: "Final error: bad stuff",
   118  					Output: &bbpb.Build_Output{
   119  						Properties:      &structpb.Struct{},
   120  						Status:          bbpb.Status_FAILURE,
   121  						SummaryMarkdown: "Final error: bad stuff",
   122  					},
   123  				})
   124  			})
   125  
   126  			Convey(`infra failure`, func() {
   127  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   128  					return errors.New("bad stuff", InfraErrorTag)
   129  				})
   130  				So(exitCode, ShouldEqual, 1)
   131  				So(lastBuild(), ShouldResembleProto, &bbpb.Build{
   132  					Status:          bbpb.Status_INFRA_FAILURE,
   133  					SummaryMarkdown: "Final infra error: bad stuff",
   134  					Output: &bbpb.Build_Output{
   135  						Properties:      &structpb.Struct{},
   136  						Status:          bbpb.Status_INFRA_FAILURE,
   137  						SummaryMarkdown: "Final infra error: bad stuff",
   138  					},
   139  				})
   140  			})
   141  
   142  			Convey(`panic`, func() {
   143  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   144  					panic(errors.New("bad stuff"))
   145  				})
   146  				So(exitCode, ShouldEqual, 2)
   147  				So(lastBuild(), ShouldResembleProto, &bbpb.Build{
   148  					Status:          bbpb.Status_INFRA_FAILURE,
   149  					SummaryMarkdown: "Final panic: bad stuff",
   150  					Output: &bbpb.Build_Output{
   151  						Properties:      &structpb.Struct{},
   152  						Status:          bbpb.Status_INFRA_FAILURE,
   153  						SummaryMarkdown: "Final panic: bad stuff",
   154  					},
   155  				})
   156  			})
   157  
   158  			Convey(`respect user program status`, func() {
   159  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   160  					build.Status = bbpb.Status_INFRA_FAILURE
   161  					build.SummaryMarkdown = "status set inside"
   162  					return nil
   163  				})
   164  				So(exitCode, ShouldEqual, 0)
   165  				So(lastBuild(), ShouldResembleProto, &bbpb.Build{
   166  					Status:          bbpb.Status_INFRA_FAILURE,
   167  					SummaryMarkdown: "status set inside",
   168  					Output: &bbpb.Build_Output{
   169  						Properties:      &structpb.Struct{},
   170  						Status:          bbpb.Status_INFRA_FAILURE,
   171  						SummaryMarkdown: "status set inside",
   172  					},
   173  				})
   174  			})
   175  		})
   176  
   177  		Convey(`send`, func() {
   178  			exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   179  				build.SummaryMarkdown = "Hi. I did stuff."
   180  				bs()
   181  				return errors.New("oh no i failed")
   182  			})
   183  			So(exitCode, ShouldEqual, 1)
   184  			builds := getBuilds(false)
   185  			So(len(builds), ShouldEqual, 2)
   186  			So(builds[0], ShouldResembleProto, &bbpb.Build{
   187  				SummaryMarkdown: "Hi. I did stuff.",
   188  				Output: &bbpb.Build_Output{
   189  					Properties: &structpb.Struct{},
   190  				},
   191  			})
   192  			So(builds[len(builds)-1], ShouldResembleProto, &bbpb.Build{
   193  				Status:          bbpb.Status_FAILURE,
   194  				SummaryMarkdown: "Hi. I did stuff.\n\nFinal error: oh no i failed",
   195  				Output: &bbpb.Build_Output{
   196  					Properties:      &structpb.Struct{},
   197  					Status:          bbpb.Status_FAILURE,
   198  					SummaryMarkdown: "Hi. I did stuff.\n\nFinal error: oh no i failed",
   199  				},
   200  			})
   201  		})
   202  
   203  		Convey(`send (zlib)`, func() {
   204  			exitCode := runCtx(ctx, args, []Option{WithZlibCompression(5)}, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   205  				build.SummaryMarkdown = "Hi. I did stuff."
   206  				bs()
   207  				return errors.New("oh no i failed")
   208  			})
   209  			So(exitCode, ShouldEqual, 1)
   210  			builds := getBuilds(true)
   211  			So(len(builds), ShouldEqual, 2)
   212  			So(builds[0], ShouldResembleProto, &bbpb.Build{
   213  				SummaryMarkdown: "Hi. I did stuff.",
   214  				Output: &bbpb.Build_Output{
   215  					Properties: &structpb.Struct{},
   216  				},
   217  			})
   218  			So(builds[len(builds)-1], ShouldResembleProto, &bbpb.Build{
   219  				Status:          bbpb.Status_FAILURE,
   220  				SummaryMarkdown: "Hi. I did stuff.\n\nFinal error: oh no i failed",
   221  				Output: &bbpb.Build_Output{
   222  					Properties:      &structpb.Struct{},
   223  					Status:          bbpb.Status_FAILURE,
   224  					SummaryMarkdown: "Hi. I did stuff.\n\nFinal error: oh no i failed",
   225  				},
   226  			})
   227  		})
   228  
   229  		Convey(`output`, func() {
   230  			tdir, err := ioutil.TempDir("", "luciexe-exe-test")
   231  			So(err, ShouldBeNil)
   232  			defer os.RemoveAll(tdir)
   233  
   234  			Convey(`binary`, func() {
   235  				outFile := filepath.Join(tdir, "out.pb")
   236  				args = append(args, luciexe.OutputCLIArg, outFile)
   237  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   238  					build.SummaryMarkdown = "Hi."
   239  					err := WriteProperties(build.Output.Properties, map[string]any{
   240  						"some": "thing",
   241  					})
   242  					if err != nil {
   243  						panic(err)
   244  					}
   245  
   246  					return nil
   247  				})
   248  				So(exitCode, ShouldEqual, 0)
   249  				So(lastBuild(), ShouldResembleProto, &bbpb.Build{
   250  					Status:          bbpb.Status_SUCCESS,
   251  					SummaryMarkdown: "Hi.",
   252  					Output: &bbpb.Build_Output{
   253  						Properties: &structpb.Struct{
   254  							Fields: map[string]*structpb.Value{
   255  								"some": {Kind: &structpb.Value_StringValue{
   256  									StringValue: "thing",
   257  								}},
   258  							},
   259  						},
   260  						Status:          bbpb.Status_SUCCESS,
   261  						SummaryMarkdown: "Hi.",
   262  					},
   263  				})
   264  				data, err := os.ReadFile(outFile)
   265  				So(err, ShouldBeNil)
   266  				So(string(data), ShouldResemble,
   267  					"`\f\x82\x01\x1a\n\x11\n\x0f\n\x04some\x12\a\x1a\x05thing\x12\x03Hi.0\f\xa2\x01\x03Hi.")
   268  			})
   269  
   270  			Convey(`textpb`, func() {
   271  				outFile := filepath.Join(tdir, "out.textpb")
   272  				args = append(args, luciexe.OutputCLIArg, outFile)
   273  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   274  					build.SummaryMarkdown = "Hi."
   275  					return nil
   276  				})
   277  				So(exitCode, ShouldEqual, 0)
   278  				So(lastBuild(), ShouldResembleProto, &bbpb.Build{
   279  					Status:          bbpb.Status_SUCCESS,
   280  					SummaryMarkdown: "Hi.",
   281  					Output: &bbpb.Build_Output{
   282  						Properties:      &structpb.Struct{},
   283  						Status:          bbpb.Status_SUCCESS,
   284  						SummaryMarkdown: "Hi.",
   285  					},
   286  				})
   287  				data, err := os.ReadFile(outFile)
   288  				So(err, ShouldBeNil)
   289  				So(string(data), ShouldResemble,
   290  					"status: SUCCESS\nsummary_markdown: \"Hi.\"\noutput: <\n  properties: <\n  >\n  status: SUCCESS\n  summary_markdown: \"Hi.\"\n>\n")
   291  			})
   292  
   293  			Convey(`jsonpb`, func() {
   294  				outFile := filepath.Join(tdir, "out.json")
   295  				args = append(args, luciexe.OutputCLIArg, outFile)
   296  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   297  					build.SummaryMarkdown = "Hi."
   298  					return nil
   299  				})
   300  				So(exitCode, ShouldEqual, 0)
   301  				So(lastBuild(), ShouldResembleProto, &bbpb.Build{
   302  					Status:          bbpb.Status_SUCCESS,
   303  					SummaryMarkdown: "Hi.",
   304  					Output: &bbpb.Build_Output{
   305  						Properties:      &structpb.Struct{},
   306  						Status:          bbpb.Status_SUCCESS,
   307  						SummaryMarkdown: "Hi.",
   308  					},
   309  				})
   310  				data, err := os.ReadFile(outFile)
   311  				So(err, ShouldBeNil)
   312  				So(string(data), ShouldResemble,
   313  					"{\n  \"status\": \"SUCCESS\",\n  \"summary_markdown\": \"Hi.\",\n  \"output\": {\n    \"properties\": {\n      },\n    \"status\": \"SUCCESS\",\n    \"summary_markdown\": \"Hi.\"\n  }\n}")
   314  			})
   315  
   316  			Convey(`pass through user args`, func() {
   317  				// Delimiter inside user args should also be passed through
   318  				expectedUserArgs := []string{"foo", "bar", ArgsDelim, "baz"}
   319  				Convey(`when output is not specified`, func() {
   320  					args = append(args, ArgsDelim)
   321  					args = append(args, expectedUserArgs...)
   322  					exitcode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   323  						So(userArgs, ShouldResemble, expectedUserArgs)
   324  						return nil
   325  					})
   326  					So(exitcode, ShouldEqual, 0)
   327  				})
   328  				Convey(`when output is specified`, func() {
   329  					tdir, err := ioutil.TempDir("", "luciexe-exe-test")
   330  					So(err, ShouldBeNil)
   331  					defer os.RemoveAll(tdir)
   332  					args = append(args, luciexe.OutputCLIArg, filepath.Join(tdir, "out.pb"), ArgsDelim)
   333  					args = append(args, expectedUserArgs...)
   334  					exitcode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   335  						So(userArgs, ShouldResemble, expectedUserArgs)
   336  						return nil
   337  					})
   338  					So(exitcode, ShouldEqual, 0)
   339  				})
   340  			})
   341  
   342  			Convey(`write output on error`, func() {
   343  				outFile := filepath.Join(tdir, "out.json")
   344  				args = append(args, luciexe.OutputCLIArg, outFile)
   345  				exitCode := runCtx(ctx, args, nil, func(ctx context.Context, build *bbpb.Build, userArgs []string, bs BuildSender) error {
   346  					build.SummaryMarkdown = "Hi."
   347  					return errors.New("bad stuff")
   348  				})
   349  				So(exitCode, ShouldEqual, 1)
   350  				data, err := os.ReadFile(outFile)
   351  				So(err, ShouldBeNil)
   352  				So(string(data), ShouldResemble,
   353  					"{\n  \"status\": \"FAILURE\",\n  \"summary_markdown\": \"Hi.\\n\\nFinal error: bad stuff\",\n  \"output\": {\n    \"properties\": {\n      },\n    \"status\": \"FAILURE\",\n    \"summary_markdown\": \"Hi.\\n\\nFinal error: bad stuff\"\n  }\n}")
   354  			})
   355  		})
   356  	})
   357  }