github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/fakes/server.go (about)

     1  package fakes
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/chunker"
    14  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/client"
    15  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/command"
    16  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest"
    17  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata"
    18  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/rexec"
    19  	"google.golang.org/grpc"
    20  	"google.golang.org/grpc/codes"
    21  	"google.golang.org/grpc/status"
    22  	"google.golang.org/protobuf/proto"
    23  
    24  	// Redundant imports are required for the google3 mirror. Aliases should not be changed.
    25  	rc "github.com/bazelbuild/remote-apis-sdks/go/pkg/client"
    26  	apb "github.com/bazelbuild/remote-apis-sdks/go/pkg/fakes/auxpb"
    27  	regrpc "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    28  	repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    29  	bsgrpc "google.golang.org/genproto/googleapis/bytestream"
    30  	bspb "google.golang.org/genproto/googleapis/bytestream"
    31  	anypb "google.golang.org/protobuf/types/known/anypb"
    32  	dpb "google.golang.org/protobuf/types/known/durationpb"
    33  	tspb "google.golang.org/protobuf/types/known/timestamppb"
    34  )
    35  
    36  // Server is a configurable fake in-process RBE server for use in integration tests.
    37  type Server struct {
    38  	Exec        *Exec
    39  	CAS         *CAS
    40  	LogStreams  *LogStreams
    41  	ActionCache *ActionCache
    42  	listener    net.Listener
    43  	srv         *grpc.Server
    44  }
    45  
    46  // NewServer creates a server that is ready to accept requests.
    47  func NewServer(t testing.TB) (s *Server, err error) {
    48  	cas := NewCAS()
    49  	ls := NewLogStreams()
    50  	ac := NewActionCache()
    51  	s = &Server{Exec: NewExec(t, ac, cas), CAS: cas, LogStreams: ls, ActionCache: ac}
    52  	s.listener, err = net.Listen("tcp", ":0")
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	s.srv = grpc.NewServer()
    57  	bsgrpc.RegisterByteStreamServer(s.srv, s)
    58  	regrpc.RegisterContentAddressableStorageServer(s.srv, s.CAS)
    59  	regrpc.RegisterActionCacheServer(s.srv, s.ActionCache)
    60  	regrpc.RegisterCapabilitiesServer(s.srv, s.Exec)
    61  	regrpc.RegisterExecutionServer(s.srv, s.Exec)
    62  	go s.srv.Serve(s.listener)
    63  	return s, nil
    64  }
    65  
    66  // Clear clears the fake results.
    67  func (s *Server) Clear() {
    68  	s.CAS.Clear()
    69  	s.LogStreams.Clear()
    70  	s.ActionCache.Clear()
    71  	s.Exec.Clear()
    72  }
    73  
    74  // Stop shuts down the in process server.
    75  func (s *Server) Stop() {
    76  	s.listener.Close()
    77  	s.srv.Stop()
    78  }
    79  
    80  // NewTestClient returns a new in-process Client connected to this server.
    81  func (s *Server) NewTestClient(ctx context.Context) (*rc.Client, error) {
    82  	return rc.NewClient(ctx, "instance", s.dialParams())
    83  }
    84  
    85  // NewClientConn returns a gRPC client connction to the server.
    86  func (s *Server) NewClientConn(ctx context.Context) (*grpc.ClientConn, error) {
    87  	p := s.dialParams()
    88  	conn, _, err := client.Dial(ctx, p.Service, p)
    89  	return conn, err
    90  }
    91  
    92  func (s *Server) dialParams() rc.DialParams {
    93  	return rc.DialParams{
    94  		Service:    s.listener.Addr().String(),
    95  		NoSecurity: true,
    96  	}
    97  }
    98  
    99  // Read will serve both logstream and CAS resources, depending on the resource type indicated in the
   100  // request.
   101  func (s *Server) Read(req *bspb.ReadRequest, stream bsgrpc.ByteStream_ReadServer) error {
   102  	path := strings.Split(req.ResourceName, "/")
   103  	if len(path) < 2 || path[0] != "instance" {
   104  		return status.Errorf(codes.InvalidArgument, "test fake expected resource name of the form \"instance/<type>/...\", got %q", req.ResourceName)
   105  	}
   106  	if path[1] == "logstreams" {
   107  		return s.LogStreams.Read(req, stream)
   108  	} else if path[1] == "blobs" || path[1] == "compressed-blobs" {
   109  		return s.CAS.Read(req, stream)
   110  	}
   111  	return status.Errorf(codes.InvalidArgument, "invalid resource type: %q", path[1])
   112  }
   113  
   114  // Write writes a blob to CAS.
   115  func (s *Server) Write(stream bsgrpc.ByteStream_WriteServer) error {
   116  	return s.CAS.Write(stream)
   117  }
   118  
   119  // QueryWriteStatus implements the corresponding RE API function.
   120  func (*Server) QueryWriteStatus(context.Context, *bspb.QueryWriteStatusRequest) (*bspb.QueryWriteStatusResponse, error) {
   121  	return nil, status.Error(codes.Unimplemented, "test fake does not implement method")
   122  }
   123  
   124  // TestEnv is a wrapper for convenient integration tests of remote execution.
   125  type TestEnv struct {
   126  	Client   *rexec.Client
   127  	Server   *Server
   128  	ExecRoot string
   129  	t        testing.TB
   130  }
   131  
   132  // NewTestEnv initializes a TestEnv containing a fake server, a client connected to it,
   133  // and a temporary directory used as execution root for inputs and outputs.
   134  // It returns the new env and a cleanup function that should be called in the end of the test.
   135  func NewTestEnv(t testing.TB) (*TestEnv, func()) {
   136  	t.Helper()
   137  	// Set up temp directory.
   138  	execRoot := t.TempDir()
   139  	// Set up the fake.
   140  	s, err := NewServer(t)
   141  	if err != nil {
   142  		t.Fatalf("Error starting fake server: %v", err)
   143  	}
   144  	grpcClient, err := s.NewTestClient(context.Background())
   145  	if err != nil {
   146  		t.Fatalf("Error connecting to server: %v", err)
   147  	}
   148  	return &TestEnv{
   149  			Client: &rexec.Client{
   150  				FileMetadataCache: filemetadata.NewNoopCache(),
   151  				GrpcClient:        grpcClient,
   152  			},
   153  			Server:   s,
   154  			ExecRoot: execRoot,
   155  			t:        t,
   156  		}, func() {
   157  			grpcClient.Close()
   158  			s.Stop()
   159  			os.RemoveAll(execRoot)
   160  		}
   161  }
   162  
   163  func timeToProto(t time.Time) *tspb.Timestamp {
   164  	if t.IsZero() {
   165  		return nil
   166  	}
   167  	return tspb.New(t)
   168  }
   169  
   170  // Set sets up the fake to return the given result on the given command execution.
   171  // It is not possible to make the fake result in a LocalErrorResultStatus or an InterruptedResultStatus.
   172  func (e *TestEnv) Set(cmd *command.Command, opt *command.ExecutionOptions, res *command.Result, opts ...Option) (cmdDg, acDg, stderrDg, stdoutDg digest.Digest) {
   173  	e.t.Helper()
   174  	cmd.FillDefaultFieldValues()
   175  	if err := cmd.Validate(); err != nil {
   176  		e.t.Fatalf("command validation failed: %v", err)
   177  	}
   178  
   179  	auxMeta := &apb.AuxiliaryMetadata{FakeMemoryPercentagePeak: 50.0}
   180  	anyAuxMeta, err := anypb.New(auxMeta)
   181  	if err != nil {
   182  		e.t.Fatalf("unable to create fake auxiliary anypb metadata: %v", err)
   183  	}
   184  	t, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
   185  	ar := &repb.ActionResult{
   186  		ExitCode: int32(res.ExitCode),
   187  		ExecutionMetadata: &repb.ExecutedActionMetadata{
   188  			QueuedTimestamp:                timeToProto(t.Add(time.Millisecond)),
   189  			WorkerStartTimestamp:           timeToProto(t.Add(2 * time.Millisecond)),
   190  			WorkerCompletedTimestamp:       timeToProto(t.Add(3 * time.Millisecond)),
   191  			InputFetchStartTimestamp:       timeToProto(t.Add(4 * time.Millisecond)),
   192  			InputFetchCompletedTimestamp:   timeToProto(t.Add(5 * time.Millisecond)),
   193  			ExecutionStartTimestamp:        timeToProto(t.Add(6 * time.Millisecond)),
   194  			ExecutionCompletedTimestamp:    timeToProto(t.Add(7 * time.Millisecond)),
   195  			OutputUploadStartTimestamp:     timeToProto(t.Add(8 * time.Millisecond)),
   196  			OutputUploadCompletedTimestamp: timeToProto(t.Add(9 * time.Millisecond)),
   197  			AuxiliaryMetadata:              []*anypb.Any{anyAuxMeta},
   198  		},
   199  	}
   200  	for _, o := range opts {
   201  		if err := o.apply(ar, e.Server, e.ExecRoot); err != nil {
   202  			e.t.Fatalf("error applying option %+v: %v", o, err)
   203  		}
   204  	}
   205  
   206  	execRoot, workingDir, remoteWorkingDir := cmd.ExecRoot, cmd.WorkingDir, cmd.RemoteWorkingDir
   207  	root, inputs, _, err := e.Client.GrpcClient.ComputeMerkleTree(context.Background(), execRoot, workingDir, remoteWorkingDir, cmd.InputSpec, e.Client.FileMetadataCache)
   208  	if err != nil {
   209  		e.t.Fatalf("error building input tree in fake setup: %v", err)
   210  		return digest.Empty, digest.Empty, digest.Empty, digest.Empty
   211  	}
   212  	for _, inp := range inputs {
   213  		ch, err := chunker.New(inp, false, int(e.Client.GrpcClient.ChunkMaxSize))
   214  		if err != nil {
   215  			e.t.Fatalf("error getting data from input entry: %v", err)
   216  		}
   217  		bytes, err := ch.FullData()
   218  		if err != nil {
   219  			e.t.Fatalf("error getting data from input chunker: %v", err)
   220  		}
   221  		e.Server.CAS.Put(bytes)
   222  	}
   223  
   224  	cmdPb := cmd.ToREProto(false)
   225  	bytes, err := proto.Marshal(cmdPb)
   226  	if err != nil {
   227  		e.t.Fatalf("error inserting command digest blob into CAS %v", err)
   228  	}
   229  	e.Server.CAS.Put(bytes)
   230  
   231  	cmdDg = digest.TestNewFromMessage(cmdPb)
   232  	ac := &repb.Action{
   233  		CommandDigest:   cmdDg.ToProto(),
   234  		InputRootDigest: root.ToProto(),
   235  		DoNotCache:      opt.DoNotCache,
   236  	}
   237  	if cmd.Timeout > 0 {
   238  		ac.Timeout = dpb.New(cmd.Timeout)
   239  	}
   240  	acDg = digest.TestNewFromMessage(ac)
   241  	if ar.StderrDigest != nil {
   242  		stderrDg = digest.NewFromProtoUnvalidated(ar.StderrDigest)
   243  	}
   244  	if ar.StdoutDigest != nil {
   245  		stdoutDg = digest.NewFromProtoUnvalidated(ar.StdoutDigest)
   246  	}
   247  
   248  	bytes, err = proto.Marshal(ac)
   249  	if err != nil {
   250  		e.t.Fatalf("error inserting action digest blob into CAS %v", err)
   251  	}
   252  	e.Server.CAS.Put(bytes)
   253  
   254  	e.Server.Exec.adg = acDg
   255  	e.Server.Exec.ActionResult = ar
   256  	switch res.Status {
   257  	case command.TimeoutResultStatus:
   258  		if res.Err == nil {
   259  			e.Server.Exec.Status = status.New(codes.DeadlineExceeded, "timeout")
   260  		} else {
   261  			e.Server.Exec.Status = status.New(codes.DeadlineExceeded, res.Err.Error())
   262  		}
   263  	case command.RemoteErrorResultStatus:
   264  		st, ok := status.FromError(res.Err)
   265  		if !ok {
   266  			if res.Err == nil {
   267  				st = status.New(codes.Internal, "remote error")
   268  			} else {
   269  				st = status.New(codes.Internal, res.Err.Error())
   270  			}
   271  		}
   272  		e.Server.Exec.Status = st
   273  	case command.CacheHitResultStatus:
   274  		if !e.Server.Exec.Cached { // Assume the user means in this case the actual ActionCache should miss.
   275  			e.Server.ActionCache.Put(acDg, ar)
   276  		}
   277  	}
   278  	return cmdDg, acDg, stderrDg, stdoutDg
   279  }
   280  
   281  // Option provides extra configuration for the test environment.
   282  type Option interface {
   283  	apply(*repb.ActionResult, *Server, string) error
   284  }
   285  
   286  // InputFile to be made available to the fake action.
   287  type InputFile struct {
   288  	Path     string
   289  	Contents string
   290  }
   291  
   292  // Apply creates a file in the execroot with the given content
   293  // and also inserts the file blob into CAS.
   294  func (f *InputFile) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   295  	bytes := []byte(f.Contents)
   296  	if err := os.MkdirAll(filepath.Join(execRoot, filepath.Dir(f.Path)), os.ModePerm); err != nil {
   297  		return fmt.Errorf("failed to create input dir %v: %v", filepath.Dir(f.Path), err)
   298  	}
   299  	err := os.WriteFile(filepath.Join(execRoot, f.Path), bytes, 0755)
   300  	if err != nil {
   301  		return fmt.Errorf("failed to setup file %v under temp exec root %v: %v", f.Path, execRoot, err)
   302  	}
   303  	s.CAS.Put(bytes)
   304  	return nil
   305  }
   306  
   307  // OutputFile is to be added as an output of the fake action.
   308  type OutputFile struct {
   309  	Path     string
   310  	Contents string
   311  }
   312  
   313  // Apply puts the file in the fake CAS and the given ActionResult.
   314  func (f *OutputFile) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   315  	bytes := []byte(f.Contents)
   316  	s.Exec.OutputBlobs = append(s.Exec.OutputBlobs, bytes)
   317  	dg := s.CAS.Put(bytes)
   318  	ac.OutputFiles = append(ac.OutputFiles, &repb.OutputFile{Path: f.Path, Digest: dg.ToProto()})
   319  	return nil
   320  }
   321  
   322  // OutputDir is to be added as an output of the fake action.
   323  type OutputDir struct {
   324  	Path string
   325  }
   326  
   327  // Apply puts the file in the fake CAS and the given ActionResult.
   328  func (d *OutputDir) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   329  	root, ch, err := BuildDir(d.Path, s, execRoot)
   330  	if err != nil {
   331  		return fmt.Errorf("failed to build directory tree: %v", err)
   332  	}
   333  	tr := &repb.Tree{
   334  		Root:     root,
   335  		Children: ch,
   336  	}
   337  	treeBlob, err := proto.Marshal(tr)
   338  	if err != nil {
   339  		return fmt.Errorf("failed to marshal tree: %v", err)
   340  	}
   341  	treeDigest := s.CAS.Put(treeBlob)
   342  	ac.OutputDirectories = append(ac.OutputDirectories, &repb.OutputDirectory{Path: d.Path, TreeDigest: treeDigest.ToProto()})
   343  	return nil
   344  }
   345  
   346  // OutputSymlink is to be added as an output of the fake action.
   347  type OutputSymlink struct {
   348  	Path   string
   349  	Target string
   350  }
   351  
   352  // Apply puts the file in the fake CAS and the given ActionResult.
   353  func (l *OutputSymlink) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   354  	ac.OutputFileSymlinks = append(ac.OutputFileSymlinks, &repb.OutputSymlink{Path: l.Path, Target: l.Target})
   355  	return nil
   356  }
   357  
   358  // BuildDir builds the directory tree by recursively iterating through the directory.
   359  // This is similar to tree.go ComputeMerkleTree.
   360  func BuildDir(path string, s *Server, execRoot string) (root *repb.Directory, childDir []*repb.Directory, err error) {
   361  	res := &repb.Directory{}
   362  	ch := []*repb.Directory{}
   363  
   364  	files, err := os.ReadDir(filepath.Join(execRoot, path))
   365  	if err != nil {
   366  		return nil, nil, fmt.Errorf("failed read directory: %v", err)
   367  	}
   368  
   369  	for _, file := range files {
   370  		fn := file.Name()
   371  		fp := filepath.Join(execRoot, path, fn)
   372  		if file.IsDir() {
   373  			root, _, err := BuildDir(fp, s, execRoot)
   374  			if err != nil {
   375  				return nil, nil, fmt.Errorf("failed to build directory tree: %v", err)
   376  			}
   377  			res.Directories = append(res.Directories, &repb.DirectoryNode{Name: fn, Digest: digest.TestNewFromMessage(root).ToProto()})
   378  			ch = append(ch, root)
   379  		} else {
   380  			content, err := os.ReadFile(fp)
   381  			if err != nil {
   382  				return nil, nil, fmt.Errorf("failed to read file: %v", err)
   383  			}
   384  			dg := s.CAS.Put(content)
   385  			res.Files = append(res.Files, &repb.FileNode{Name: fn, Digest: dg.ToProto()})
   386  		}
   387  	}
   388  	return res, ch, nil
   389  }
   390  
   391  // LogStream adds a new logstream that may be served from the bytestream API.
   392  type LogStream struct {
   393  	// Name is the name of the stream. The stream may be downloaded by fetching the resource named
   394  	// instance/logstreams/<name> from bytestream.
   395  	Name string
   396  	// Chunks is a list of the chunks that will be sent by bytestream.
   397  	Chunks []string
   398  }
   399  
   400  func (l *LogStream) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   401  	return s.LogStreams.Put(l.Name, l.Chunks...)
   402  }
   403  
   404  // StdOutStream causes the fake action to indicate this as the name of the stdout logstream.
   405  type StdOutStream string
   406  
   407  func (o StdOutStream) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   408  	s.Exec.StdOutStreamName = string(o)
   409  	return nil
   410  }
   411  
   412  // StdErrStream causes the fake action to indicate this as the name of the stderr logstream.
   413  type StdErrStream string
   414  
   415  func (e StdErrStream) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   416  	s.Exec.StdErrStreamName = string(e)
   417  	return nil
   418  }
   419  
   420  // StdOut is to be added as an output of the fake action.
   421  type StdOut string
   422  
   423  // Apply puts the action stdout in the fake CAS and the given ActionResult.
   424  func (o StdOut) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   425  	bytes := []byte(o)
   426  	s.Exec.OutputBlobs = append(s.Exec.OutputBlobs, bytes)
   427  	dg := s.CAS.Put(bytes)
   428  	ac.StdoutDigest = dg.ToProto()
   429  	return nil
   430  }
   431  
   432  // StdOutRaw is to be added as a raw output of the fake action.
   433  type StdOutRaw string
   434  
   435  // Apply puts the action stdout as raw bytes in the given ActionResult.
   436  func (o StdOutRaw) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   437  	ac.StdoutRaw = []byte(o)
   438  	return nil
   439  }
   440  
   441  // StdErr is to be added as an output of the fake action.
   442  type StdErr string
   443  
   444  // Apply puts the action stderr in the fake CAS and the given ActionResult.
   445  func (o StdErr) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   446  	bytes := []byte(o)
   447  	s.Exec.OutputBlobs = append(s.Exec.OutputBlobs, bytes)
   448  	dg := s.CAS.Put(bytes)
   449  	ac.StderrDigest = dg.ToProto()
   450  	return nil
   451  }
   452  
   453  // StdErrRaw is to be added as a raw output of the fake action.
   454  type StdErrRaw string
   455  
   456  // Apply puts the action stderr as raw bytes in the given ActionResult.
   457  func (o StdErrRaw) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   458  	ac.StderrRaw = []byte(o)
   459  	return nil
   460  }
   461  
   462  // ExecutionCacheHit of true will cause the ActionResult to be returned as a cache hit during
   463  // fake execution.
   464  type ExecutionCacheHit bool
   465  
   466  // Apply on true will cause the ActionResult to be returned as a cache hit during fake execution.
   467  func (c ExecutionCacheHit) apply(ac *repb.ActionResult, s *Server, execRoot string) error {
   468  	s.Exec.Cached = bool(c)
   469  	return nil
   470  }