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 }