github.com/saucelabs/saucectl@v0.175.1/internal/xcuitest/config_test.go (about) 1 package xcuitest 2 3 import ( 4 "errors" 5 "os" 6 "path/filepath" 7 "reflect" 8 "testing" 9 10 "github.com/saucelabs/saucectl/internal/config" 11 "github.com/saucelabs/saucectl/internal/insights" 12 13 "github.com/google/go-cmp/cmp" 14 "github.com/stretchr/testify/assert" 15 "gotest.tools/v3/fs" 16 ) 17 18 func TestTestOptions_ToMap(t *testing.T) { 19 opts := TestOptions{ 20 Class: []string{}, 21 NotClass: []string{}, 22 TestLanguage: "", 23 TestRegion: "", 24 TestTimeoutsEnabled: "", 25 MaximumTestExecutionTimeAllowance: 20, 26 DefaultTestExecutionTimeAllowance: 0, 27 StatusBarOverrideTime: "", 28 } 29 wantLength := 8 30 31 m := opts.ToMap() 32 33 if len(m) != wantLength { 34 t.Errorf("Length of converted TestOptions should match original, got (%v) want (%v)", len(m), wantLength) 35 } 36 37 v := reflect.ValueOf(m["maximumTestExecutionTimeAllowance"]) 38 vtype := v.Type() 39 if vtype.Kind() != reflect.String { 40 t.Errorf("ints should be converted to strings when mapping, got (%v) want (%v)", vtype, reflect.String) 41 } 42 43 if v := m["defaultTestExecutionTimeAllowance"]; v != "" { 44 t.Errorf("0 values should be cast to empty strings, got (%v)", v) 45 } 46 } 47 48 func TestValidate(t *testing.T) { 49 dir := fs.NewDir(t, "xcuitest-config", 50 fs.WithFile("test.ipa", "", fs.WithMode(0655)), 51 fs.WithFile("testApp.ipa", "", fs.WithMode(0655)), 52 fs.WithDir("test.app", fs.WithMode(0755)), 53 fs.WithDir("testApp.app", fs.WithMode(0755))) 54 defer dir.Remove() 55 appF := filepath.Join(dir.Path(), "test.ipa") 56 testAppF := filepath.Join(dir.Path(), "testApp.ipa") 57 appD := filepath.Join(dir.Path(), "test.app") 58 testAppD := filepath.Join(dir.Path(), "testApp.app") 59 60 testCases := []struct { 61 name string 62 p *Project 63 expectedErr error 64 }{ 65 { 66 name: "validating throws error on empty app", 67 p: &Project{ 68 Sauce: config.SauceConfig{Region: "us-west-1"}, 69 Suites: []Suite{ 70 { 71 Name: "suite with missing app", 72 Devices: []config.Device{ 73 {Name: "iPhone.*"}, 74 }, 75 }, 76 }, 77 }, 78 expectedErr: errors.New("missing path to app .ipa"), 79 }, 80 { 81 name: "validating passing with .ipa", 82 p: &Project{ 83 Sauce: config.SauceConfig{Region: "us-west-1"}, 84 Suites: []Suite{ 85 { 86 Name: "iphone", 87 App: appF, 88 TestApp: testAppF, 89 Devices: []config.Device{ 90 {Name: "iPhone.*"}, 91 }, 92 }, 93 }, 94 }, 95 expectedErr: nil, 96 }, 97 { 98 name: "validating passing with .app", 99 p: &Project{ 100 Sauce: config.SauceConfig{Region: "us-west-1"}, 101 Suites: []Suite{ 102 { 103 Name: "iphone", 104 App: appD, 105 TestApp: testAppD, 106 Devices: []config.Device{ 107 {Name: "iPhone.*"}, 108 }, 109 }, 110 }, 111 }, 112 expectedErr: nil, 113 }, 114 { 115 name: "validating error with app other than .ipa / .app", 116 p: &Project{ 117 Sauce: config.SauceConfig{Region: "us-west-1"}, 118 Suites: []Suite{ 119 { 120 Name: "suite with invalid apps", 121 App: "/path/to/app.zip", 122 TestApp: testAppD, 123 Devices: []config.Device{ 124 {Name: "iPhone.*"}, 125 }, 126 }, 127 }, 128 }, 129 expectedErr: errors.New("invalid application file: /path/to/app.zip, make sure extension is one of the following: .app, .ipa"), 130 }, 131 { 132 name: "validating error with test app other than .ipa / .app", 133 p: &Project{ 134 Sauce: config.SauceConfig{Region: "us-west-1"}, 135 Suites: []Suite{ 136 { 137 App: appF, 138 TestApp: "/path/to/app.zip", 139 Devices: []config.Device{ 140 {Name: "iPhone.*"}, 141 }, 142 }, 143 }, 144 }, 145 expectedErr: errors.New("invalid test application file: /path/to/app.zip, make sure extension is one of the following: .app, .ipa"), 146 }, 147 { 148 name: "validating throws error on empty testApp", 149 p: &Project{ 150 Sauce: config.SauceConfig{Region: "us-west-1"}, 151 Suites: []Suite{ 152 { 153 Name: "missing test app", 154 App: appF, 155 TestApp: "", 156 Devices: []config.Device{ 157 {Name: "iPhone.*"}, 158 }, 159 }, 160 }, 161 }, 162 expectedErr: errors.New("missing path to test app .ipa"), 163 }, 164 { 165 name: "validating throws error on not test app .ipa", 166 p: &Project{ 167 Sauce: config.SauceConfig{Region: "us-west-1"}, 168 Suites: []Suite{ 169 { 170 App: appF, 171 TestApp: "/path/to/bundle/tests", 172 Devices: []config.Device{ 173 {Name: "iPhone.*"}, 174 }, 175 }, 176 }, 177 }, 178 expectedErr: errors.New("invalid test application file: /path/to/bundle/tests, make sure extension is one of the following: .app, .ipa"), 179 }, 180 { 181 name: "validating throws error on missing suites", 182 p: &Project{ 183 Sauce: config.SauceConfig{Region: "us-west-1"}, 184 Xcuitest: Xcuitest{ 185 App: appF, 186 TestApp: testAppF, 187 }, 188 }, 189 expectedErr: errors.New("no suites defined"), 190 }, 191 { 192 name: "validating throws error on missing devices", 193 p: &Project{ 194 Sauce: config.SauceConfig{Region: "us-west-1"}, 195 Suites: []Suite{ 196 { 197 Name: "no devices", 198 App: appF, 199 TestApp: testAppF, 200 Devices: []config.Device{}, 201 }, 202 }, 203 }, 204 expectedErr: errors.New("missing devices configuration for suite: no devices"), 205 }, 206 { 207 name: "validating throws error on missing device name", 208 p: &Project{ 209 Sauce: config.SauceConfig{Region: "us-west-1"}, 210 Suites: []Suite{ 211 { 212 Name: "no device name", 213 App: appF, 214 TestApp: testAppF, 215 Devices: []config.Device{ 216 { 217 Name: "", 218 }, 219 }, 220 }, 221 }, 222 }, 223 expectedErr: errors.New("missing device name or ID for suite: no device name. Devices index: 0"), 224 }, 225 { 226 name: "validating throws error on unsupported device type", 227 p: &Project{ 228 Sauce: config.SauceConfig{Region: "us-west-1"}, 229 Suites: []Suite{ 230 { 231 Name: "unsupported device type", 232 App: appF, 233 TestApp: testAppF, 234 Devices: []config.Device{ 235 { 236 Name: "iPhone 11", 237 PlatformName: "iOS", 238 Options: config.DeviceOptions{ 239 DeviceType: "some", 240 }, 241 }, 242 }, 243 }, 244 }, 245 }, 246 expectedErr: errors.New("deviceType: some is unsupported for suite: unsupported device type. Devices index: 0. Supported device types: ANY,PHONE,TABLET"), 247 }, 248 { 249 name: "throws error if devices and simulators are defined", 250 p: &Project{ 251 Sauce: config.SauceConfig{Region: "us-west-1"}, 252 Suites: []Suite{ 253 { 254 Name: "", 255 App: appF, 256 TestApp: testAppF, 257 Simulators: []config.Simulator{ 258 { 259 Name: "iPhone 12 Simulator", 260 PlatformName: "iOS", 261 PlatformVersions: []string{"16.2"}, 262 }, 263 }, 264 Devices: []config.Device{ 265 { 266 Name: "iPhone 11", 267 PlatformName: "iOS", 268 Options: config.DeviceOptions{ 269 DeviceType: "some", 270 }, 271 }, 272 }, 273 }, 274 }, 275 }, 276 expectedErr: errors.New("suite cannot have both simulators and devices"), 277 }, 278 } 279 for _, tc := range testCases { 280 t.Run(tc.name, func(t *testing.T) { 281 err := Validate(*tc.p) 282 if tc.expectedErr == nil && err != nil { 283 t.Errorf("want: %v, got: %v", tc.expectedErr, err) 284 } 285 if tc.expectedErr != nil && tc.expectedErr.Error() != err.Error() { 286 t.Errorf("want: %v, got: %v", tc.expectedErr, err) 287 } 288 }) 289 } 290 } 291 292 func TestFromFile(t *testing.T) { 293 dir := fs.NewDir(t, "xcuitest-cfg", 294 fs.WithFile("config.yml", `apiVersion: v1alpha 295 kind: xcuitest 296 xcuitest: 297 app: "./tests/apps/xcuitest/SauceLabs.Mobile.Sample.XCUITest.App.ipa" 298 testApp: "./tests/apps/xcuitest/SwagLabsMobileAppUITests-Runner.ipa" 299 suites: 300 - name: "saucy barista" 301 devices: 302 - name: "iPhone XR" 303 platformVersion: "14.3" 304 testOptions: 305 class: ["SwagLabsMobileAppUITests.LoginTests/testSuccessfulLogin", "SwagLabsMobileAppUITests.LoginTests"] 306 `, fs.WithMode(0655))) 307 defer dir.Remove() 308 309 cfg, err := FromFile(filepath.Join(dir.Path(), "config.yml")) 310 if err != nil { 311 t.Errorf("expected error: %v, got: %v", nil, err) 312 } 313 expected := Project{ 314 Xcuitest: Xcuitest{ 315 App: "./tests/apps/xcuitest/SauceLabs.Mobile.Sample.XCUITest.App.ipa", 316 TestApp: "./tests/apps/xcuitest/SwagLabsMobileAppUITests-Runner.ipa", 317 }, 318 Suites: []Suite{ 319 { 320 Name: "saucy barista", 321 Devices: []config.Device{ 322 { 323 Name: "iPhone XR", 324 PlatformVersion: "14.3", 325 }, 326 }, 327 TestOptions: TestOptions{ 328 Class: []string{ 329 "SwagLabsMobileAppUITests.LoginTests/testSuccessfulLogin", 330 "SwagLabsMobileAppUITests.LoginTests", 331 }, 332 }, 333 }, 334 }, 335 } 336 if !reflect.DeepEqual(cfg.Xcuitest, expected.Xcuitest) { 337 t.Errorf("expected: %v, got: %v", expected, cfg) 338 } 339 if !reflect.DeepEqual(cfg.Suites, expected.Suites) { 340 t.Errorf("expected: %v, got: %v", expected, cfg) 341 } 342 } 343 344 func TestSetDefaults_Platform(t *testing.T) { 345 type args struct { 346 Device config.Device 347 } 348 tests := []struct { 349 name string 350 args args 351 want string 352 }{ 353 { 354 name: "no platform specified", 355 args: args{Device: config.Device{}}, 356 want: "iOS", 357 }, 358 { 359 name: "wrong platform specified", 360 args: args{Device: config.Device{PlatformName: "myOS"}}, 361 want: "iOS", 362 }, 363 } 364 365 for _, tt := range tests { 366 t.Run(tt.name, func(t *testing.T) { 367 p := Project{Suites: []Suite{{ 368 Devices: []config.Device{tt.args.Device}, 369 }}} 370 371 SetDefaults(&p) 372 373 got := p.Suites[0].Devices[0].PlatformName 374 if got != tt.want { 375 t.Errorf("SetDefaults() got: %v, want: %v", got, tt.want) 376 } 377 }) 378 } 379 } 380 381 func TestSetDefaults_DeviceType(t *testing.T) { 382 type args struct { 383 Device config.Device 384 } 385 tests := []struct { 386 name string 387 args args 388 want string 389 }{ 390 { 391 name: "device type is always uppercase", 392 args: args{Device: config.Device{Options: config.DeviceOptions{DeviceType: "phone"}}}, 393 want: "PHONE", 394 }, 395 } 396 397 for _, tt := range tests { 398 t.Run(tt.name, func(t *testing.T) { 399 p := Project{Suites: []Suite{{ 400 Devices: []config.Device{tt.args.Device}, 401 }}} 402 403 SetDefaults(&p) 404 405 got := p.Suites[0].Devices[0].Options.DeviceType 406 if got != tt.want { 407 t.Errorf("SetDefaults() got: %v, want: %v", got, tt.want) 408 } 409 }) 410 } 411 } 412 413 func TestSetDefaults_TestApp(t *testing.T) { 414 testCase := []struct { 415 name string 416 project Project 417 expResult string 418 }{ 419 { 420 name: "Set TestApp on suite level", 421 project: Project{ 422 Xcuitest: Xcuitest{ 423 TestApp: "test-app", 424 }, 425 Suites: []Suite{ 426 { 427 TestApp: "suite-test-app", 428 }, 429 }, 430 }, 431 expResult: "suite-test-app", 432 }, 433 { 434 name: "Set empty TestApp on suite level", 435 project: Project{ 436 Xcuitest: Xcuitest{ 437 TestApp: "test-app", 438 }, 439 Suites: []Suite{ 440 {}, 441 }, 442 }, 443 expResult: "test-app", 444 }, 445 } 446 for _, tc := range testCase { 447 t.Run(tc.name, func(t *testing.T) { 448 SetDefaults(&tc.project) 449 assert.Equal(t, tc.expResult, tc.project.Suites[0].TestApp) 450 }) 451 } 452 } 453 454 func TestXCUITest_SortByHistory(t *testing.T) { 455 testCases := []struct { 456 name string 457 suites []Suite 458 history insights.JobHistory 459 expRes []Suite 460 }{ 461 { 462 name: "sort suites by job history", 463 suites: []Suite{ 464 Suite{Name: "suite 1"}, 465 Suite{Name: "suite 2"}, 466 Suite{Name: "suite 3"}, 467 }, 468 history: insights.JobHistory{ 469 TestCases: []insights.TestCase{ 470 insights.TestCase{Name: "suite 2"}, 471 insights.TestCase{Name: "suite 1"}, 472 insights.TestCase{Name: "suite 3"}, 473 }, 474 }, 475 expRes: []Suite{ 476 Suite{Name: "suite 2"}, 477 Suite{Name: "suite 1"}, 478 Suite{Name: "suite 3"}, 479 }, 480 }, 481 { 482 name: "suites is the subset of job history", 483 suites: []Suite{ 484 Suite{Name: "suite 1"}, 485 Suite{Name: "suite 2"}, 486 }, 487 history: insights.JobHistory{ 488 TestCases: []insights.TestCase{ 489 insights.TestCase{Name: "suite 2"}, 490 insights.TestCase{Name: "suite 1"}, 491 insights.TestCase{Name: "suite 3"}, 492 }, 493 }, 494 expRes: []Suite{ 495 Suite{Name: "suite 2"}, 496 Suite{Name: "suite 1"}, 497 }, 498 }, 499 { 500 name: "job history is the subset of suites", 501 suites: []Suite{ 502 Suite{Name: "suite 1"}, 503 Suite{Name: "suite 2"}, 504 Suite{Name: "suite 3"}, 505 Suite{Name: "suite 4"}, 506 Suite{Name: "suite 5"}, 507 }, 508 history: insights.JobHistory{ 509 TestCases: []insights.TestCase{ 510 insights.TestCase{Name: "suite 2"}, 511 insights.TestCase{Name: "suite 1"}, 512 insights.TestCase{Name: "suite 3"}, 513 }, 514 }, 515 expRes: []Suite{ 516 Suite{Name: "suite 2"}, 517 Suite{Name: "suite 1"}, 518 Suite{Name: "suite 3"}, 519 Suite{Name: "suite 4"}, 520 Suite{Name: "suite 5"}, 521 }, 522 }, 523 } 524 525 for _, tc := range testCases { 526 t.Run(tc.name, func(t *testing.T) { 527 result := SortByHistory(tc.suites, tc.history) 528 for i := 0; i < len(result); i++ { 529 assert.Equal(t, tc.expRes[i].Name, result[i].Name) 530 } 531 }) 532 } 533 } 534 535 func TestXCUITest_ShardSuites(t *testing.T) { 536 testCases := []struct { 537 name string 538 project Project 539 content string 540 configEnabled bool 541 expSuites []Suite 542 expErr bool 543 }{ 544 { 545 name: "should keep original test options when sharding is disabled", 546 project: Project{ 547 Suites: []Suite{ 548 { 549 Name: "no shard", 550 TestOptions: TestOptions{ 551 Class: []string{"no update"}, 552 }, 553 }, 554 }, 555 }, 556 expSuites: []Suite{ 557 { 558 Name: "no shard", 559 TestOptions: TestOptions{ 560 Class: []string{"no update"}, 561 }, 562 }, 563 }, 564 }, 565 { 566 name: "should shard tests by ccy when sharding is enabled", 567 project: Project{ 568 Sauce: config.SauceConfig{ 569 Concurrency: 2, 570 }, 571 Suites: []Suite{ 572 { 573 Name: "sharding test", 574 Shard: "concurrency", 575 }, 576 }, 577 }, 578 content: "test1\ntest2\n", 579 configEnabled: true, 580 expSuites: []Suite{ 581 { 582 Name: "sharding test - 1/2", 583 TestOptions: TestOptions{ 584 Class: []string{"test1"}, 585 }, 586 }, 587 { 588 Name: "sharding test - 2/2", 589 TestOptions: TestOptions{ 590 Class: []string{"test2"}, 591 }, 592 }, 593 }, 594 }, 595 { 596 name: "should ignore empty lines and spaces in testListFile when sharding is enabled", 597 project: Project{ 598 Sauce: config.SauceConfig{ 599 Concurrency: 2, 600 }, 601 Suites: []Suite{ 602 { 603 Name: "sharding test", 604 Shard: "concurrency", 605 }, 606 }, 607 }, 608 content: " test1\t\n\ntest2\t\n\n", 609 configEnabled: true, 610 expSuites: []Suite{ 611 { 612 Name: "sharding test - 1/2", 613 TestOptions: TestOptions{ 614 Class: []string{"test1"}, 615 }, 616 }, 617 { 618 Name: "sharding test - 2/2", 619 TestOptions: TestOptions{ 620 Class: []string{"test2"}, 621 }, 622 }, 623 }, 624 }, 625 { 626 name: "should return error when sharding w/o a testListFile", 627 project: Project{ 628 Sauce: config.SauceConfig{ 629 Concurrency: 2, 630 }, 631 Suites: []Suite{ 632 { 633 Name: "sharding test", 634 Shard: "concurrency", 635 TestOptions: TestOptions{ 636 Class: []string{"test1"}, 637 }, 638 }, 639 }, 640 }, 641 configEnabled: false, 642 expSuites: []Suite{ 643 { 644 Name: "sharding test", 645 TestOptions: TestOptions{ 646 Class: []string{"test1"}, 647 }, 648 }, 649 }, 650 expErr: true, 651 }, 652 { 653 name: "should return error when sharding w/ an empty testListFile", 654 project: Project{ 655 Sauce: config.SauceConfig{ 656 Concurrency: 2, 657 }, 658 Suites: []Suite{ 659 { 660 Name: "sharding test", 661 Shard: "concurrency", 662 TestOptions: TestOptions{ 663 Class: []string{"test1"}, 664 }, 665 }, 666 }, 667 }, 668 configEnabled: true, 669 content: "", 670 expSuites: []Suite{ 671 { 672 Name: "sharding test", 673 TestOptions: TestOptions{ 674 Class: []string{"test1"}, 675 }, 676 }, 677 }, 678 expErr: true, 679 }, 680 } 681 682 for _, tc := range testCases { 683 t.Run(tc.name, func(t *testing.T) { 684 var testListFile string 685 if tc.configEnabled { 686 testListFile = createTestListFile(t, tc.content) 687 tc.project.Suites[0].TestListFile = testListFile 688 } 689 err := ShardSuites(&tc.project) 690 if err != nil { 691 assert.True(t, tc.expErr) 692 } 693 for i, s := range tc.project.Suites { 694 assert.True(t, cmp.Equal(s.TestOptions, tc.expSuites[i].TestOptions)) 695 assert.True(t, cmp.Equal(s.Name, tc.expSuites[i].Name)) 696 } 697 }) 698 } 699 } 700 701 func createTestListFile(t *testing.T, content string) string { 702 t.Helper() 703 tmpDir := t.TempDir() 704 file := filepath.Join(tmpDir, "tests.txt") 705 if err := os.WriteFile(file, []byte(content), 0644); err != nil { 706 t.Fatalf("Setup failed: could not write tests.txt: %v", err) 707 return "" 708 } 709 return file 710 }