github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/tool/tool_test.go (about) 1 package tool 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path" 8 "path/filepath" 9 "testing" 10 11 "github.com/bazelbuild/remote-apis-sdks/go/pkg/command" 12 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 13 "github.com/bazelbuild/remote-apis-sdks/go/pkg/fakes" 14 "github.com/bazelbuild/remote-apis-sdks/go/pkg/outerr" 15 "github.com/google/go-cmp/cmp" 16 "google.golang.org/protobuf/encoding/prototext" 17 18 cpb "github.com/bazelbuild/remote-apis-sdks/go/api/command" 19 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 20 ) 21 22 var fooProperties = &cpb.NodeProperties{Properties: []*cpb.NodeProperty{{Name: "fooName", Value: "fooValue"}}} 23 24 func TestTool_DownloadActionResult(t *testing.T) { 25 e, cleanup := fakes.NewTestEnv(t) 26 defer cleanup() 27 cmd := &command.Command{ 28 Args: []string{"tool"}, 29 ExecRoot: e.ExecRoot, 30 InputSpec: &command.InputSpec{}, 31 OutputFiles: []string{"a/b/out"}, 32 } 33 opt := command.DefaultExecutionOptions() 34 output := "output" 35 _, acDg, _, _ := e.Set(cmd, opt, &command.Result{Status: command.CacheHitResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: output}, 36 fakes.StdOut("stdout"), fakes.StdErr("stderr")) 37 38 toolClient := &Client{GrpcClient: e.Client.GrpcClient} 39 tmpDir := t.TempDir() 40 if err := toolClient.DownloadActionResult(context.Background(), acDg.String(), tmpDir); err != nil { 41 t.Fatalf("DownloadActionResult(%v,%v) failed: %v", acDg.String(), tmpDir, err) 42 } 43 verifyData := map[string]string{ 44 filepath.Join(tmpDir, "a/b/out"): "output", 45 filepath.Join(tmpDir, "stdout"): "stdout", 46 filepath.Join(tmpDir, "stderr"): "stderr", 47 } 48 for fp, want := range verifyData { 49 c, err := os.ReadFile(fp) 50 if err != nil { 51 t.Fatalf("Unable to read downloaded output file %v: %v", fp, err) 52 } 53 got := string(c) 54 if got != want { 55 t.Fatalf("Incorrect content in downloaded file %v, want %v, got %v", fp, want, got) 56 } 57 } 58 } 59 60 func TestTool_ShowAction(t *testing.T) { 61 e, cleanup := fakes.NewTestEnv(t) 62 defer cleanup() 63 cmd := &command.Command{ 64 Args: []string{"tool"}, 65 ExecRoot: e.ExecRoot, 66 InputSpec: &command.InputSpec{ 67 Inputs: []string{ 68 "a/b/input.txt", 69 "a/b/input2.txt", 70 }, 71 InputNodeProperties: map[string]*cpb.NodeProperties{"a/b/input2.txt": fooProperties}, 72 }, 73 OutputFiles: []string{"a/b/out"}, 74 } 75 76 opt := command.DefaultExecutionOptions() 77 _, acDg, _, _ := e.Set(cmd, opt, &command.Result{Status: command.CacheHitResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "output"}, 78 fakes.StdOut("stdout"), fakes.StdErr("stderr"), &fakes.InputFile{Path: "a/b/input.txt", Contents: "input"}, &fakes.InputFile{Path: "a/b/input2.txt", Contents: "input2"}) 79 80 toolClient := &Client{GrpcClient: e.Client.GrpcClient} 81 got, err := toolClient.ShowAction(context.Background(), acDg.String()) 82 if err != nil { 83 t.Fatalf("ShowAction(%v) failed: %v", acDg.String(), err) 84 } 85 want := fmt.Sprintf(`Command 86 ======= 87 Command Digest: 76a608e419da9ed3673f59b8b903f21dbf7cc3178281029151a090cac02d9e4d/15 88 tool 89 90 Platform 91 ======== 92 93 Inputs 94 ====== 95 [Root directory digest: 456e94a43b31b158fa7b3fe8d3a8cd6f0b66ef8a6a05ab8350e03df83b9740b6/75] 96 a/b/input.txt: [File digest: c96c6d5be8d08a12e7b5cdc1b207fa6b2430974c86803d8891675e76fd992c20/5] 97 a/b/input2.txt: [File digest: 124d8541ff3d7a18b95432bdfbecd86816b86c8265bff44ef629765afb25f06b/6] [Node properties: %s] 98 99 ------------------------------------------------------------------------ 100 Action Result 101 102 Exit code: 0 103 stdout digest: 63d42d26156fcc761e57da4128e9881d5bdf3bf933f0f6e9c93d6e26b9b90ae7/6 104 stderr digest: 7e6b710b765404cccbad9eedcff7615fc37b269d6db12cd81a58be541d93083c/6 105 106 Output Files 107 ============ 108 a/b/out, digest: e0ee8bb50685e05fa0f47ed04203ae953fdfd055f5bd2892ea186504254f8c3a/6 109 110 Output Files From Directories 111 ============================= 112 `, prototext.MarshalOptions{Multiline: false}.Format(fooProperties)) 113 if diff := cmp.Diff(want, got); diff != "" { 114 t.Fatalf("ShowAction(%v) returned diff (-want +got): %v\n\ngot: %v\n\nwant: %v\n", acDg.String(), diff, got, want) 115 } 116 } 117 118 func TestTool_CheckDeterminism(t *testing.T) { 119 e, cleanup := fakes.NewTestEnv(t) 120 defer cleanup() 121 cmd := &command.Command{ 122 Args: []string{"foo", "bar", "baz"}, 123 ExecRoot: e.ExecRoot, 124 InputSpec: &command.InputSpec{Inputs: []string{"i1", "i2"}}, 125 OutputFiles: []string{"a/b/out"}, 126 } 127 _, acDg, _, _ := e.Set(cmd, command.DefaultExecutionOptions(), &command.Result{Status: command.SuccessResultStatus}, &fakes.InputFile{Path: "i1", Contents: "i1"}, &fakes.InputFile{Path: "i2", Contents: "i2"}, &fakes.OutputFile{Path: "a/b/out", Contents: "out"}) 128 129 client := &Client{GrpcClient: e.Client.GrpcClient} 130 if err := client.CheckDeterminism(context.Background(), acDg.String(), "", 2); err != nil { 131 t.Errorf("CheckDeterminism returned an error: %v", err) 132 } 133 // Now execute again and return a different output. 134 testOnlyStartDeterminismExec = func() { 135 e.Set(cmd, command.DefaultExecutionOptions(), &command.Result{Status: command.SuccessResultStatus}, &fakes.InputFile{Path: "i1", Contents: "i1"}, &fakes.InputFile{Path: "i2", Contents: "i2"}, &fakes.OutputFile{Path: "a/b/out", Contents: "out2"}) 136 } 137 defer func() { testOnlyStartDeterminismExec = func() {} }() 138 if err := client.CheckDeterminism(context.Background(), acDg.String(), "", 2); err == nil { 139 t.Errorf("CheckDeterminism returned nil, want error") 140 } 141 } 142 143 func TestTool_DownloadAction(t *testing.T) { 144 e, cleanup := fakes.NewTestEnv(t) 145 defer cleanup() 146 cmd := &command.Command{ 147 Args: []string{"foo", "bar", "baz"}, 148 ExecRoot: e.ExecRoot, 149 InputSpec: &command.InputSpec{Inputs: []string{"i1", "a/b/i2"}, InputNodeProperties: map[string]*cpb.NodeProperties{"i1": fooProperties}}, 150 OutputFiles: []string{"a/b/out"}, 151 Platform: map[string]string{ 152 "container-image": "foo", 153 }, 154 } 155 _, acDg, _, _ := e.Set(cmd, command.DefaultExecutionOptions(), &command.Result{Status: command.SuccessResultStatus}, &fakes.InputFile{Path: "i1", Contents: "i1"}, &fakes.InputFile{Path: "a/b/i2", Contents: "i2"}) 156 157 client := &Client{GrpcClient: e.Client.GrpcClient} 158 tmpDir := filepath.Join(t.TempDir(), "action_root") 159 os.MkdirAll(tmpDir, os.ModePerm) 160 if err := client.DownloadAction(context.Background(), acDg.String(), tmpDir, true); err != nil { 161 t.Errorf("error DownloadAction: %v", err) 162 } 163 164 reCmdPb := &repb.Command{ 165 Arguments: []string{"foo", "bar", "baz"}, 166 OutputFiles: []string{"a/b/out"}, 167 Platform: &repb.Platform{ 168 Properties: []*repb.Platform_Property{ 169 &repb.Platform_Property{ 170 Name: "container-image", 171 Value: "foo", 172 }, 173 }, 174 }, 175 } 176 acPb := &repb.Action{ 177 CommandDigest: digest.TestNewFromMessage(reCmdPb).ToProto(), 178 InputRootDigest: &repb.Digest{Hash: "01dc5b6fa6d16d30bf80a7d88ff58f13fe801c4060b9782253b94e793444df05", SizeBytes: 176}, 179 } 180 ipPb := &cpb.InputSpec{InputNodeProperties: cmd.InputSpec.InputNodeProperties} 181 expectedContents := []struct { 182 path string 183 contents string 184 }{ 185 { 186 path: "ac.textproto", 187 contents: prototext.Format(acPb), 188 }, 189 { 190 path: "cmd.textproto", 191 contents: prototext.Format(reCmdPb), 192 }, 193 { 194 path: "input_node_properties.textproto", 195 contents: prototext.Format(ipPb), 196 }, 197 { 198 path: "input/i1", 199 contents: "i1", 200 }, 201 { 202 path: "input/a/b/i2", 203 contents: "i2", 204 }, 205 } 206 for _, ec := range expectedContents { 207 fp := filepath.Join(tmpDir, ec.path) 208 got, err := os.ReadFile(fp) 209 if err != nil { 210 t.Fatalf("Unable to read downloaded action file %v: %v", fp, err) 211 } 212 if diff := cmp.Diff(ec.contents, string(got)); diff != "" { 213 t.Errorf("Incorrect content in downloaded file %v: diff (-want +got): %v\n\ngot: %s\n\nwant: %v\n", fp, diff, got, ec.contents) 214 } 215 } 216 } 217 218 func TestTool_ExecuteAction(t *testing.T) { 219 e, cleanup := fakes.NewTestEnv(t) 220 defer cleanup() 221 cmd := &command.Command{ 222 Args: []string{"foo", "bar", "baz"}, 223 ExecRoot: e.ExecRoot, 224 InputSpec: &command.InputSpec{Inputs: []string{"i1", "i2"}, InputNodeProperties: map[string]*cpb.NodeProperties{"i1": fooProperties}}, 225 OutputFiles: []string{"a/b/out"}, 226 Platform: map[string]string{ 227 "container-image": "foo", 228 }, 229 } 230 opt := &command.ExecutionOptions{AcceptCached: false, DownloadOutputs: true, DownloadOutErr: true} 231 _, acDg, _, _ := e.Set(cmd, opt, &command.Result{Status: command.SuccessResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "out"}, 232 &fakes.InputFile{Path: "i1", Contents: "i1"}, &fakes.InputFile{Path: "i2", Contents: "i2"}, fakes.StdOut("stdout"), fakes.StdErr("stderr")) 233 234 client := &Client{GrpcClient: e.Client.GrpcClient} 235 oe := outerr.NewRecordingOutErr() 236 if _, err := client.ExecuteAction(context.Background(), acDg.String(), "", "", oe); err != nil { 237 t.Errorf("error executeAction: %v", err) 238 } 239 if string(oe.Stderr()) != "stderr" { 240 t.Errorf("Incorrect stderr %v, expected \"stderr\"", oe.Stderr()) 241 } 242 if string(oe.Stdout()) != "stdout" { 243 t.Errorf("Incorrect stdout %v, expected \"stdout\"", oe.Stdout()) 244 } 245 // Now execute again with changed inputs. 246 tmpDir := filepath.Join(t.TempDir(), "action_root") 247 os.MkdirAll(tmpDir, os.ModePerm) 248 inputRoot := filepath.Join(tmpDir, "input") 249 if err := client.DownloadAction(context.Background(), acDg.String(), tmpDir, true); err != nil { 250 t.Errorf("error DownloadAction: %v", err) 251 } 252 if err := os.WriteFile(filepath.Join(inputRoot, "i1"), []byte("i11"), 0644); err != nil { 253 t.Fatalf("failed overriding input file: %v", err) 254 } 255 if err := os.WriteFile(filepath.Join(inputRoot, "i2"), []byte("i22"), 0644); err != nil { 256 t.Fatalf("failed overriding input file: %v", err) 257 } 258 cmd.ExecRoot = inputRoot 259 _, acDg2, _, _ := e.Set(cmd, opt, &command.Result{Status: command.SuccessResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "out2"}, 260 fakes.StdOut("stdout2"), fakes.StdErr("stderr2")) 261 if diff := cmp.Diff(acDg, acDg2); diff == "" { 262 t.Fatalf("expected action digest to change after input change, got %v\n", acDg) 263 } 264 oe = outerr.NewRecordingOutErr() 265 if _, err := client.ExecuteAction(context.Background(), acDg2.String(), "", tmpDir, oe); err != nil { 266 t.Errorf("error executeAction: %v", err) 267 } 268 269 fp := filepath.Join(tmpDir, "a/b/out") 270 c, err := os.ReadFile(fp) 271 if err != nil { 272 t.Fatalf("Unable to read downloaded output %v: %v", fp, err) 273 } 274 if string(c) != "out2" { 275 t.Fatalf("Incorrect content in downloaded file %v, want \"out2\", got %s", fp, c) 276 } 277 if string(oe.Stderr()) != "stderr2" { 278 t.Errorf("Incorrect stderr %v, expected \"stderr2\"", oe.Stderr()) 279 } 280 if string(oe.Stdout()) != "stdout2" { 281 t.Errorf("Incorrect stdout %v, expected \"stdout2\"", oe.Stdout()) 282 } 283 // Now execute again without node properties. 284 fp = filepath.Join(tmpDir, "input_node_properties.textproto") 285 if err := os.Remove(fp); err != nil { 286 t.Fatalf("Unable to remove %v: %v", fp, err) 287 } 288 cmd.InputSpec.InputNodeProperties = nil 289 _, acDg3, _, _ := e.Set(cmd, opt, &command.Result{Status: command.SuccessResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "out3"}, 290 fakes.StdOut("stdout3"), fakes.StdErr("stderr3")) 291 if diff := cmp.Diff(acDg2, acDg3); diff == "" { 292 t.Errorf("Expected action digests to be different when input node properties change, got: %v\n", acDg2) 293 } 294 oe = outerr.NewRecordingOutErr() 295 if _, err := client.ExecuteAction(context.Background(), acDg3.String(), "", tmpDir, oe); err != nil { 296 t.Errorf("error executeAction: %v", err) 297 } 298 299 fp = filepath.Join(tmpDir, "a/b/out") 300 c, err = os.ReadFile(fp) 301 if err != nil { 302 t.Fatalf("Unable to read downloaded output %v: %v", fp, err) 303 } 304 if string(c) != "out3" { 305 t.Fatalf("Incorrect content in downloaded file %v, want \"out3\", got %s", fp, c) 306 } 307 if string(oe.Stderr()) != "stderr3" { 308 t.Errorf("Incorrect stderr %v, expected \"stderr3\"", oe.Stderr()) 309 } 310 if string(oe.Stdout()) != "stdout3" { 311 t.Errorf("Incorrect stdout %v, expected \"stdout3\"", oe.Stdout()) 312 } 313 } 314 315 func TestTool_ExecuteActionFromRoot(t *testing.T) { 316 e, cleanup := fakes.NewTestEnv(t) 317 defer cleanup() 318 cmd := &command.Command{ 319 Args: []string{"foo", "bar", "baz"}, 320 ExecRoot: e.ExecRoot, 321 InputSpec: &command.InputSpec{Inputs: []string{"i1", "i2"}, InputNodeProperties: map[string]*cpb.NodeProperties{"i1": fooProperties}}, 322 OutputFiles: []string{"a/b/out"}, 323 } 324 // Create files necessary for the fake 325 if err := os.WriteFile(filepath.Join(e.ExecRoot, "i1"), []byte("i1"), 0644); err != nil { 326 t.Fatalf("failed creating input file: %v", err) 327 } 328 if err := os.WriteFile(filepath.Join(e.ExecRoot, "i2"), []byte("i2"), 0644); err != nil { 329 t.Fatalf("failed creating input file: %v", err) 330 } 331 opt := &command.ExecutionOptions{AcceptCached: false, DownloadOutputs: false, DownloadOutErr: true} 332 e.Set(cmd, opt, &command.Result{Status: command.SuccessResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "out"}, 333 fakes.StdOut("stdout"), fakes.StdErr("stderr")) 334 335 client := &Client{GrpcClient: e.Client.GrpcClient} 336 oe := outerr.NewRecordingOutErr() 337 // Construct the action root 338 os.Mkdir(filepath.Join(e.ExecRoot, "input"), os.ModePerm) 339 if err := os.WriteFile(filepath.Join(e.ExecRoot, "input", "i1"), []byte("i1"), 0644); err != nil { 340 t.Fatalf("failed creating input file: %v", err) 341 } 342 if err := os.WriteFile(filepath.Join(e.ExecRoot, "input", "i2"), []byte("i2"), 0644); err != nil { 343 t.Fatalf("failed creating input file: %v", err) 344 } 345 ipPb := &cpb.InputSpec{ 346 InputNodeProperties: map[string]*cpb.NodeProperties{"i1": fooProperties}, 347 } 348 if err := os.WriteFile(filepath.Join(e.ExecRoot, "input_node_properties.textproto"), []byte(prototext.Format(ipPb)), 0644); err != nil { 349 t.Fatalf("failed creating input node properties file: %v", err) 350 } 351 reCmdPb := &repb.Command{ 352 Arguments: []string{"foo", "bar", "baz"}, 353 OutputFiles: []string{"a/b/out"}, 354 } 355 if err := os.WriteFile(filepath.Join(e.ExecRoot, "cmd.textproto"), []byte(prototext.Format(reCmdPb)), 0644); err != nil { 356 t.Fatalf("failed creating command file: %v", err) 357 } 358 // The tool will embed the Command proto digest into the Action proto, so the `command_digest` field is effectively ignored: 359 if err := os.WriteFile(filepath.Join(e.ExecRoot, "ac.textproto"), []byte(`command_digest: {hash: "whatever"}`), 0644); err != nil { 360 t.Fatalf("failed creating action file: %v", err) 361 } 362 if _, err := client.ExecuteAction(context.Background(), "", e.ExecRoot, "", oe); err != nil { 363 t.Errorf("error executeAction: %v", err) 364 } 365 if string(oe.Stderr()) != "stderr" { 366 t.Errorf("Incorrect stderr %v, expected \"stderr\"", string(oe.Stderr())) 367 } 368 if string(oe.Stdout()) != "stdout" { 369 t.Errorf("Incorrect stdout %v, expected \"stdout\"", oe.Stdout()) 370 } 371 } 372 373 func TestTool_DownloadBlob(t *testing.T) { 374 e, cleanup := fakes.NewTestEnv(t) 375 defer cleanup() 376 cas := e.Server.CAS 377 dg := cas.Put([]byte("hello")) 378 379 toolClient := &Client{GrpcClient: e.Client.GrpcClient} 380 got, err := toolClient.DownloadBlob(context.Background(), dg.String(), "") 381 if err != nil { 382 t.Fatalf("DownloadBlob(%v) failed: %v", dg.String(), err) 383 } 384 want := "hello" 385 if diff := cmp.Diff(want, got); diff != "" { 386 t.Fatalf("DownloadBlob(%v) returned diff (-want +got): %v\n\ngot: %v\n\nwant: %v\n", dg.String(), diff, got, want) 387 } 388 // Now download into a specified location. 389 tmpFile, err := os.CreateTemp(t.TempDir(), "") 390 if err != nil { 391 t.Fatalf("TempFile failed: %v", err) 392 } 393 if err := tmpFile.Close(); err != nil { 394 t.Fatalf("TempFile Close failed: %v", err) 395 } 396 fp := tmpFile.Name() 397 got, err = toolClient.DownloadBlob(context.Background(), dg.String(), fp) 398 if err != nil { 399 t.Fatalf("DownloadBlob(%v) failed: %v", dg.String(), err) 400 } 401 if got != "" { 402 t.Fatalf("DownloadBlob(%v) returned %v, expected empty: ", dg.String(), got) 403 } 404 c, err := os.ReadFile(fp) 405 if err != nil { 406 t.Fatalf("Unable to read downloaded output file %v: %v", fp, err) 407 } 408 got = string(c) 409 if got != want { 410 t.Fatalf("Incorrect content in downloaded file %v, want %v, got %v", fp, want, got) 411 } 412 } 413 414 func TestTool_UploadBlob(t *testing.T) { 415 e, cleanup := fakes.NewTestEnv(t) 416 defer cleanup() 417 cas := e.Server.CAS 418 419 tmpFile := path.Join(t.TempDir(), "blob") 420 if err := os.WriteFile(tmpFile, []byte("Hello, World!"), 0777); err != nil { 421 t.Fatalf("Could not create temp blob: %v", err) 422 } 423 424 dg, err := digest.NewFromFile(tmpFile) 425 if err != nil { 426 t.Fatalf("digest.NewFromFile('%v') failed: %v", tmpFile, err) 427 } 428 429 toolClient := &Client{GrpcClient: e.Client.GrpcClient} 430 if err := toolClient.UploadBlob(context.Background(), tmpFile); err != nil { 431 t.Fatalf("UploadBlob('%v', '%v') failed: %v", dg.String(), tmpFile, err) 432 } 433 434 // First request should upload the blob. 435 if cas.BlobWrites(dg) != 1 { 436 t.Fatalf("Expected 1 write for blob '%v', got %v", dg.String(), cas.BlobWrites(dg)) 437 } 438 439 // Retries should check whether the blob already exists and skip uploading if it does. 440 if err := toolClient.UploadBlob(context.Background(), tmpFile); err != nil { 441 t.Fatalf("UploadBlob('%v', '%v') failed: %v", dg.String(), tmpFile, err) 442 } 443 if cas.BlobWrites(dg) != 1 { 444 t.Fatalf("Expected 1 write for blob '%v', got %v", dg.String(), cas.BlobWrites(dg)) 445 } 446 }