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 }