github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/command/command_test.go (about) 1 package command 2 3 import ( 4 "errors" 5 "testing" 6 "time" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/google/go-cmp/cmp/cmpopts" 10 "google.golang.org/protobuf/proto" 11 12 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 13 ) 14 15 func TestStableId_SameCommands(t *testing.T) { 16 t.Parallel() 17 testcases := []struct { 18 label string 19 A, B *Command 20 }{ 21 { 22 label: "platform", 23 A: &Command{ 24 Platform: map[string]string{"a": "1", "b": "2", "c": "3"}, 25 }, 26 B: &Command{ 27 Platform: map[string]string{"c": "3", "b": "2", "a": "1"}, 28 }, 29 }, 30 { 31 label: "inputs", 32 A: &Command{ 33 InputSpec: &InputSpec{ 34 Inputs: []string{"a", "b", "c"}, 35 }, 36 }, 37 B: &Command{ 38 InputSpec: &InputSpec{ 39 Inputs: []string{"c", "b", "a"}, 40 }, 41 }, 42 }, 43 { 44 label: "output files", 45 A: &Command{ 46 OutputFiles: []string{"a", "b", "c"}, 47 }, 48 B: &Command{ 49 OutputFiles: []string{"c", "b", "a"}, 50 }, 51 }, 52 { 53 label: "output directories", 54 A: &Command{ 55 OutputDirs: []string{"a", "b", "c"}, 56 }, 57 B: &Command{ 58 OutputDirs: []string{"c", "b", "a"}, 59 }, 60 }, 61 { 62 label: "environment", 63 A: &Command{ 64 InputSpec: &InputSpec{ 65 EnvironmentVariables: map[string]string{"a": "1", "b": "2", "c": "3"}, 66 }, 67 }, 68 B: &Command{ 69 InputSpec: &InputSpec{ 70 EnvironmentVariables: map[string]string{"c": "3", "b": "2", "a": "1"}, 71 }, 72 }, 73 }, 74 { 75 label: "exclusions", 76 A: &Command{ 77 InputSpec: &InputSpec{ 78 InputExclusions: []*InputExclusion{ 79 {Regex: "a", Type: FileInputType}, 80 {Regex: "b", Type: DirectoryInputType}, 81 {Regex: "c", Type: UnspecifiedInputType}, 82 }, 83 }, 84 }, 85 B: &Command{ 86 InputSpec: &InputSpec{ 87 InputExclusions: []*InputExclusion{ 88 {Regex: "b", Type: DirectoryInputType}, 89 {Regex: "c", Type: UnspecifiedInputType}, 90 {Regex: "a", Type: FileInputType}, 91 }, 92 }, 93 }, 94 }, 95 } 96 for _, tc := range testcases { 97 aID := tc.A.stableID() 98 bID := tc.B.stableID() 99 if aID != bID { 100 t.Errorf("%s: stableID of %v = %s different from %v = %s", tc.label, tc.A, aID, tc.B, bID) 101 } 102 } 103 } 104 105 func TestStableId_DifferentCommands(t *testing.T) { 106 t.Parallel() 107 testcases := []struct { 108 label string 109 A, B *Command 110 }{ 111 { 112 label: "args", 113 A: &Command{Args: []string{"a", "b"}}, 114 B: &Command{Args: []string{"b", "a"}}, 115 }, 116 { 117 label: "exec root", 118 A: &Command{ExecRoot: "a"}, 119 B: &Command{ExecRoot: "b"}, 120 }, 121 { 122 label: "working dir", 123 A: &Command{WorkingDir: "a"}, 124 B: &Command{WorkingDir: "b"}, 125 }, 126 { 127 label: "output files", 128 A: &Command{OutputFiles: []string{"a", "b", "c"}}, 129 B: &Command{OutputFiles: []string{"c", "b", "c"}}, 130 }, 131 { 132 label: "output dirs", 133 A: &Command{OutputDirs: []string{"a", "b", "c"}}, 134 B: &Command{OutputDirs: []string{"c", "b", "c"}}, 135 }, 136 { 137 label: "platform", 138 A: &Command{Platform: map[string]string{"a": "1", "b": "2", "c": "3"}}, 139 B: &Command{Platform: map[string]string{"c": "3", "b": "2", "a": "10"}}, 140 }, 141 { 142 label: "inputs", 143 A: &Command{ 144 InputSpec: &InputSpec{ 145 Inputs: []string{"a", "b", "c"}, 146 }, 147 }, 148 B: &Command{ 149 InputSpec: &InputSpec{ 150 Inputs: []string{"c", "b", "a1"}, 151 }, 152 }, 153 }, 154 { 155 label: "environment", 156 A: &Command{ 157 InputSpec: &InputSpec{ 158 EnvironmentVariables: map[string]string{"a": "1", "b": "2", "c": "3"}, 159 }, 160 }, 161 B: &Command{ 162 InputSpec: &InputSpec{ 163 EnvironmentVariables: map[string]string{"c": "3", "b": "2", "a": "10"}, 164 }, 165 }, 166 }, 167 { 168 label: "exclusions", 169 A: &Command{ 170 InputSpec: &InputSpec{ 171 InputExclusions: []*InputExclusion{ 172 {Regex: "a", Type: FileInputType}, 173 {Regex: "b", Type: DirectoryInputType}, 174 {Regex: "c", Type: UnspecifiedInputType}, 175 }, 176 }, 177 }, 178 B: &Command{ 179 InputSpec: &InputSpec{ 180 InputExclusions: []*InputExclusion{ 181 {Regex: "b", Type: UnspecifiedInputType}, 182 {Regex: "c", Type: UnspecifiedInputType}, 183 {Regex: "a", Type: FileInputType}, 184 }, 185 }, 186 }, 187 }, 188 } 189 for _, tc := range testcases { 190 aID := tc.A.stableID() 191 bID := tc.B.stableID() 192 if aID == bID { 193 t.Errorf("%s: stableID of %v = %s is same as %v", tc.label, tc.A, aID, tc.B) 194 } 195 } 196 } 197 198 func TestFillDefaultFieldValues_Empty(t *testing.T) { 199 t.Parallel() 200 c := &Command{} 201 c.FillDefaultFieldValues() 202 if c.Identifiers == nil { 203 t.Fatal("{}.Identifiers = nil, expected filled") 204 } 205 206 if c.Identifiers.CommandID == "" { 207 t.Errorf("did not fill command id for empty command") 208 } 209 if c.Identifiers.ToolName == "" { 210 t.Errorf("did not fill tool name for empty command, expected \"remote-client\"") 211 } 212 if c.Identifiers.InvocationID == "" { 213 t.Errorf("did not generate invocation id for empty command") 214 } 215 if c.InputSpec == nil { 216 t.Errorf("did not generate input spec for empty command") 217 } 218 } 219 220 func TestFillDefaultFieldValues_PreserveExisting(t *testing.T) { 221 t.Parallel() 222 ids := &Identifiers{ 223 CommandID: "bla", 224 ToolName: "foo", 225 InvocationID: "bar", 226 } 227 inputSpec := &InputSpec{} 228 c := &Command{InputSpec: inputSpec, Identifiers: ids} 229 c.FillDefaultFieldValues() 230 if c.Identifiers != ids { 231 t.Fatal("command.Identifiers address not preserved") 232 } 233 234 if c.Identifiers.CommandID != "bla" { 235 t.Errorf("did not preserve CommandID: got %s, expected bla", c.Identifiers.CommandID) 236 } 237 if c.Identifiers.ToolName != "foo" { 238 t.Errorf("did not preserve CommandID: got %s, expected foo", c.Identifiers.ToolName) 239 } 240 if c.Identifiers.InvocationID != "bar" { 241 t.Errorf("did not preserve CommandID: got %s, expected bar", c.Identifiers.InvocationID) 242 } 243 if c.InputSpec != inputSpec { 244 t.Fatal("command.InputSpec address not preserved") 245 } 246 } 247 248 func TestValidate_Errors(t *testing.T) { 249 t.Parallel() 250 testcases := []struct { 251 label string 252 Command *Command 253 }{ 254 { 255 label: "missing args", 256 Command: &Command{ 257 Identifiers: &Identifiers{}, 258 ExecRoot: "a", 259 InputSpec: &InputSpec{}, 260 }, 261 }, 262 { 263 label: "missing input spec", 264 Command: &Command{ 265 Identifiers: &Identifiers{}, 266 Args: []string{"a"}, 267 ExecRoot: "a", 268 }, 269 }, 270 { 271 label: "missing exec root", 272 Command: &Command{ 273 Identifiers: &Identifiers{}, 274 Args: []string{"a"}, 275 InputSpec: &InputSpec{}, 276 }, 277 }, 278 { 279 label: "missing identifiers", 280 Command: &Command{ 281 Args: []string{"a"}, 282 InputSpec: &InputSpec{}, 283 ExecRoot: "a", 284 }, 285 }, 286 { 287 label: "mismatch between local and remote working dir depth", 288 Command: &Command{ 289 Identifiers: &Identifiers{}, 290 Args: []string{"a"}, 291 ExecRoot: "a", 292 InputSpec: &InputSpec{}, 293 WorkingDir: "foo", 294 RemoteWorkingDir: "bar/baz", 295 }, 296 }, 297 } 298 for _, tc := range testcases { 299 if err := tc.Command.Validate(); err == nil { 300 t.Errorf("%s: expected Validate of %v to error, got nil", tc.label, tc.Command) 301 } 302 } 303 } 304 305 func TestValidate_NilSuccess(t *testing.T) { 306 t.Parallel() 307 var c *Command 308 if err := c.Validate(); err != nil { 309 t.Errorf("expected Validate of nil = nil, got %v", err) 310 } 311 } 312 313 func TestValidate_Success(t *testing.T) { 314 t.Parallel() 315 c := &Command{ 316 Identifiers: &Identifiers{}, 317 Args: []string{"a"}, 318 ExecRoot: "a", 319 InputSpec: &InputSpec{}, 320 } 321 if err := c.Validate(); err != nil { 322 t.Errorf("expected Validate of %v = nil, got %v", c, err) 323 } 324 } 325 326 func TestToREProto(t *testing.T) { 327 tests := []struct { 328 name string 329 cmd *Command 330 wantCmd *repb.Command 331 }{ 332 { 333 name: "pass args", 334 cmd: &Command{Args: []string{"foo", "bar"}}, 335 wantCmd: &repb.Command{Arguments: []string{"foo", "bar"}}, 336 }, 337 { 338 name: "pass working directory", 339 cmd: &Command{WorkingDir: "a/b"}, 340 wantCmd: &repb.Command{WorkingDirectory: "a/b"}, 341 }, 342 { 343 name: "sort output files", 344 cmd: &Command{OutputFiles: []string{"foo", "bar", "abc"}}, 345 wantCmd: &repb.Command{OutputFiles: []string{"abc", "bar", "foo"}}, 346 }, 347 { 348 name: "sort output directories", 349 cmd: &Command{OutputDirs: []string{"foo", "bar", "abc"}}, 350 wantCmd: &repb.Command{OutputDirectories: []string{"abc", "bar", "foo"}}, 351 }, 352 { 353 name: "sort environment variables", 354 cmd: &Command{ 355 InputSpec: &InputSpec{ 356 EnvironmentVariables: map[string]string{"b": "3", "a": "2", "c": "1"}, 357 }, 358 }, 359 wantCmd: &repb.Command{ 360 EnvironmentVariables: []*repb.Command_EnvironmentVariable{ 361 &repb.Command_EnvironmentVariable{Name: "a", Value: "2"}, 362 &repb.Command_EnvironmentVariable{Name: "b", Value: "3"}, 363 &repb.Command_EnvironmentVariable{Name: "c", Value: "1"}, 364 }, 365 }, 366 }, 367 { 368 name: "sort platform", 369 cmd: &Command{Platform: map[string]string{"b": "3", "a": "2", "c": "1"}}, 370 wantCmd: &repb.Command{ 371 Platform: &repb.Platform{ 372 Properties: []*repb.Platform_Property{ 373 &repb.Platform_Property{Name: "a", Value: "2"}, 374 &repb.Platform_Property{Name: "b", Value: "3"}, 375 &repb.Platform_Property{Name: "c", Value: "1"}, 376 }, 377 }, 378 }, 379 }, 380 } 381 for _, tc := range tests { 382 t.Run(tc.name, func(t *testing.T) { 383 tc.cmd.FillDefaultFieldValues() 384 gotCmd := tc.cmd.ToREProto(false) 385 if diff := cmp.Diff(tc.wantCmd, gotCmd, cmpopts.EquateEmpty(), cmp.Comparer(proto.Equal)); diff != "" { 386 t.Errorf("%s: buildCommand gave result diff (-want +got):\n%s", tc.name, diff) 387 } 388 }) 389 } 390 } 391 392 func TestToREProtoWithOutputPaths(t *testing.T) { 393 tests := []struct { 394 name string 395 cmd *Command 396 wantCmd *repb.Command 397 }{ 398 { 399 name: "pass args", 400 cmd: &Command{Args: []string{"foo", "bar"}}, 401 wantCmd: &repb.Command{Arguments: []string{"foo", "bar"}}, 402 }, 403 { 404 name: "pass working directory", 405 cmd: &Command{WorkingDir: "a/b"}, 406 wantCmd: &repb.Command{WorkingDirectory: "a/b"}, 407 }, 408 { 409 name: "sort output files", 410 cmd: &Command{OutputFiles: []string{"foo", "bar", "abc"}}, 411 wantCmd: &repb.Command{OutputPaths: []string{"abc", "bar", "foo"}}, 412 }, 413 { 414 name: "sort output directories", 415 cmd: &Command{OutputDirs: []string{"foo", "bar", "abc"}}, 416 wantCmd: &repb.Command{OutputPaths: []string{"abc", "bar", "foo"}}, 417 }, 418 { 419 name: "sort environment variables", 420 cmd: &Command{ 421 InputSpec: &InputSpec{ 422 EnvironmentVariables: map[string]string{"b": "3", "a": "2", "c": "1"}, 423 }, 424 }, 425 wantCmd: &repb.Command{ 426 EnvironmentVariables: []*repb.Command_EnvironmentVariable{ 427 &repb.Command_EnvironmentVariable{Name: "a", Value: "2"}, 428 &repb.Command_EnvironmentVariable{Name: "b", Value: "3"}, 429 &repb.Command_EnvironmentVariable{Name: "c", Value: "1"}, 430 }, 431 }, 432 }, 433 { 434 name: "sort platform", 435 cmd: &Command{Platform: map[string]string{"b": "3", "a": "2", "c": "1"}}, 436 wantCmd: &repb.Command{ 437 Platform: &repb.Platform{ 438 Properties: []*repb.Platform_Property{ 439 &repb.Platform_Property{Name: "a", Value: "2"}, 440 &repb.Platform_Property{Name: "b", Value: "3"}, 441 &repb.Platform_Property{Name: "c", Value: "1"}, 442 }, 443 }, 444 }, 445 }, 446 } 447 for _, tc := range tests { 448 t.Run(tc.name, func(t *testing.T) { 449 tc.cmd.FillDefaultFieldValues() 450 gotCmd := tc.cmd.ToREProto(true) 451 if diff := cmp.Diff(tc.wantCmd, gotCmd, cmpopts.EquateEmpty(), cmp.Comparer(proto.Equal)); diff != "" { 452 t.Errorf("%s: buildCommand gave result diff (-want +got):\n%s", tc.name, diff) 453 } 454 }) 455 } 456 } 457 458 func TestToFromProto(t *testing.T) { 459 cmd := &Command{ 460 Identifiers: &Identifiers{ 461 CommandID: "a", 462 InvocationID: "b", 463 ToolName: "c", 464 }, 465 Args: []string{"a", "b", "c"}, 466 ExecRoot: "/exec/root", 467 InputSpec: &InputSpec{ 468 Inputs: []string{"foo.h", "bar.h"}, 469 VirtualInputs: []*VirtualInput{ 470 &VirtualInput{ 471 Path: "empty_file", 472 IsExecutable: true, 473 Mtime: time.Unix(1711556358, 123456789), 474 }, 475 &VirtualInput{ 476 Path: "foo/empty_dir", 477 IsEmptyDirectory: true, 478 Mtime: time.Unix(1711556358, 123456789), 479 }, 480 &VirtualInput{ 481 Path: "foo/bar", 482 Contents: []byte("bar-contents"), 483 Mtime: time.Unix(1711556358, 123456789), 484 }, 485 }, 486 InputExclusions: []*InputExclusion{ 487 &InputExclusion{ 488 Regex: "*.bla", 489 Type: DirectoryInputType, 490 }, 491 &InputExclusion{ 492 Regex: "*.blo", 493 Type: FileInputType, 494 }, 495 }, 496 EnvironmentVariables: map[string]string{ 497 "k": "v", 498 "k1": "v1", 499 }, 500 SymlinkBehavior: ResolveSymlink, 501 }, 502 OutputFiles: []string{"a/b/out"}, 503 } 504 gotCmd := FromProto(ToProto(cmd)) 505 if diff := cmp.Diff(cmd, gotCmd, cmpopts.EquateEmpty()); diff != "" { 506 t.Errorf("FromProto(ToProto()) returned diff in result: (-want +got)\n%s", diff) 507 } 508 } 509 510 func TestResultToFromProto(t *testing.T) { 511 res := &Result{ 512 Status: CacheHitResultStatus, 513 ExitCode: 42, 514 Err: errors.New("message"), 515 } 516 gotRes := ResultFromProto(ResultToProto(res)) 517 if diff := cmp.Diff(res, gotRes, cmpopts.IgnoreFields(Result{}, "Err")); diff != "" { 518 t.Errorf("ResultFromProto(ResultToProto()) returned diff in result: (-want +got)\n%s", diff) 519 } 520 if res.Err.Error() != gotRes.Err.Error() { 521 t.Errorf("ResultFromProto(ResultToProto()) returned diff in error: want %v, got %v", res.Err, gotRes.Err) 522 } 523 } 524 525 func TestTimeIntervalToFromProto(t *testing.T) { 526 ti := &TimeInterval{ 527 From: time.Now(), 528 To: time.Now(), 529 } 530 gotTi := TimeIntervalFromProto(TimeIntervalToProto(ti)) 531 if diff := cmp.Diff(ti, gotTi); diff != "" { 532 t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned diff in result: (-want +got)\n%s", diff) 533 } 534 ti = &TimeInterval{From: time.Now()} 535 gotTi = TimeIntervalFromProto(TimeIntervalToProto(ti)) 536 if diff := cmp.Diff(ti, gotTi); diff != "" { 537 t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned diff in result: (-want +got)\n%s", diff) 538 } 539 ti = &TimeInterval{To: time.Now()} 540 gotTi = TimeIntervalFromProto(TimeIntervalToProto(ti)) 541 if diff := cmp.Diff(ti, gotTi); diff != "" { 542 t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned diff in result: (-want +got)\n%s", diff) 543 } 544 ti = &TimeInterval{} 545 gotTi = TimeIntervalFromProto(TimeIntervalToProto(ti)) 546 if diff := cmp.Diff(ti, gotTi); diff != "" { 547 t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned diff in result: (-want +got)\n%s", diff) 548 } 549 gotTi = TimeIntervalFromProto(TimeIntervalToProto(nil)) 550 if gotTi != nil { 551 t.Errorf("TimeIntervalFromProto(TimeIntervalToProto()) returned %v, wanted nil", gotTi) 552 } 553 }