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 }