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

     1  // Copyright 2020 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 build
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"os"
    21  	"path/filepath"
    22  	"strings"
    23  	"testing"
    24  
    25  	. "github.com/smartystreets/goconvey/convey"
    26  	"golang.org/x/time/rate"
    27  	"google.golang.org/protobuf/encoding/protojson"
    28  	"google.golang.org/protobuf/proto"
    29  	"google.golang.org/protobuf/types/known/structpb"
    30  	"google.golang.org/protobuf/types/known/timestamppb"
    31  
    32  	bbpb "go.chromium.org/luci/buildbucket/proto"
    33  	"go.chromium.org/luci/common/clock/testclock"
    34  	"go.chromium.org/luci/common/errors"
    35  	"go.chromium.org/luci/common/logging"
    36  	"go.chromium.org/luci/common/logging/memlogger"
    37  	"go.chromium.org/luci/common/system/environ"
    38  	. "go.chromium.org/luci/common/testing/assertions"
    39  	"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
    40  	"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
    41  	"go.chromium.org/luci/luciexe/build/internal/testpb"
    42  )
    43  
    44  func init() {
    45  	// ensure that send NEVER blocks while testing Main functionality
    46  	mainSendRate = rate.Inf
    47  }
    48  
    49  func TestMain(t *testing.T) {
    50  	// avoid t.Parallel() because this registers property handlers.
    51  
    52  	Convey(`Main`, t, func() {
    53  		ctx := memlogger.Use(context.Background())
    54  		logs := logging.Get(ctx).(*memlogger.MemLogger)
    55  
    56  		ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
    57  		nowpb := timestamppb.New(testclock.TestRecentTimeUTC)
    58  
    59  		scFake := streamclient.NewFake()
    60  		defer scFake.Unregister()
    61  
    62  		env := environ.New(nil)
    63  		env.Set(bootstrap.EnvStreamServerPath, scFake.StreamServerPath())
    64  		env.Set(bootstrap.EnvNamespace, "u")
    65  		ctx = env.SetInCtx(ctx)
    66  
    67  		imsg := &testpb.TopLevel{}
    68  		var setOut func(*testpb.TopLevel)
    69  
    70  		tdir := t.TempDir()
    71  
    72  		finalBuildPath := filepath.Join(tdir, "finalBuild.json")
    73  		args := []string{"myprogram", "--output", finalBuildPath}
    74  		stdin := &bytes.Buffer{}
    75  
    76  		mkStruct := func(dictlike map[string]any) *structpb.Struct {
    77  			s, err := structpb.NewStruct(dictlike)
    78  			So(err, ShouldBeNil)
    79  			return s
    80  		}
    81  
    82  		writeStdinProps := func(dictlike map[string]any) {
    83  			b := &bbpb.Build{
    84  				Input: &bbpb.Build_Input{
    85  					Properties: mkStruct(dictlike),
    86  				},
    87  			}
    88  			data, err := proto.Marshal(b)
    89  			So(err, ShouldBeNil)
    90  			_, err = stdin.Write(data)
    91  			So(err, ShouldBeNil)
    92  		}
    93  
    94  		getFinal := func() *bbpb.Build {
    95  			data, err := os.ReadFile(finalBuildPath)
    96  			So(err, ShouldBeNil)
    97  			ret := &bbpb.Build{}
    98  			So(protojson.Unmarshal(data, ret), ShouldBeNil)
    99  
   100  			// proto module is cute and tries to introduce non-deterministic
   101  			// characters into their error messages. This is annoying and unhelpful
   102  			// for tests where error messages intentionally can show up in the Build
   103  			// output. We manually normalize them here. Replaces non-breaking space
   104  			// (U+00a0) with space (U+0020)
   105  			ret.SummaryMarkdown = strings.ReplaceAll(ret.SummaryMarkdown, " ", " ")
   106  
   107  			return ret
   108  		}
   109  
   110  		Convey(`good`, func() {
   111  			Convey(`simple`, func() {
   112  				err := main(ctx, args, stdin, imsg, nil, nil, func(ctx context.Context, args []string, st *State) error {
   113  					So(args, ShouldBeNil)
   114  					return nil
   115  				})
   116  				So(err, ShouldBeNil)
   117  				So(getFinal(), ShouldResembleProto, &bbpb.Build{
   118  					StartTime: nowpb,
   119  					EndTime:   nowpb,
   120  					Status:    bbpb.Status_SUCCESS,
   121  					Output: &bbpb.Build_Output{
   122  						Status: bbpb.Status_SUCCESS,
   123  					},
   124  					Input: &bbpb.Build_Input{},
   125  				})
   126  			})
   127  
   128  			Convey(`user args`, func() {
   129  				args = append(args, "--", "custom", "stuff")
   130  				err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
   131  					So(args, ShouldResemble, []string{"custom", "stuff"})
   132  					return nil
   133  				})
   134  				So(err, ShouldBeNil)
   135  				So(getFinal(), ShouldResembleProto, &bbpb.Build{
   136  					StartTime: nowpb,
   137  					EndTime:   nowpb,
   138  					Status:    bbpb.Status_SUCCESS,
   139  					Output: &bbpb.Build_Output{
   140  						Status: bbpb.Status_SUCCESS,
   141  					},
   142  					Input: &bbpb.Build_Input{},
   143  				})
   144  			})
   145  
   146  			Convey(`inputProps`, func() {
   147  				writeStdinProps(map[string]any{
   148  					"field": "something",
   149  					"$cool": "blah",
   150  				})
   151  
   152  				err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
   153  					So(imsg, ShouldResembleProto, &testpb.TopLevel{
   154  						Field:         "something",
   155  						JsonNameField: "blah",
   156  					})
   157  					return nil
   158  				})
   159  				So(err, ShouldBeNil)
   160  			})
   161  
   162  			Convey(`help`, func() {
   163  				args = append(args, "--help")
   164  				err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
   165  					return nil
   166  				})
   167  				So(err, ShouldBeNil)
   168  				So(logs, memlogger.ShouldHaveLog, logging.Info, "`myprogram` is a `luciexe` binary. See go.chromium.org/luci/luciexe.")
   169  				So(logs, memlogger.ShouldHaveLog, logging.Info, "======= I/O Proto =======")
   170  				// TODO(iannucci): check I/O proto when implemented
   171  			})
   172  		})
   173  
   174  		Convey(`errors`, func() {
   175  			Convey(`returned`, func() {
   176  				err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
   177  					So(args, ShouldBeNil)
   178  					return errors.New("bad stuff")
   179  				})
   180  				So(err, ShouldEqual, errNonSuccess)
   181  				So(getFinal(), ShouldResembleProto, &bbpb.Build{
   182  					StartTime: nowpb,
   183  					EndTime:   nowpb,
   184  					Status:    bbpb.Status_FAILURE,
   185  					Output: &bbpb.Build_Output{
   186  						Status: bbpb.Status_FAILURE,
   187  					},
   188  					Input: &bbpb.Build_Input{},
   189  				})
   190  				So(logs, memlogger.ShouldHaveLog, logging.Error, "set status: FAILURE: bad stuff")
   191  			})
   192  
   193  			Convey(`panic`, func() {
   194  				err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
   195  					So(args, ShouldBeNil)
   196  					panic("BAD THINGS")
   197  				})
   198  				So(err, ShouldEqual, errNonSuccess)
   199  				So(getFinal(), ShouldResembleProto, &bbpb.Build{
   200  					StartTime: nowpb,
   201  					EndTime:   nowpb,
   202  					Status:    bbpb.Status_INFRA_FAILURE,
   203  					Output: &bbpb.Build_Output{
   204  						Status: bbpb.Status_INFRA_FAILURE,
   205  					},
   206  					Input: &bbpb.Build_Input{},
   207  				})
   208  				So(logs, memlogger.ShouldHaveLog, logging.Error, "set status: INFRA_FAILURE: PANIC")
   209  				So(logs, memlogger.ShouldHaveLog, logging.Error, "recovered panic: BAD THINGS")
   210  			})
   211  
   212  			Convey(`inputProps`, func() {
   213  				writeStdinProps(map[string]any{
   214  					"bogus": "something",
   215  				})
   216  				args = append(args, "--strict-input")
   217  
   218  				err := main(ctx, args, stdin, imsg, &setOut, nil, func(ctx context.Context, args []string, st *State) error {
   219  					return nil
   220  				})
   221  				So(err, ShouldErrLike, "parsing top-level properties")
   222  				summary := "fatal error starting build: parsing top-level properties: proto: (line 1:2): unknown field \"bogus\""
   223  				final := getFinal()
   224  				// protobuf package deliberately introduce random prefix:
   225  				// https://github.com/protocolbuffers/protobuf-go/blob/master/internal/errors/errors.go#L26
   226  				So(strings.ReplaceAll(final.SummaryMarkdown, "\u00a0", " "), ShouldEqual, summary)
   227  				So(strings.ReplaceAll(final.Output.SummaryMarkdown, "\u00a0", " "), ShouldEqual, summary)
   228  				So(final.Status, ShouldEqual, bbpb.Status_INFRA_FAILURE)
   229  				So(final.Output.Status, ShouldEqual, bbpb.Status_INFRA_FAILURE)
   230  			})
   231  
   232  		})
   233  	})
   234  }