github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/rexec/rexec_test.go (about) 1 // Package rexec_test contains tests for rexec package. It is a different package to avoid an 2 // import cycle. 3 package rexec_test 4 5 import ( 6 "bytes" 7 "context" 8 "os" 9 "path/filepath" 10 "testing" 11 12 "github.com/bazelbuild/remote-apis-sdks/go/pkg/command" 13 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 14 "github.com/bazelbuild/remote-apis-sdks/go/pkg/fakes" 15 "github.com/bazelbuild/remote-apis-sdks/go/pkg/outerr" 16 "github.com/google/go-cmp/cmp" 17 "github.com/google/go-cmp/cmp/cmpopts" 18 "google.golang.org/grpc/codes" 19 "google.golang.org/grpc/status" 20 "google.golang.org/protobuf/proto" 21 22 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 23 ) 24 25 func TestExecCacheHit(t *testing.T) { 26 e, cleanup := fakes.NewTestEnv(t) 27 defer cleanup() 28 fooPath := filepath.Join(e.ExecRoot, "foo") 29 fooBlob := []byte("hello") 30 if err := os.WriteFile(fooPath, fooBlob, 0777); err != nil { 31 t.Fatalf("failed to write input file %s", fooBlob) 32 } 33 tests := []struct { 34 name string 35 cmd *command.Command 36 output string 37 }{ 38 { 39 name: "no working dir", 40 cmd: &command.Command{ 41 Args: []string{"tool"}, 42 ExecRoot: e.ExecRoot, 43 InputSpec: &command.InputSpec{Inputs: []string{"foo"}}, 44 OutputFiles: []string{"a/b/out"}, 45 }, 46 output: "a/b/out", 47 }, { 48 name: "working dir", 49 cmd: &command.Command{ 50 Args: []string{"tool"}, 51 ExecRoot: e.ExecRoot, 52 WorkingDir: "wd", 53 InputSpec: &command.InputSpec{Inputs: []string{"foo"}}, 54 OutputFiles: []string{"a/b/out"}, 55 }, 56 output: "wd/a/b/out", 57 }, 58 } 59 60 for _, tc := range tests { 61 t.Run(tc.name, func(t *testing.T) { 62 opt := command.DefaultExecutionOptions() 63 wantRes := &command.Result{Status: command.CacheHitResultStatus} 64 cmdDg, acDg, stderrDg, stdoutDg := e.Set(tc.cmd, opt, wantRes, &fakes.OutputFile{Path: "a/b/out", Contents: "output"}, 65 fakes.StdOut("stdout"), fakes.StdErrRaw("stderr")) 66 oe := outerr.NewRecordingOutErr() 67 for i := 0; i < 2; i++ { 68 res, meta := e.Client.Run(context.Background(), tc.cmd, opt, oe) 69 70 fooDg := digest.NewFromBlob(fooBlob) 71 fooDir := &repb.Directory{Files: []*repb.FileNode{{Name: "foo", Digest: fooDg.ToProto(), IsExecutable: true}}} 72 fooDirDg, err := digest.NewFromMessage(fooDir) 73 if err != nil { 74 t.Fatalf("failed digesting message %v: %v", fooDir, err) 75 } 76 wantMeta := &command.Metadata{ 77 CommandDigest: cmdDg, 78 ActionDigest: acDg, 79 InputDirectories: 1, 80 InputFiles: 1, 81 TotalInputBytes: fooDirDg.Size + cmdDg.Size + acDg.Size + fooDg.Size, 82 OutputFiles: 1, 83 TotalOutputBytes: 18, // "output" + "stdout" + "stderr" 84 // "output" + "stdout" for both. StdErr is inlined in ActionResult in this test, and ActionResult 85 // isn't done through bytestream so not checked here. 86 LogicalBytesDownloaded: 12, 87 RealBytesDownloaded: 12, 88 OutputFileDigests: map[string]digest.Digest{"a/b/out": digest.NewFromBlob([]byte("output"))}, 89 OutputDirectoryDigests: map[string]digest.Digest{}, 90 OutputSymlinks: map[string]string{}, 91 StderrDigest: stderrDg, 92 StdoutDigest: stdoutDg, 93 } 94 if diff := cmp.Diff(wantRes, res); diff != "" { 95 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 96 } 97 if diff := cmp.Diff(wantMeta, meta, cmpopts.IgnoreFields(command.Metadata{}, "EventTimes", "AuxiliaryMetadata")); diff != "" { 98 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 99 } 100 var eventNames []string 101 for name, interval := range meta.EventTimes { 102 eventNames = append(eventNames, name) 103 if interval == nil || interval.To.Before(interval.From) { 104 t.Errorf("Run() gave bad timing stats for event %v: %v", name, interval) 105 } 106 } 107 wantNames := []string{ 108 command.EventComputeMerkleTree, 109 command.EventCheckActionCache, 110 command.EventDownloadResults, 111 } 112 if diff := cmp.Diff(wantNames, eventNames, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" { 113 t.Errorf("Run gave different events: want %v, got %v", wantNames, eventNames) 114 } 115 if i == 0 { 116 if !bytes.Equal(oe.Stdout(), []byte("stdout")) { 117 t.Errorf("Run() gave stdout diff: want \"stdout\", got: %v", oe.Stdout()) 118 } 119 if !bytes.Equal(oe.Stderr(), []byte("stderr")) { 120 t.Errorf("Run() gave stderr diff: want \"stderr\", got: %v", oe.Stderr()) 121 } 122 } 123 path := filepath.Join(e.ExecRoot, tc.output) 124 contents, err := os.ReadFile(path) 125 if err != nil { 126 t.Errorf("error reading from %s: %v", path, err) 127 } 128 if !bytes.Equal(contents, []byte("output")) { 129 t.Errorf("expected %s to contain \"output\", got %v", path, contents) 130 } 131 } 132 }) 133 } 134 } 135 136 // TestExecNotAcceptCached should skip both client-side and server side action cache lookups. 137 func TestExecNotAcceptCached(t *testing.T) { 138 e, cleanup := fakes.NewTestEnv(t) 139 defer cleanup() 140 cmd := &command.Command{Args: []string{"tool"}, ExecRoot: e.ExecRoot} 141 opt := &command.ExecutionOptions{AcceptCached: false, DownloadOutputs: true, DownloadOutErr: true} 142 wantRes := &command.Result{Status: command.SuccessResultStatus} 143 _, acDg, stderrDg, stdoutDg := e.Set(cmd, opt, wantRes, fakes.StdOutRaw("not cached")) 144 e.Server.ActionCache.Put(acDg, &repb.ActionResult{StdoutRaw: []byte("cached")}) 145 146 oe := outerr.NewRecordingOutErr() 147 148 res, meta := e.Client.Run(context.Background(), cmd, opt, oe) 149 wantMeta := &command.Metadata{ 150 ActionDigest: acDg, 151 InputDirectories: 1, 152 TotalOutputBytes: 10, 153 StderrDigest: stderrDg, 154 StdoutDigest: stdoutDg, 155 } 156 if diff := cmp.Diff(wantRes, res); diff != "" { 157 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 158 } 159 if diff := cmp.Diff(wantMeta, meta, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(command.Metadata{}, "CommandDigest", "TotalInputBytes", "EventTimes", "MissingDigests", "AuxiliaryMetadata")); diff != "" { 160 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 161 } 162 var eventNames []string 163 for name, interval := range meta.EventTimes { 164 eventNames = append(eventNames, name) 165 if interval == nil || interval.To.Before(interval.From) { 166 t.Errorf("Run() gave bad timing stats for event %v: %v", name, interval) 167 } 168 } 169 wantNames := []string{ 170 command.EventComputeMerkleTree, 171 command.EventUploadInputs, 172 command.EventExecuteRemotely, 173 command.EventServerQueued, 174 command.EventServerWorker, 175 command.EventServerWorkerInputFetch, 176 command.EventServerWorkerExecution, 177 command.EventServerWorkerOutputUpload, 178 command.EventDownloadResults, 179 } 180 if diff := cmp.Diff(wantNames, eventNames, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" { 181 t.Errorf("Run gave different events: want %v, got %v", wantNames, eventNames) 182 } 183 if len(meta.AuxiliaryMetadata) != 1 { 184 t.Errorf("Run gave incorrect auxiliary metadata entries: want %v, got %v", len(meta.AuxiliaryMetadata), 1) 185 } 186 187 if diff := cmp.Diff(wantRes, res); diff != "" { 188 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 189 } 190 if !bytes.Equal(oe.Stdout(), []byte("not cached")) { 191 t.Errorf("Run() gave stdout diff: want \"not cached\", got: %v", oe.Stdout()) 192 } 193 // We did specify DoNotCache=false, so the new result should now be cached: 194 if diff := cmp.Diff(e.Server.Exec.ActionResult, e.Server.ActionCache.Get(acDg), cmp.Comparer(proto.Equal)); diff != "" { 195 t.Errorf("Run() did not cache executed result (-want +got):\n%s", diff) 196 } 197 } 198 199 func TestExecManualCacheMiss(t *testing.T) { 200 tests := []struct { 201 name string 202 cached bool 203 want command.ResultStatus 204 }{ 205 { 206 name: "remote hit", 207 cached: true, 208 want: command.CacheHitResultStatus, 209 }, 210 { 211 name: "remote miss", 212 cached: false, 213 want: command.SuccessResultStatus, 214 }, 215 } 216 for _, tc := range tests { 217 t.Run(tc.name, func(t *testing.T) { 218 e, cleanup := fakes.NewTestEnv(t) 219 defer cleanup() 220 cmd := &command.Command{Args: []string{"tool"}, ExecRoot: e.ExecRoot} 221 opt := &command.ExecutionOptions{AcceptCached: true, DownloadOutputs: true, DownloadOutErr: true} 222 wantRes := &command.Result{Status: tc.want} 223 e.Set(cmd, opt, wantRes, fakes.StdErr("stderr"), fakes.ExecutionCacheHit(tc.cached)) 224 oe := outerr.NewRecordingOutErr() 225 226 res, _ := e.Client.Run(context.Background(), cmd, opt, oe) 227 228 if diff := cmp.Diff(wantRes, res); diff != "" { 229 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 230 } 231 if !bytes.Equal(oe.Stderr(), []byte("stderr")) { 232 t.Errorf("Run() gave stderr diff: want \"stderr\", got: %v", oe.Stderr()) 233 } 234 }) 235 } 236 } 237 238 func TestExecDoNotCache_NotAcceptCached(t *testing.T) { 239 e, cleanup := fakes.NewTestEnv(t) 240 defer cleanup() 241 cmd := &command.Command{Args: []string{"tool"}, ExecRoot: e.ExecRoot} 242 // DoNotCache true implies in particular that we also skip action cache lookups, local or remote. 243 opt := &command.ExecutionOptions{DoNotCache: true, DownloadOutputs: true, DownloadOutErr: true} 244 wantRes := &command.Result{Status: command.SuccessResultStatus} 245 _, acDg, _, _ := e.Set(cmd, opt, wantRes, fakes.StdOutRaw("not cached")) 246 e.Server.ActionCache.Put(acDg, &repb.ActionResult{StdoutRaw: []byte("cached")}) 247 oe := outerr.NewRecordingOutErr() 248 249 res, _ := e.Client.Run(context.Background(), cmd, opt, oe) 250 251 if diff := cmp.Diff(wantRes, res); diff != "" { 252 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 253 } 254 if !bytes.Equal(oe.Stdout(), []byte("not cached")) { 255 t.Errorf("Run() gave stdout diff: want \"not cached\", got: %v", oe.Stdout()) 256 } 257 // The action cache should still contain the same result, because we specified DoNotCache. 258 if !bytes.Equal(e.Server.ActionCache.Get(acDg).StdoutRaw, []byte("cached")) { 259 t.Error("Run() cached result for do_not_cache=true") 260 } 261 } 262 263 func TestExecRemoteFailureDownloadsPartialResults(t *testing.T) { 264 tests := []struct { 265 name string 266 wantRes *command.Result 267 }{ 268 { 269 name: "non zero exit", 270 wantRes: &command.Result{ExitCode: 52, Status: command.NonZeroExitResultStatus}, 271 }, 272 { 273 name: "remote error", 274 wantRes: command.NewRemoteErrorResult(status.New(codes.Internal, "problem").Err()), 275 }, 276 { 277 name: "timeout", 278 wantRes: command.NewTimeoutResult(), 279 }, 280 } 281 for _, tc := range tests { 282 t.Run(tc.name, func(t *testing.T) { 283 e, cleanup := fakes.NewTestEnv(t) 284 defer cleanup() 285 e.Client.GrpcClient.Retrier = nil // Disable retries 286 cmd := &command.Command{ 287 Args: []string{"tool"}, 288 OutputFiles: []string{"a/b/out"}, 289 ExecRoot: e.ExecRoot, 290 } 291 opt := command.DefaultExecutionOptions() 292 e.Set(cmd, opt, tc.wantRes, fakes.StdErr("stderr"), &fakes.OutputFile{Path: "a/b/out", Contents: "output"}) 293 oe := outerr.NewRecordingOutErr() 294 295 res, _ := e.Client.Run(context.Background(), cmd, opt, oe) 296 297 if diff := cmp.Diff(tc.wantRes, res, cmp.Comparer(proto.Equal), cmp.Comparer(equalError)); diff != "" { 298 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 299 } 300 if len(oe.Stdout()) != 0 { 301 t.Errorf("Run() gave unexpected stdout: %v", oe.Stdout()) 302 } 303 if !bytes.Equal(oe.Stderr(), []byte("stderr")) { 304 t.Errorf("Run() gave stderr diff: want \"stderr\", got: %v", oe.Stderr()) 305 } 306 path := filepath.Join(e.ExecRoot, "a/b/out") 307 contents, err := os.ReadFile(path) 308 if err != nil { 309 t.Errorf("error reading from %s: %v", path, err) 310 } 311 if !bytes.Equal(contents, []byte("output")) { 312 t.Errorf("expected %s to contain \"output\", got %v", path, contents) 313 } 314 }) 315 } 316 } 317 318 func equalError(x, y error) bool { 319 return x == y || (x != nil && y != nil && x.Error() == y.Error()) 320 } 321 322 func TestDoNotDownloadOutputs(t *testing.T) { 323 tests := []struct { 324 name string 325 cached bool 326 status *status.Status 327 exitCode int32 328 wantRes *command.Result 329 }{ 330 { 331 name: "success", 332 wantRes: &command.Result{Status: command.SuccessResultStatus}, 333 }, 334 { 335 name: "remote exec cache hit", 336 cached: true, 337 wantRes: &command.Result{Status: command.CacheHitResultStatus}, 338 }, 339 { 340 name: "action cache hit", 341 wantRes: &command.Result{Status: command.CacheHitResultStatus}, 342 }, 343 { 344 name: "non zero exit", 345 exitCode: 11, 346 wantRes: &command.Result{ExitCode: 11, Status: command.NonZeroExitResultStatus}, 347 }, 348 { 349 name: "timeout", 350 status: status.New(codes.DeadlineExceeded, "timeout"), 351 wantRes: command.NewTimeoutResult(), 352 }, 353 { 354 name: "remote failure", 355 status: status.New(codes.Internal, "problem"), 356 wantRes: command.NewRemoteErrorResult(status.New(codes.Internal, "problem").Err()), 357 }, 358 } 359 for _, tc := range tests { 360 t.Run(tc.name, func(t *testing.T) { 361 e, cleanup := fakes.NewTestEnv(t) 362 defer cleanup() 363 e.Client.GrpcClient.Retrier = nil // Disable retries 364 cmd := &command.Command{ 365 Args: []string{"tool"}, 366 OutputFiles: []string{"a/b/out"}, 367 ExecRoot: e.ExecRoot, 368 } 369 opt := &command.ExecutionOptions{AcceptCached: true, DownloadOutputs: false, DownloadOutErr: false} 370 e.Set(cmd, opt, tc.wantRes, fakes.StdOut("stdout"), fakes.StdErr("stderr"), &fakes.OutputFile{Path: "a/b/out", Contents: "output"}, fakes.ExecutionCacheHit(tc.cached)) 371 oe := outerr.NewRecordingOutErr() 372 373 res, _ := e.Client.Run(context.Background(), cmd, opt, oe) 374 375 if diff := cmp.Diff(tc.wantRes, res, cmp.Comparer(proto.Equal), cmp.Comparer(equalError)); diff != "" { 376 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 377 } 378 if len(oe.Stdout()) != 0 { 379 t.Errorf("Run() gave unexpected stdout: %v", string(oe.Stdout())) 380 } 381 if len(oe.Stderr()) != 0 { 382 t.Errorf("Run() gave unexpected stderr: %v", string(oe.Stderr())) 383 } 384 path := filepath.Join(e.ExecRoot, "a/b/out") 385 if _, err := os.Stat(path); !os.IsNotExist(err) { 386 t.Errorf("expected output file %s to not be downloaded, but it was", path) 387 } 388 }) 389 } 390 } 391 392 func TestStreamOutErr(t *testing.T) { 393 tests := []struct { 394 name string 395 cached bool 396 status *status.Status 397 exitCode int32 398 requestStreams bool 399 hasStdOutStream bool 400 hasStdErrStream bool 401 outChunks []string 402 errChunks []string 403 outContent string 404 errContent string 405 wantRes *command.Result 406 wantStdOut string 407 wantStdErr string 408 }{ 409 { 410 name: "success", 411 requestStreams: true, 412 hasStdOutStream: true, 413 hasStdErrStream: true, 414 wantRes: &command.Result{Status: command.SuccessResultStatus}, 415 wantStdOut: "streaming-stdout", 416 wantStdErr: "streaming-stderr", 417 }, 418 { 419 name: "not streaming", 420 requestStreams: false, 421 hasStdOutStream: true, 422 hasStdErrStream: true, 423 outContent: "stdout-blob", 424 errContent: "stderr-blob", 425 wantRes: &command.Result{Status: command.SuccessResultStatus}, 426 wantStdOut: "stdout-blob", 427 wantStdErr: "stderr-blob", 428 }, 429 { 430 name: "no stderr stream available", 431 requestStreams: true, 432 hasStdOutStream: true, 433 hasStdErrStream: false, 434 errContent: "stderr-blob", 435 wantRes: &command.Result{Status: command.SuccessResultStatus}, 436 wantStdOut: "streaming-stdout", 437 wantStdErr: "stderr-blob", 438 }, 439 { 440 name: "no stdout stream available", 441 requestStreams: true, 442 hasStdOutStream: false, 443 hasStdErrStream: true, 444 outContent: "stdout-blob", 445 wantRes: &command.Result{Status: command.SuccessResultStatus}, 446 wantStdOut: "stdout-blob", 447 wantStdErr: "streaming-stderr", 448 }, 449 { 450 name: "no streams available", 451 requestStreams: true, 452 hasStdOutStream: false, 453 hasStdErrStream: false, 454 outContent: "stdout-blob", 455 errContent: "stderr-blob", 456 wantRes: &command.Result{Status: command.SuccessResultStatus}, 457 wantStdOut: "stdout-blob", 458 wantStdErr: "stderr-blob", 459 }, 460 { 461 name: "remote exec cache hit", 462 requestStreams: true, 463 hasStdOutStream: true, 464 hasStdErrStream: true, 465 cached: true, 466 wantRes: &command.Result{Status: command.CacheHitResultStatus}, 467 wantStdOut: "streaming-stdout", 468 wantStdErr: "streaming-stderr", 469 }, 470 { 471 name: "action cache hit", 472 requestStreams: true, 473 hasStdOutStream: true, 474 hasStdErrStream: true, 475 outContent: "stdout-blob", 476 errContent: "stderr-blob", 477 wantRes: &command.Result{Status: command.CacheHitResultStatus}, 478 wantStdOut: "stdout-blob", 479 wantStdErr: "stderr-blob", 480 }, 481 { 482 name: "non zero exit", 483 requestStreams: true, 484 hasStdOutStream: true, 485 hasStdErrStream: true, 486 exitCode: 11, 487 wantRes: &command.Result{ExitCode: 11, Status: command.NonZeroExitResultStatus}, 488 wantStdOut: "streaming-stdout", 489 wantStdErr: "streaming-stderr", 490 }, 491 { 492 name: "remote failure", 493 requestStreams: true, 494 hasStdOutStream: true, 495 hasStdErrStream: true, 496 status: status.New(codes.Internal, "problem"), 497 wantRes: command.NewRemoteErrorResult(status.New(codes.Internal, "problem").Err()), 498 wantStdOut: "streaming-stdout", 499 wantStdErr: "streaming-stderr", 500 }, 501 { 502 name: "remote failure partial stream", 503 requestStreams: true, 504 hasStdOutStream: true, 505 hasStdErrStream: true, 506 outChunks: []string{"streaming"}, 507 errChunks: []string{"streaming"}, 508 outContent: "streaming-stdout", 509 errContent: "streaming-stderr", 510 status: status.New(codes.Internal, "problem"), 511 wantRes: command.NewRemoteErrorResult(status.New(codes.Internal, "problem").Err()), 512 wantStdOut: "streaming-stdout", 513 wantStdErr: "streaming-stderr", 514 }, 515 } 516 for _, tc := range tests { 517 t.Run(tc.name, func(t *testing.T) { 518 e, cleanup := fakes.NewTestEnv(t) 519 defer cleanup() 520 e.Client.GrpcClient.Retrier = nil // Disable retries 521 cmd := &command.Command{ 522 Args: []string{"tool"}, 523 OutputFiles: []string{"a/b/out"}, 524 ExecRoot: e.ExecRoot, 525 } 526 execOpts := &command.ExecutionOptions{ 527 AcceptCached: true, 528 DownloadOutputs: false, 529 DownloadOutErr: true, 530 StreamOutErr: tc.requestStreams, 531 } 532 outChunks := tc.outChunks 533 if outChunks == nil { 534 outChunks = []string{"streaming", "-", "stdout"} 535 } 536 errChunks := tc.errChunks 537 if errChunks == nil { 538 errChunks = []string{"streaming", "-", "stderr"} 539 } 540 outContent := tc.outContent 541 if outContent == "" { 542 outContent = "streaming-stdout" 543 } 544 errContent := tc.errContent 545 if errContent == "" { 546 errContent = "streaming-stderr" 547 } 548 opts := []fakes.Option{ 549 fakes.StdOut(outContent), 550 fakes.StdErr(errContent), 551 &fakes.LogStream{Name: "stdout-stream", Chunks: outChunks}, 552 &fakes.LogStream{Name: "stderr-stream", Chunks: errChunks}, 553 fakes.ExecutionCacheHit(tc.cached), 554 } 555 if tc.hasStdOutStream { 556 opts = append(opts, fakes.StdOutStream("stdout-stream")) 557 } 558 if tc.hasStdErrStream { 559 opts = append(opts, fakes.StdErrStream("stderr-stream")) 560 } 561 e.Set(cmd, execOpts, tc.wantRes, opts...) 562 oe := outerr.NewRecordingOutErr() 563 res, _ := e.Client.Run(context.Background(), cmd, execOpts, oe) 564 565 if diff := cmp.Diff(tc.wantRes, res, cmp.Comparer(proto.Equal), cmp.Comparer(equalError)); diff != "" { 566 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 567 } 568 if got := oe.Stdout(); !bytes.Equal(got, []byte(tc.wantStdOut)) { 569 t.Errorf("Run() gave stdout diff: want %q, got %q", tc.wantStdOut, string(got)) 570 } 571 if got := oe.Stderr(); !bytes.Equal(got, []byte(tc.wantStdErr)) { 572 t.Errorf("Run() gave stderr diff: want %q, got %q", tc.wantStdErr, string(got)) 573 } 574 }) 575 } 576 } 577 578 func TestOutputSymlinks(t *testing.T) { 579 e, cleanup := fakes.NewTestEnv(t) 580 defer cleanup() 581 e.Client.GrpcClient.Retrier = nil // Disable retries 582 cmd := &command.Command{ 583 Args: []string{"tool"}, 584 OutputFiles: []string{"a/b/out"}, 585 ExecRoot: e.ExecRoot, 586 } 587 opt := &command.ExecutionOptions{AcceptCached: true, DownloadOutputs: true, DownloadOutErr: false} 588 wantRes := &command.Result{Status: command.CacheHitResultStatus} 589 cmdDg, acDg, stderrDg, stdoutDg := e.Set(cmd, opt, wantRes, fakes.StdOut("stdout"), fakes.StdErr("stderr"), &fakes.OutputFile{Path: "a/b/out", Contents: "output"}, &fakes.OutputSymlink{Path: "a/b/sl", Target: "out"}, fakes.ExecutionCacheHit(true)) 590 oe := outerr.NewRecordingOutErr() 591 592 res, meta := e.Client.Run(context.Background(), cmd, opt, oe) 593 594 if diff := cmp.Diff(wantRes, res, cmp.Comparer(proto.Equal), cmp.Comparer(equalError)); diff != "" { 595 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 596 } 597 if len(oe.Stdout()) != 0 { 598 t.Errorf("Run() gave unexpected stdout: %v", string(oe.Stdout())) 599 } 600 if len(oe.Stderr()) != 0 { 601 t.Errorf("Run() gave unexpected stderr: %v", string(oe.Stderr())) 602 } 603 path := filepath.Join(e.ExecRoot, "a/b/out") 604 if _, err := os.Stat(path); err != nil { 605 t.Errorf("expected output file %s to not be downloaded, but it was", path) 606 } 607 path = filepath.Join(e.ExecRoot, "a/b/sl") 608 file, err := os.Lstat(path) 609 if err != nil { 610 t.Errorf("expected output file %s to be downloaded, but it was not", path) 611 } 612 if file.Mode()&os.ModeSymlink == 0 { 613 t.Errorf("expected output file %s to be a symlink, but it was not", path) 614 } 615 if dest, err := os.Readlink(path); err != nil || dest != "out" { 616 t.Errorf("expected output file %s to link to a/b/out, got %v, %v", path, dest, err) 617 } 618 wantMeta := &command.Metadata{ 619 CommandDigest: cmdDg, 620 ActionDigest: acDg, 621 InputDirectories: 1, 622 TotalInputBytes: cmdDg.Size + acDg.Size, 623 OutputFiles: 2, 624 TotalOutputBytes: 18, // "output" + "stdout" + "stderr" 625 // "output" + "stdout" for both. StdErr is inlined in ActionResult in this test, and ActionResult 626 // isn't done through bytestream so not checked here. 627 LogicalBytesDownloaded: 6, 628 RealBytesDownloaded: 6, 629 OutputFileDigests: map[string]digest.Digest{"a/b/out": digest.NewFromBlob([]byte("output"))}, 630 OutputDirectoryDigests: map[string]digest.Digest{}, 631 OutputSymlinks: map[string]string{"a/b/sl": "out"}, 632 StderrDigest: stderrDg, 633 StdoutDigest: stdoutDg, 634 } 635 if diff := cmp.Diff(wantRes, res); diff != "" { 636 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 637 } 638 if diff := cmp.Diff(wantMeta, meta, cmpopts.IgnoreFields(command.Metadata{}, "EventTimes", "AuxiliaryMetadata")); diff != "" { 639 t.Errorf("Run() gave result diff (-want +got):\n%s", diff) 640 } 641 } 642 643 func TestGetOutputFileDigests(t *testing.T) { 644 e, cleanup := fakes.NewTestEnv(t) 645 defer cleanup() 646 fooPath := filepath.Join(e.ExecRoot, "foo") 647 fooBlob := []byte("hello") 648 if err := os.WriteFile(fooPath, fooBlob, 0777); err != nil { 649 t.Fatalf("failed to write input file %s", fooBlob) 650 } 651 cmd := &command.Command{ 652 Args: []string{"tool"}, 653 ExecRoot: e.ExecRoot, 654 InputSpec: &command.InputSpec{Inputs: []string{"foo"}}, 655 OutputFiles: []string{"a/b/out"}, 656 } 657 opt := &command.ExecutionOptions{AcceptCached: true, DownloadOutputs: false, DownloadOutErr: false} 658 oe := outerr.NewRecordingOutErr() 659 ec, err := e.Client.NewContext(context.Background(), cmd, opt, oe) 660 if err != nil { 661 t.Fatalf("failed creating execution context: %v", err) 662 } 663 outBlob := []byte("out!") 664 wantRes := &command.Result{Status: command.CacheHitResultStatus} 665 e.Set(cmd, opt, wantRes, &fakes.OutputFile{Path: "a/b/out", Contents: string(outBlob)}, 666 fakes.StdOut("stdout"), fakes.StdErrRaw("stderr")) 667 668 ec.GetCachedResult() 669 670 tests := []struct { 671 useAbsPath bool 672 name string 673 want map[string]digest.Digest 674 }{ 675 { 676 name: "relative paths", 677 useAbsPath: false, 678 want: map[string]digest.Digest{ 679 "a/b/out": digest.NewFromBlob(outBlob), 680 }, 681 }, 682 { 683 name: "absolute paths", 684 useAbsPath: true, 685 want: map[string]digest.Digest{ 686 filepath.Join(e.ExecRoot, "a/b/out"): digest.NewFromBlob(outBlob), 687 }, 688 }, 689 } 690 for _, tc := range tests { 691 t.Run(tc.name, func(t *testing.T) { 692 got, err := ec.GetOutputFileDigests(tc.useAbsPath) 693 if err != nil { 694 t.Fatalf("GetOutputFileDigests(%v) failed: %v", tc.useAbsPath, err) 695 } 696 if diff := cmp.Diff(tc.want, got); diff != "" { 697 t.Fatalf("GetOutputFileDigests(%v) returned diff (-want +got):\n%s", tc.useAbsPath, diff) 698 } 699 }) 700 } 701 } 702 703 func TestUpdateRemoteCache(t *testing.T) { 704 e, cleanup := fakes.NewTestEnv(t) 705 defer cleanup() 706 fooPath := filepath.Join(e.ExecRoot, "foo") 707 fooBlob := []byte("hello") 708 if err := os.WriteFile(fooPath, fooBlob, 0777); err != nil { 709 t.Fatalf("failed to write input file %s", fooBlob) 710 } 711 cmd := &command.Command{ 712 Args: []string{"tool"}, 713 ExecRoot: e.ExecRoot, 714 InputSpec: &command.InputSpec{Inputs: []string{"foo"}}, 715 OutputFiles: []string{"a/b/out"}, 716 } 717 opt := command.DefaultExecutionOptions() 718 oe := outerr.NewRecordingOutErr() 719 720 ec, err := e.Client.NewContext(context.Background(), cmd, opt, oe) 721 if err != nil { 722 t.Fatalf("failed creating execution context: %v", err) 723 } 724 // Simulating local execution. 725 outPath := filepath.Join(e.ExecRoot, "a/b/out") 726 if err := os.MkdirAll(filepath.Dir(outPath), os.FileMode(0777)); err != nil { 727 t.Fatalf("failed to create output file parents %s: %v", outPath, err) 728 } 729 outBlob := []byte("out!") 730 if err := os.WriteFile(outPath, outBlob, 0777); err != nil { 731 t.Fatalf("failed to write output file %s: %v", outPath, err) 732 } 733 ec.UpdateCachedResult() 734 if diff := cmp.Diff(&command.Result{Status: command.SuccessResultStatus}, ec.Result); diff != "" { 735 t.Errorf("UpdateCachedResult() gave result diff (-want +got):\n%s", diff) 736 } 737 if _, ok := e.Server.CAS.Get(ec.Metadata.ActionDigest); !ok { 738 t.Error("UpdateCachedResult() failed to upload Action proto") 739 } 740 if _, ok := e.Server.CAS.Get(ec.Metadata.CommandDigest); !ok { 741 t.Error("UpdateCachedResult() failed to upload Command proto") 742 } 743 // Now delete the local result and check that we get a remote cache hit and download it. 744 if err := os.Remove(outPath); err != nil { 745 t.Fatalf("failed to remove output file %s", outPath) 746 } 747 ec.GetCachedResult() 748 if diff := cmp.Diff(&command.Result{Status: command.CacheHitResultStatus}, ec.Result); diff != "" { 749 t.Errorf("GetCachedResult() gave result diff (-want +got):\n%s", diff) 750 } 751 contents, err := os.ReadFile(outPath) 752 if err != nil { 753 t.Errorf("error reading from %s: %v", outPath, err) 754 } 755 if !bytes.Equal(contents, outBlob) { 756 t.Errorf("expected %s to contain %q, got %v", outPath, string(outBlob), contents) 757 } 758 file, err := os.Stat(outPath) 759 if err != nil { 760 t.Errorf("error reading from %s: %v", outPath, err) 761 } else if (file.Mode() & 0100) == 0 { 762 t.Errorf("expected %s to have executable permission", outPath) 763 } 764 if len(oe.Stdout()) != 0 { 765 t.Errorf("GetCachedResult() gave unexpected stdout: %v", oe.Stdout()) 766 } 767 if len(oe.Stderr()) != 0 { 768 t.Errorf("GetCachedResult() gave unexpected stdout: %v", oe.Stderr()) 769 } 770 } 771 772 func TestDownloadResults(t *testing.T) { 773 e, cleanup := fakes.NewTestEnv(t) 774 defer cleanup() 775 fooPath := filepath.Join(e.ExecRoot, "foo") 776 fooBlob := []byte("hello") 777 if err := os.WriteFile(fooPath, fooBlob, 0777); err != nil { 778 t.Fatalf("failed to write input file %s", fooBlob) 779 } 780 cmd := &command.Command{ 781 Args: []string{"tool"}, 782 ExecRoot: e.ExecRoot, 783 InputSpec: &command.InputSpec{Inputs: []string{"foo"}}, 784 OutputFiles: []string{"a/b/out"}, 785 } 786 opt := &command.ExecutionOptions{AcceptCached: true, DownloadOutputs: false, DownloadOutErr: false} 787 oe := outerr.NewRecordingOutErr() 788 ec, err := e.Client.NewContext(context.Background(), cmd, opt, oe) 789 if err != nil { 790 t.Fatalf("failed creating execution context: %v", err) 791 } 792 outPath := filepath.Join(e.ExecRoot, "a/b/out") 793 outBlob := []byte("out!") 794 wantRes := &command.Result{Status: command.CacheHitResultStatus} 795 e.Set(cmd, opt, wantRes, &fakes.OutputFile{Path: "a/b/out", Contents: string(outBlob)}, 796 fakes.StdOut("stdout"), fakes.StdErrRaw("stderr")) 797 ec.GetCachedResult() 798 if diff := cmp.Diff(wantRes, ec.Result); diff != "" { 799 t.Errorf("GetCachedResult() gave result diff (-want +got):\n%s", diff) 800 } 801 if _, err := os.Stat(outPath); !os.IsNotExist(err) { 802 t.Errorf("expected output file %s to not be downloaded, but it was", outPath) 803 } 804 if len(oe.Stdout()) != 0 { 805 t.Errorf("DownloadOutputs() gave unexpected stdout: %v", string(oe.Stdout())) 806 } 807 if len(oe.Stderr()) != 0 { 808 t.Errorf("DownloadOutputs() gave unexpected stderr: %v", string(oe.Stderr())) 809 } 810 ec.DownloadOutErr() 811 if _, err := os.Stat(outPath); !os.IsNotExist(err) { 812 t.Errorf("expected output file %s to not be downloaded, but it was", outPath) 813 } 814 if string(oe.Stdout()) != "stdout" { 815 t.Errorf("DownloadOutputs() stdout = %v, want 'stdout'", string(oe.Stdout())) 816 } 817 if string(oe.Stderr()) != "stderr" { 818 t.Errorf("DownloadOutputs() stderr = %v, want 'stderr'", string(oe.Stderr())) 819 } 820 ec.DownloadOutputs(e.ExecRoot) 821 contents, err := os.ReadFile(outPath) 822 if err != nil { 823 t.Errorf("error reading from %s: %v", outPath, err) 824 } 825 if !bytes.Equal(contents, outBlob) { 826 t.Errorf("expected %s to contain %q, got %v", outPath, string(outBlob), contents) 827 } 828 if string(oe.Stdout()) != "stdout" { 829 t.Errorf("DownloadOutputs() stdout = %v, want 'stdout'", string(oe.Stdout())) 830 } 831 if string(oe.Stderr()) != "stderr" { 832 t.Errorf("DownloadOutputs() stderr = %v, want 'stderr'", string(oe.Stderr())) 833 } 834 }