github.com/saucelabs/saucectl@v0.175.1/internal/saucecloud/cloud_test.go (about) 1 package saucecloud 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "syscall" 11 "testing" 12 "time" 13 14 "github.com/saucelabs/saucectl/internal/job" 15 "github.com/saucelabs/saucectl/internal/junit" 16 "github.com/saucelabs/saucectl/internal/mocks" 17 "github.com/saucelabs/saucectl/internal/saucecloud/retry" 18 "github.com/saucelabs/saucectl/internal/saucecloud/zip" 19 "github.com/saucelabs/saucectl/internal/sauceignore" 20 "github.com/saucelabs/saucectl/internal/saucereport" 21 "github.com/stretchr/testify/assert" 22 "gotest.tools/v3/fs" 23 ) 24 25 func TestSignalDetection(t *testing.T) { 26 r := CloudRunner{JobService: JobService{VDCStopper: &mocks.FakeJobStopper{}}} 27 assert.False(t, r.interrupted) 28 c := r.registerSkipSuitesOnSignal() 29 defer unregisterSignalCapture(c) 30 31 c <- syscall.SIGINT 32 33 deadline := time.NewTimer(3 * time.Second) 34 defer deadline.Stop() 35 36 // Wait for interrupt to be processed, as it happens asynchronously. 37 for { 38 select { 39 case <-deadline.C: 40 assert.True(t, r.interrupted) 41 return 42 default: 43 if r.interrupted { 44 return 45 } 46 time.Sleep(1 * time.Nanosecond) // allow context switch 47 } 48 } 49 } 50 51 func TestSignalDetectionExit(t *testing.T) { 52 if os.Getenv("FORCE_EXIT_TEST") == "1" { 53 r := CloudRunner{JobService: JobService{VDCStopper: &mocks.FakeJobStopper{}}} 54 assert.False(t, r.interrupted) 55 c := r.registerSkipSuitesOnSignal() 56 defer unregisterSignalCapture(c) 57 58 c <- syscall.SIGINT 59 60 deadline := time.NewTimer(3 * time.Second) 61 defer deadline.Stop() 62 63 // Wait for interrupt to be processed, as it happens asynchronously. 64 loop: 65 for { 66 select { 67 case <-deadline.C: 68 return 69 default: 70 if r.interrupted { 71 break loop 72 } 73 time.Sleep(1 * time.Nanosecond) // allow context switch 74 } 75 } 76 77 c <- syscall.SIGINT 78 79 // Process should get killed due to double interrupt. If this doesn't happen, the test will exit cleanly 80 // which will be caught by the original process of the test, which expects an exit code of 1. 81 time.Sleep(3 * time.Second) 82 return 83 } 84 cmd := exec.Command(os.Args[0], "-test.run=TestSignalDetectionExit") 85 cmd.Env = append(os.Environ(), "FORCE_EXIT_TEST=1") 86 err := cmd.Run() 87 if e, ok := err.(*exec.ExitError); ok && !e.Success() { 88 return 89 } 90 t.Fatalf("process ran with err %v, want exit status 1", err) 91 } 92 93 func TestSkippedRunJobs(t *testing.T) { 94 sut := CloudRunner{ 95 JobService: JobService{ 96 VDCStarter: &mocks.FakeJobStarter{ 97 StartJobFn: func(ctx context.Context, opts job.StartOptions) (jobID string, isRDC bool, err error) { 98 return "fake-id", false, nil 99 }, 100 }, 101 VDCStopper: &mocks.FakeJobStopper{ 102 StopJobFn: func(ctx context.Context, id string) (job.Job, error) { 103 return job.Job{ 104 ID: "fake-id", 105 }, nil 106 }, 107 }, 108 VDCReader: &mocks.FakeJobReader{ 109 PollJobFn: func(ctx context.Context, id string, interval time.Duration, timeout time.Duration) (job.Job, error) { 110 return job.Job{ 111 ID: "fake-id", 112 Passed: true, 113 Error: "", 114 Status: job.StateComplete, 115 }, nil 116 }, 117 }, 118 VDCWriter: &mocks.FakeJobWriter{ 119 UploadAssetFn: func(jobID string, fileName string, contentType string, content []byte) error { 120 return nil 121 }, 122 }, 123 }, 124 } 125 sut.interrupted = true 126 127 _, skipped, err := sut.runJob(job.StartOptions{}) 128 129 assert.True(t, skipped) 130 assert.Nil(t, err) 131 } 132 133 func TestRunJobsSkipped(t *testing.T) { 134 r := CloudRunner{} 135 r.interrupted = true 136 137 opts := make(chan job.StartOptions) 138 results := make(chan result) 139 140 go r.runJobs(opts, results) 141 opts <- job.StartOptions{} 142 close(opts) 143 res := <-results 144 assert.Nil(t, res.err) 145 assert.True(t, res.skipped) 146 } 147 148 func TestRunJobTimeout(t *testing.T) { 149 r := CloudRunner{ 150 JobService: JobService{ 151 VDCStarter: &mocks.FakeJobStarter{ 152 StartJobFn: func(ctx context.Context, opts job.StartOptions) (jobID string, isRDC bool, err error) { 153 return "1", false, nil 154 }, 155 }, 156 VDCReader: &mocks.FakeJobReader{ 157 PollJobFn: func(ctx context.Context, id string, interval time.Duration, timeout time.Duration) (job.Job, error) { 158 return job.Job{ID: id, TimedOut: true}, nil 159 }, 160 }, 161 VDCStopper: &mocks.FakeJobStopper{ 162 StopJobFn: func(ctx context.Context, jobID string) (job.Job, error) { 163 return job.Job{ID: jobID}, nil 164 }, 165 }, 166 VDCWriter: &mocks.FakeJobWriter{UploadAssetFn: func(jobID string, fileName string, contentType string, content []byte) error { 167 return nil 168 }}, 169 }, 170 } 171 172 opts := make(chan job.StartOptions) 173 results := make(chan result) 174 175 go r.runJobs(opts, results) 176 opts <- job.StartOptions{ 177 DisplayName: "dummy", 178 Timeout: 1, 179 } 180 close(opts) 181 res := <-results 182 assert.Error(t, res.err, "suite 'dummy' has reached timeout") 183 assert.True(t, res.job.TimedOut) 184 } 185 186 func TestRunJobRetries(t *testing.T) { 187 type testCase struct { 188 retries int 189 wantAttempts int 190 } 191 192 tests := []testCase{ 193 { 194 retries: 0, 195 wantAttempts: 1, 196 }, 197 { 198 retries: 4, 199 wantAttempts: 5, 200 }, 201 } 202 for _, tt := range tests { 203 r := CloudRunner{ 204 Retrier: &retry.SauceReportRetrier{}, 205 JobService: JobService{ 206 VDCStarter: &mocks.FakeJobStarter{ 207 StartJobFn: func(ctx context.Context, opts job.StartOptions) (jobID string, isRDC bool, err error) { 208 return "1", false, nil 209 }, 210 }, 211 VDCReader: &mocks.FakeJobReader{ 212 PollJobFn: func(ctx context.Context, id string, interval time.Duration, timeout time.Duration) (job.Job, error) { 213 return job.Job{ID: id, Passed: false}, nil 214 }, 215 }, 216 VDCStopper: &mocks.FakeJobStopper{ 217 StopJobFn: func(ctx context.Context, jobID string) (job.Job, error) { 218 return job.Job{ID: jobID}, nil 219 }, 220 }, 221 VDCWriter: &mocks.FakeJobWriter{UploadAssetFn: func(jobID string, fileName string, contentType string, content []byte) error { 222 return nil 223 }}, 224 }, 225 } 226 227 opts := make(chan job.StartOptions, tt.retries+1) 228 results := make(chan result) 229 230 go r.runJobs(opts, results) 231 opts <- job.StartOptions{ 232 DisplayName: "retry job", 233 Retries: tt.retries, 234 } 235 res := <-results 236 close(opts) 237 close(results) 238 assert.Equal(t, len(res.attempts), tt.wantAttempts) 239 } 240 } 241 242 func TestRunJobTimeoutRDC(t *testing.T) { 243 r := CloudRunner{ 244 JobService: JobService{ 245 RDCStarter: &mocks.FakeJobStarter{ 246 StartJobFn: func(ctx context.Context, opts job.StartOptions) (jobID string, isRDC bool, err error) { 247 return "1", true, nil 248 }, 249 }, 250 RDCReader: &mocks.FakeJobReader{ 251 PollJobFn: func(ctx context.Context, id string, interval time.Duration, timeout time.Duration) (job.Job, error) { 252 return job.Job{ID: id, TimedOut: true}, nil 253 }, 254 }, 255 RDCStopper: &mocks.FakeJobStopper{ 256 StopJobFn: func(ctx context.Context, id string) (job.Job, error) { 257 return job.Job{ID: id, TimedOut: true}, nil 258 }, 259 }, 260 }, 261 } 262 263 opts := make(chan job.StartOptions) 264 results := make(chan result) 265 266 go r.runJobs(opts, results) 267 opts <- job.StartOptions{ 268 DisplayName: "dummy", 269 Timeout: 1, 270 RealDevice: true, 271 } 272 close(opts) 273 res := <-results 274 assert.Error(t, res.err) 275 assert.True(t, res.job.TimedOut) 276 } 277 278 func TestCloudRunner_archiveNodeModules(t *testing.T) { 279 tempDir, err := os.MkdirTemp(os.TempDir(), "saucectl-app-payload-") 280 if err != nil { 281 t.Error(err) 282 } 283 defer os.RemoveAll(tempDir) 284 285 projectsDir := fs.NewDir(t, "project", 286 fs.WithDir("has-mods", 287 fs.WithDir("node_modules", 288 fs.WithDir("mod1", 289 fs.WithFile("package.json", "{}"), 290 ), 291 ), 292 ), 293 fs.WithDir("no-mods"), 294 fs.WithDir("empty-mods", 295 fs.WithDir("node_modules"), 296 ), 297 ) 298 defer projectsDir.Remove() 299 300 wd, err := os.Getwd() 301 if err != nil { 302 t.Errorf("Failed to get the current working dir: %v", err) 303 } 304 305 if err := os.Chdir(projectsDir.Path()); err != nil { 306 t.Errorf("Failed to change the current working dir: %v", err) 307 } 308 defer func() { 309 if err := os.Chdir(wd); err != nil { 310 t.Errorf("Failed to change the current working dir back to original: %v", err) 311 } 312 }() 313 314 type fields struct { 315 NPMDependencies []string 316 } 317 type args struct { 318 tempDir string 319 rootDir string 320 matcher sauceignore.Matcher 321 } 322 tests := []struct { 323 name string 324 fields fields 325 args args 326 want string 327 wantErr assert.ErrorAssertionFunc 328 }{ 329 { 330 "want to include mods, but node_modules does not exist", 331 fields{ 332 NPMDependencies: []string{"mod1"}, 333 }, 334 args{ 335 tempDir: tempDir, 336 rootDir: "no-mods", 337 matcher: sauceignore.NewMatcher([]sauceignore.Pattern{}), 338 }, 339 "", 340 func(t assert.TestingT, err error, args ...interface{}) bool { 341 return assert.EqualError(t, err, "unable to access 'node_modules' folder, but you have npm dependencies defined in your configuration; ensure that the folder exists and is accessible", args) 342 }, 343 }, 344 { 345 "have and want mods, but mods are ignored", 346 fields{ 347 NPMDependencies: []string{"mod1"}, 348 }, 349 args{ 350 tempDir: tempDir, 351 rootDir: "has-mods", 352 matcher: sauceignore.NewMatcher([]sauceignore.Pattern{sauceignore.NewPattern("/has-mods/node_modules")}), 353 }, 354 "", 355 func(t assert.TestingT, err error, args ...interface{}) bool { 356 return assert.EqualError(t, err, "'node_modules' is ignored by sauceignore, but you have npm dependencies defined in your project; please remove 'node_modules' from your sauceignore file", args) 357 }, 358 }, 359 { 360 "have mods, don't want them and they are ignored", 361 fields{ 362 NPMDependencies: []string{}, // no mods selected, because we don't want any 363 }, 364 args{ 365 tempDir: tempDir, 366 rootDir: "has-mods", 367 matcher: sauceignore.NewMatcher([]sauceignore.Pattern{sauceignore.NewPattern("/has-mods/node_modules")}), 368 }, 369 "", 370 assert.NoError, 371 }, 372 { 373 "no mods wanted and no mods exist", 374 fields{ 375 NPMDependencies: []string{}, 376 }, 377 args{ 378 tempDir: tempDir, 379 rootDir: "no-mods", 380 matcher: sauceignore.NewMatcher([]sauceignore.Pattern{}), 381 }, 382 "", 383 assert.NoError, 384 }, 385 { 386 "has and wants mods (happy path)", 387 fields{ 388 NPMDependencies: []string{"mod1"}, 389 }, 390 args{ 391 tempDir: tempDir, 392 rootDir: "has-mods", 393 matcher: sauceignore.NewMatcher([]sauceignore.Pattern{}), 394 }, 395 filepath.Join(tempDir, "node_modules.zip"), 396 assert.NoError, 397 }, 398 { 399 "want mods, but node_modules folder is empty", 400 fields{ 401 NPMDependencies: []string{"mod1"}, 402 }, 403 args{ 404 tempDir: tempDir, 405 rootDir: "empty-mods", 406 matcher: sauceignore.NewMatcher([]sauceignore.Pattern{}), 407 }, 408 "", 409 func(t assert.TestingT, err error, args ...interface{}) bool { 410 return assert.EqualError(t, err, "unable to find required dependencies; please check 'node_modules' folder and make sure the dependencies exist", args) 411 }, 412 }, 413 } 414 for _, tt := range tests { 415 t.Run(tt.name, func(t *testing.T) { 416 r := &CloudRunner{ 417 NPMDependencies: tt.fields.NPMDependencies, 418 } 419 got, err := zip.ArchiveNodeModules(tt.args.tempDir, tt.args.rootDir, tt.args.matcher, r.NPMDependencies) 420 if !tt.wantErr(t, err, fmt.Sprintf("archiveNodeModules(%v, %v, %v)", tt.args.tempDir, tt.args.rootDir, tt.args.matcher)) { 421 return 422 } 423 assert.Equalf(t, tt.want, got, "archiveNodeModules(%v, %v, %v)", tt.args.tempDir, tt.args.rootDir, tt.args.matcher) 424 }) 425 } 426 } 427 428 func Test_arrayContains(t *testing.T) { 429 type args struct { 430 list []string 431 want string 432 } 433 tests := []struct { 434 name string 435 args args 436 want bool 437 }{ 438 { 439 name: "Empty set", 440 args: args{ 441 list: []string{}, 442 want: "value", 443 }, 444 want: false, 445 }, 446 { 447 name: "Complete set - false", 448 args: args{ 449 list: []string{"val1", "val2", "val3"}, 450 want: "value", 451 }, 452 want: false, 453 }, 454 { 455 name: "Found", 456 args: args{ 457 list: []string{"val1", "val2", "val3"}, 458 want: "val1", 459 }, 460 want: true, 461 }, 462 } 463 for _, tt := range tests { 464 t.Run(tt.name, func(t *testing.T) { 465 assert.Equalf(t, tt.want, arrayContains(tt.args.list, tt.args.want), "arrayContains(%v, %v)", tt.args.list, tt.args.want) 466 }) 467 } 468 } 469 470 func TestCloudRunner_loadSauceTestReport(t *testing.T) { 471 type args struct { 472 jobID string 473 isRDC bool 474 } 475 type fields struct { 476 GetJobAssetFileNamesFn func(ctx context.Context, jobID string) ([]string, error) 477 GetJobAssetFileContentFn func(ctx context.Context, jobID, fileName string) ([]byte, error) 478 } 479 tests := []struct { 480 name string 481 args args 482 fields fields 483 want saucereport.SauceReport 484 wantErr assert.ErrorAssertionFunc 485 }{ 486 { 487 name: "Complete unmarshall", 488 args: args{ 489 jobID: "test1", 490 isRDC: false, 491 }, 492 fields: fields{ 493 GetJobAssetFileNamesFn: func(ctx context.Context, jobID string) ([]string, error) { 494 return []string{saucereport.SauceReportFileName}, nil 495 }, 496 GetJobAssetFileContentFn: func(ctx context.Context, jobID, fileName string) ([]byte, error) { 497 if fileName == saucereport.SauceReportFileName { 498 return []byte(`{"status":"failed","attachments":[],"suites":[{"name":"cypress/e2e/examples/actions.cy.js","status":"failed","metadata":{},"suites":[{"name":"Actions","status":"failed","metadata":{},"suites":[],"attachments":[],"tests":[{"name":".type() - type into a DOM element","status":"passed","startTime":"2022-12-22T10:10:11.083Z","duration":1802,"metadata":{},"output":null,"attachments":[],"code":{"lines":["() => {"," // https://on.cypress.io/type"," cy.get('.action-email').type('fake@email.com').should('have.value', 'fake@email.com');"," }"]},"videoTimestamp":26.083},{"name":".type() - type into a wrong DOM element","status":"failed","startTime":"2022-12-22T10:10:12.907Z","duration":5010,"metadata":{},"output":"AssertionError: Timed out retrying after 4000ms: expected '<input#email1.form-control.action-email>' to have value 'wrongy@email.com', but the value was 'fake@email.com'\n\n 11 | // https://on.cypress.io/type\n 12 | cy.get('.action-email')\n> 13 | .type('fake@email.com').should('have.value', 'wrongy@email.com')\n | ^\n 14 | })\n 15 | })\n 16 | ","attachments":[{"name":"screenshot","path":"Actions -- .type() - type into a wrong DOM element (failed).png","contentType":"image/png"}],"code":{"lines":["() => {"," // https://on.cypress.io/type"," cy.get('.action-email').type('fake@email.com').should('have.value', 'wrongy@email.com');"," }"]},"videoTimestamp":27.907}]}],"attachments":[],"tests":[]}],"metadata":{}}`), nil 499 } 500 return []byte{}, errors.New("not-found") 501 }, 502 }, 503 wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 504 return err == nil 505 }, 506 want: saucereport.SauceReport{ 507 Status: saucereport.StatusFailed, 508 Attachments: []saucereport.Attachment{}, 509 Suites: []saucereport.Suite{ 510 { 511 Name: "cypress/e2e/examples/actions.cy.js", 512 Status: saucereport.StatusFailed, 513 Attachments: []saucereport.Attachment{}, 514 Metadata: saucereport.Metadata{}, 515 Tests: []saucereport.Test{}, 516 Suites: []saucereport.Suite{ 517 { 518 Name: "Actions", 519 Status: saucereport.StatusFailed, 520 Attachments: []saucereport.Attachment{}, 521 Suites: []saucereport.Suite{}, 522 Metadata: saucereport.Metadata{}, 523 Tests: []saucereport.Test{ 524 { 525 Name: ".type() - type into a DOM element", 526 Status: saucereport.StatusPassed, 527 StartTime: time.Date(2022, 12, 22, 10, 10, 11, 83000000, time.UTC), 528 Duration: 1802, 529 Metadata: saucereport.Metadata{}, 530 Code: saucereport.Code{ 531 Lines: []string{ 532 "() => {", 533 " // https://on.cypress.io/type", 534 " cy.get('.action-email').type('fake@email.com').should('have.value', 'fake@email.com');", 535 " }", 536 }, 537 }, 538 VideoTimestamp: 26.083, 539 Attachments: []saucereport.Attachment{}, 540 }, 541 { 542 Name: ".type() - type into a wrong DOM element", 543 Status: saucereport.StatusFailed, 544 StartTime: time.Date(2022, 12, 22, 10, 10, 12, 907000000, time.UTC), 545 Duration: 5010, 546 Output: "AssertionError: Timed out retrying after 4000ms: expected '<input#email1.form-control.action-email>' to have value 'wrongy@email.com', but the value was 'fake@email.com'\n\n 11 | // https://on.cypress.io/type\n 12 | cy.get('.action-email')\n> 13 | .type('fake@email.com').should('have.value', 'wrongy@email.com')\n | ^\n 14 | })\n 15 | })\n 16 | ", 547 Attachments: []saucereport.Attachment{ 548 { 549 Name: "screenshot", 550 Path: "Actions -- .type() - type into a wrong DOM element (failed).png", 551 ContentType: "image/png", 552 }, 553 }, 554 Metadata: saucereport.Metadata{}, 555 Code: saucereport.Code{ 556 Lines: []string{ 557 "() => {", 558 " // https://on.cypress.io/type", 559 " cy.get('.action-email').type('fake@email.com').should('have.value', 'wrongy@email.com');", 560 " }", 561 }, 562 }, 563 VideoTimestamp: 27.907, 564 }, 565 }, 566 }, 567 }, 568 }, 569 }, 570 }, 571 }, 572 } 573 574 for _, tt := range tests { 575 t.Run(tt.name, func(t *testing.T) { 576 r := CloudRunner{ 577 JobService: JobService{ 578 VDCReader: &mocks.FakeJobReader{ 579 GetJobAssetFileNamesFn: tt.fields.GetJobAssetFileNamesFn, 580 GetJobAssetFileContentFn: tt.fields.GetJobAssetFileContentFn, 581 }, 582 }, 583 } 584 got, err := r.loadSauceTestReport(tt.args.jobID, tt.args.isRDC) 585 if !tt.wantErr(t, err, fmt.Sprintf("loadSauceTestReport(%v, %v)", tt.args.jobID, tt.args.isRDC)) { 586 return 587 } 588 assert.Equalf(t, tt.want, got, "loadSauceTestReport(%v, %v)", tt.args.jobID, tt.args.isRDC) 589 }) 590 } 591 } 592 593 func TestCloudRunner_loadJUnitReport(t *testing.T) { 594 type args struct { 595 jobID string 596 isRDC bool 597 } 598 type fields struct { 599 GetJobAssetFileNamesFn func(ctx context.Context, jobID string) ([]string, error) 600 GetJobAssetFileContentFn func(ctx context.Context, jobID, fileName string) ([]byte, error) 601 } 602 tests := []struct { 603 name string 604 fields fields 605 args args 606 want junit.TestSuites 607 wantErr assert.ErrorAssertionFunc 608 }{ 609 { 610 name: "Unmarshall XML", 611 fields: fields{ 612 GetJobAssetFileNamesFn: func(ctx context.Context, jobID string) ([]string, error) { 613 return []string{junit.FileName}, nil 614 }, 615 GetJobAssetFileContentFn: func(ctx context.Context, jobID, fileName string) ([]byte, error) { 616 if fileName == junit.FileName { 617 return []byte(`<?xml version="1.0" encoding="utf-8"?><testsuite package="com.saucelabs.mydemoapp.android" tests="7" time="52.056"><testcase classname="com.saucelabs.mydemoapp.android.view.activities.DashboardToCheckout" name="dashboardProductTest" status="success"/><testcase classname="com.saucelabs.mydemoapp.android.view.activities.LoginTest" name="succesfulLoginTest" status="success"/><testcase classname="com.saucelabs.mydemoapp.android.view.activities.LoginTest" name="noUsernameLoginTest" status="success"/><testcase classname="com.saucelabs.mydemoapp.android.view.activities.LoginTest" name="noPasswordLoginTest" status="success"/><testcase classname="com.saucelabs.mydemoapp.android.view.activities.LoginTest" name="noCredentialLoginTest" status="success"/><testcase classname="com.saucelabs.mydemoapp.android.view.activities.WebViewTest" name="webViewTest" status="success"/><testcase classname="com.saucelabs.mydemoapp.android.view.activities.WebViewTest" name="withoutUrlTest" status="success"/><system-out>INSTRUMENTATION_STATUS: class=com.saucelabs.mydemoapp.android.view.activities.DashboardToCheckout</system-out></testsuite>`), nil 618 } 619 return []byte{}, errors.New("not-found") 620 }, 621 }, 622 args: args{ 623 jobID: "dummy-jobID", 624 isRDC: false, 625 }, 626 want: junit.TestSuites{ 627 TestSuites: []junit.TestSuite{ 628 { 629 Package: "com.saucelabs.mydemoapp.android", 630 Tests: 7, 631 Time: "52.056", 632 TestCases: []junit.TestCase{ 633 { 634 ClassName: "com.saucelabs.mydemoapp.android.view.activities.DashboardToCheckout", 635 Name: "dashboardProductTest", 636 Status: "success", 637 }, 638 { 639 ClassName: "com.saucelabs.mydemoapp.android.view.activities.LoginTest", 640 Name: "succesfulLoginTest", 641 Status: "success", 642 }, 643 { 644 ClassName: "com.saucelabs.mydemoapp.android.view.activities.LoginTest", 645 Name: "noUsernameLoginTest", 646 Status: "success", 647 }, 648 { 649 ClassName: "com.saucelabs.mydemoapp.android.view.activities.LoginTest", 650 Name: "noPasswordLoginTest", 651 Status: "success", 652 }, 653 { 654 ClassName: "com.saucelabs.mydemoapp.android.view.activities.LoginTest", 655 Name: "noCredentialLoginTest", 656 Status: "success", 657 }, 658 { 659 ClassName: "com.saucelabs.mydemoapp.android.view.activities.WebViewTest", 660 Name: "webViewTest", 661 Status: "success", 662 }, 663 { 664 ClassName: "com.saucelabs.mydemoapp.android.view.activities.WebViewTest", 665 Name: "withoutUrlTest", 666 Status: "success", 667 }, 668 }, 669 SystemOut: "INSTRUMENTATION_STATUS: class=com.saucelabs.mydemoapp.android.view.activities.DashboardToCheckout", 670 }, 671 }, 672 }, 673 wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 674 return err == nil 675 }, 676 }, 677 } 678 for _, tt := range tests { 679 t.Run(tt.name, func(t *testing.T) { 680 r := &CloudRunner{ 681 JobService: JobService{ 682 VDCReader: &mocks.FakeJobReader{ 683 GetJobAssetFileNamesFn: tt.fields.GetJobAssetFileNamesFn, 684 GetJobAssetFileContentFn: tt.fields.GetJobAssetFileContentFn, 685 }, 686 }, 687 } 688 got, err := r.loadJUnitReport(tt.args.jobID, tt.args.isRDC) 689 if !tt.wantErr(t, err, fmt.Sprintf("loadJUnitReport(%v, %v)", tt.args.jobID, tt.args.isRDC)) { 690 return 691 } 692 assert.Equalf(t, tt.want, got, "loadJUnitReport(%v, %v)", tt.args.jobID, tt.args.isRDC) 693 }) 694 } 695 }