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  }