diff --git a/internal/api/create_test_plan_test.go b/internal/api/create_test_plan_test.go index 30ae7eed..7a6e29d6 100644 --- a/internal/api/create_test_plan_test.go +++ b/internal/api/create_test_plan_test.go @@ -18,7 +18,7 @@ import ( func TestCreateTestPlan(t *testing.T) { mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ Consumer: "TestEngineClient", - Provider: "TestPlanServer", + Provider: "TestEngineServer", }) if err != nil { @@ -122,7 +122,7 @@ func TestCreateTestPlan(t *testing.T) { func TestCreateTestPlan_SplitByExample(t *testing.T) { mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ Consumer: "TestEngineClient", - Provider: "TestPlanServer", + Provider: "TestEngineServer", }) if err != nil { @@ -145,6 +145,7 @@ func TestCreateTestPlan_SplitByExample(t *testing.T) { }, }, }, + Runner: "rspec", } err = mockProvider. @@ -274,6 +275,116 @@ func TestCreateTestPlan_BadRequest(t *testing.T) { } } +func TestCreateTestPlan_MutedTests(t *testing.T) { + mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ + Consumer: "TestEngineClient", + Provider: "TestEngineServer", + }) + + if err != nil { + t.Error("Error mocking provider", err) + } + + params := TestPlanParams{ + Runner: "rspec", + Branch: "tet-123-add-branch-name", + Identifier: "abc123", + Parallelism: 3, + Tests: TestPlanParamsTest{ + Files: []plan.TestCase{ + {Path: "sky_spec.rb"}, + }, + }, + } + + err = mockProvider. + AddInteraction(). + Given("A test plan doesn't exist and muted test exists"). + UponReceiving("A request to create test plan with identifier abc123 and split by example disabled"). + WithRequest("POST", "/v2/analytics/organizations/buildkite/suites/rspec/test_plan", func(b *consumer.V2RequestBuilder) { + b.Header("Authorization", matchers.String("Bearer asdf1234")) + b.Header("Content-Type", matchers.String("application/json")) + b.JSONBody(params) + }). + WillRespondWith(200, func(b *consumer.V2ResponseBuilder) { + b.Header("Content-Type", matchers.String("application/json; charset=utf-8")) + b.JSONBody(matchers.MapMatcher{ + "tasks": matchers.Like(map[string]interface{}{ + "0": matchers.Like(map[string]interface{}{ + "node_number": matchers.Like(0), + "tests": matchers.EachLike(matchers.MapMatcher{ + "path": matchers.Like("sky_spec.rb"), + "format": matchers.Like("file"), + "estimated_duration": matchers.Like(1000), + }, 1), + }), + "1": matchers.Like(map[string]interface{}{ + "node_number": matchers.Like(1), + "tests": []plan.TestCase{}, + }), + "2": matchers.Like(map[string]interface{}{ + "node_number": matchers.Like(2), + "tests": []plan.TestCase{}, + }), + }), + "muted_tests": matchers.EachLike(matchers.MapMatcher{ + "path": matchers.Like("./turtle_spec.rb:3"), + "scope": matchers.Like("turtle"), + "name": matchers.Like("is green"), + }, 1), + }) + }). + ExecuteTest(t, func(config consumer.MockServerConfig) error { + ctx := context.Background() + fetchCtx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + + url := fmt.Sprintf("http://%s:%d", config.Host, config.Port) + apiClient := NewClient(ClientConfig{ + AccessToken: "asdf1234", + OrganizationSlug: "buildkite", + ServerBaseUrl: url, + }) + + got, err := apiClient.CreateTestPlan(fetchCtx, "rspec", params) + if err != nil { + t.Errorf("CreateTestPlan(ctx, %v) error = %v", params, err) + } + + want := plan.TestPlan{ + Tasks: map[string]*plan.Task{ + "0": { + NodeNumber: 0, + Tests: []plan.TestCase{{ + Path: "sky_spec.rb", + Format: "file", + EstimatedDuration: 1000, + }}, + }, + "1": { + NodeNumber: 1, + Tests: []plan.TestCase{}, + }, + "2": { + NodeNumber: 2, + Tests: []plan.TestCase{}, + }, + }, + MutedTests: []plan.TestCase{{Name: "is green", Path: "./turtle_spec.rb:3", Scope: "turtle"}}, + } + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("CreateTestPlan(ctx, %v) diff (-got +want):\n%s", params, diff) + } + + return nil + }) + + if err != nil { + t.Error("mockProvider error", err) + } +} + func TestCreateTestPlan_InternalServerError(t *testing.T) { originalTimeout := retryTimeout retryTimeout = 1 * time.Millisecond diff --git a/internal/api/fetch_files_timing_test.go b/internal/api/fetch_files_timing_test.go index aaf6993a..0fc8851e 100644 --- a/internal/api/fetch_files_timing_test.go +++ b/internal/api/fetch_files_timing_test.go @@ -17,7 +17,7 @@ import ( func TestFetchFilesTiming(t *testing.T) { mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ Consumer: "TestEngineClient", - Provider: "TestPlanServer", + Provider: "TestEngineServer", }) if err != nil { diff --git a/internal/api/fetch_test_plan_test.go b/internal/api/fetch_test_plan_test.go index 6f5a4804..d5c61579 100644 --- a/internal/api/fetch_test_plan_test.go +++ b/internal/api/fetch_test_plan_test.go @@ -18,7 +18,7 @@ import ( func TestFetchTestPlan(t *testing.T) { mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ Consumer: "TestEngineClient", - Provider: "TestPlanServer", + Provider: "TestEngineServer", }) if err != nil { @@ -99,7 +99,7 @@ func TestFetchTestPlan(t *testing.T) { func TestFetchTestPlan_NotFound(t *testing.T) { mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ Consumer: "TestEngineClient", - Provider: "TestPlanServer", + Provider: "TestEngineServer", }) if err != nil { diff --git a/internal/api/filter_tests_test.go b/internal/api/filter_tests_test.go index 7f0a98f1..028969fd 100644 --- a/internal/api/filter_tests_test.go +++ b/internal/api/filter_tests_test.go @@ -18,7 +18,7 @@ import ( func TestFilterTests_SlowFiles(t *testing.T) { mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ Consumer: "TestEngineClient", - Provider: "TestPlanServer", + Provider: "TestEngineServer", }) if err != nil { diff --git a/internal/api/post_test_plan_metadata_test.go b/internal/api/post_test_plan_metadata_test.go index 04c8fccd..50d2bfb8 100644 --- a/internal/api/post_test_plan_metadata_test.go +++ b/internal/api/post_test_plan_metadata_test.go @@ -12,7 +12,7 @@ import ( func TestPostTestPlanMetadata(t *testing.T) { mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ Consumer: "TestEngineClient", - Provider: "TestPlanServer", + Provider: "TestEngineServer", }) if err != nil { @@ -85,7 +85,7 @@ func TestPostTestPlanMetadata(t *testing.T) { func TestPostTestPlanMetadata_NotFound(t *testing.T) { mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ Consumer: "TestEngineClient", - Provider: "TestPlanServer", + Provider: "TestEngineServer", }) if err != nil { diff --git a/internal/plan/type.go b/internal/plan/type.go index 1e99d3fd..3424b71a 100644 --- a/internal/plan/type.go +++ b/internal/plan/type.go @@ -28,4 +28,5 @@ type TestPlan struct { Experiment string `json:"experiment"` Tasks map[string]*Task `json:"tasks"` Fallback bool + MutedTests []TestCase `json:"muted_tests,omitempty"` } diff --git a/internal/runner/cypress.go b/internal/runner/cypress.go index c7d80ad5..812ef544 100644 --- a/internal/runner/cypress.go +++ b/internal/runner/cypress.go @@ -31,8 +31,12 @@ func NewCypress(c RunnerConfig) Cypress { return Cypress{c} } -func (c Cypress) Run(testCases []string, retry bool) (RunResult, error) { - cmdName, cmdArgs, err := c.commandNameAndArgs(c.TestCommand, testCases) +func (c Cypress) Run(testCases []plan.TestCase, retry bool) (RunResult, error) { + testPaths := make([]string, len(testCases)) + for i, tc := range testCases { + testPaths[i] = tc.Path + } + cmdName, cmdArgs, err := c.commandNameAndArgs(c.TestCommand, testPaths) if err != nil { return RunResult{Status: RunStatusError}, fmt.Errorf("failed to build command: %w", err) } diff --git a/internal/runner/cypress_test.go b/internal/runner/cypress_test.go index 2c338251..57dd5c95 100644 --- a/internal/runner/cypress_test.go +++ b/internal/runner/cypress_test.go @@ -6,6 +6,7 @@ import ( "syscall" "testing" + "github.com/buildkite/test-engine-client/internal/plan" "github.com/google/go-cmp/cmp" "github.com/kballard/go-shellquote" ) @@ -17,19 +18,21 @@ func TestCypressRun(t *testing.T) { TestCommand: "yarn cypress run --spec {{testExamples}}", }) - files := []string{"./cypress/e2e/passing_spec.cy.js"} - got, err := cypress.Run(files, false) + testCases := []plan.TestCase{ + {Path: "./cypress/e2e/passing_spec.cy.js"}, + } + got, err := cypress.Run(testCases, false) want := RunResult{ Status: RunStatusPassed, } if err != nil { - t.Errorf("Cypress.Run(%q) error = %v", files, err) + t.Errorf("Cypress.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Cypress.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Cypress.Run(%q) diff (-got +want):\n%s", testCases, diff) } } @@ -40,20 +43,23 @@ func TestCypressRun_TestFailed(t *testing.T) { TestCommand: "yarn cypress run --spec {{testExamples}}", }) - files := []string{"./cypress/e2e/failing_spec.cy.js", "./cypress/e2e/passing_spec.cy.js"} - got, err := cypress.Run(files, false) + testCases := []plan.TestCase{ + {Path: "./cypress/e2e/failing_spec.cy.js"}, + {Path: "./cypress/e2e/passing_spec.cy.js"}, + } + got, err := cypress.Run(testCases, false) want := RunResult{ Status: RunStatusError, } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Cypress.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Cypress.Run(%q) diff (-got +want):\n%s", testCases, diff) } exitError := new(exec.ExitError) if !errors.As(err, &exitError) { - t.Errorf("Cypress.Run(%q) error type = %T (%v), want *exec.ExitError", files, err, err) + t.Errorf("Cypress.Run(%q) error type = %T (%v), want *exec.ExitError", testCases, err, err) } } @@ -64,20 +70,20 @@ func TestCypressRun_CommandFailed(t *testing.T) { }, } - files := []string{} - got, err := cypress.Run(files, false) + testCases := []plan.TestCase{} + got, err := cypress.Run(testCases, false) want := RunResult{ Status: RunStatusError, } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Cypress.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Cypress.Run(%q) diff (-got +want):\n%s", testCases, diff) } exitError := new(exec.ExitError) if !errors.As(err, &exitError) { - t.Errorf("Cypress.Run(%q) error type = %T (%v), want *exec.ExitError", files, err, err) + t.Errorf("Cypress.Run(%q) error type = %T (%v), want *exec.ExitError", testCases, err, err) } } @@ -85,24 +91,26 @@ func TestCypressRun_SignaledError(t *testing.T) { cypress := NewCypress(RunnerConfig{ TestCommand: "./testdata/segv.sh", }) - files := []string{"./doesnt-matter.cy.js"} + testCases := []plan.TestCase{ + {Path: "./doesnt-matter.cy.js"}, + } - got, err := cypress.Run(files, false) + got, err := cypress.Run(testCases, false) want := RunResult{ Status: RunStatusError, } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Cypress.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Cypress.Run(%q) diff (-got +want):\n%s", testCases, diff) } signalError := new(ProcessSignaledError) if !errors.As(err, &signalError) { - t.Errorf("Cypress.Run(%q) error type = %T (%v), want *ErrProcessSignaled", files, err, err) + t.Errorf("Cypress.Run(%q) error type = %T (%v), want *ErrProcessSignaled", testCases, err, err) } if signalError.Signal != syscall.SIGSEGV { - t.Errorf("Cypress.Run(%q) signal = %d, want %d", files, syscall.SIGSEGV, signalError.Signal) + t.Errorf("Cypress.Run(%q) signal = %d, want %d", testCases, syscall.SIGSEGV, signalError.Signal) } } diff --git a/internal/runner/detector.go b/internal/runner/detector.go index 5412cbce..d386f90d 100644 --- a/internal/runner/detector.go +++ b/internal/runner/detector.go @@ -17,7 +17,7 @@ type RunnerConfig struct { } type TestRunner interface { - Run(testCases []string, retry bool) (RunResult, error) + Run(testCases []plan.TestCase, retry bool) (RunResult, error) GetExamples(files []string) ([]plan.TestCase, error) GetFiles() ([]string, error) Name() string diff --git a/internal/runner/jest.go b/internal/runner/jest.go index 0d19dfb2..bd00fd8e 100644 --- a/internal/runner/jest.go +++ b/internal/runner/jest.go @@ -56,19 +56,27 @@ func (j Jest) GetFiles() ([]string, error) { return files, nil } -func (j Jest) Run(testCases []string, retry bool) (RunResult, error) { +func (j Jest) Run(testCases []plan.TestCase, retry bool) (RunResult, error) { var cmd *exec.Cmd var err error if !retry { - commandName, commandArgs, err := j.commandNameAndArgs(j.TestCommand, testCases) + testPaths := make([]string, len(testCases)) + for i, testCase := range testCases { + testPaths[i] = testCase.Path + } + commandName, commandArgs, err := j.commandNameAndArgs(j.TestCommand, testPaths) if err != nil { return RunResult{Status: RunStatusError}, fmt.Errorf("failed to build command: %w", err) } cmd = exec.Command(commandName, commandArgs...) } else { - commandName, commandArgs, err := j.retryCommandNameAndArgs(j.RetryTestCommand, testCases) + testNames := make([]string, len(testCases)) + for i, testCase := range testCases { + testNames[i] = fmt.Sprintf("%s %s", testCase.Scope, testCase.Name) + } + commandName, commandArgs, err := j.retryCommandNameAndArgs(j.RetryTestCommand, testNames) if err != nil { return RunResult{Status: RunStatusError}, fmt.Errorf("failed to build command: %w", err) } @@ -94,11 +102,17 @@ func (j Jest) Run(testCases []string, retry bool) (RunResult, error) { } if report.NumFailedTests > 0 { - var failedTests []string + var failedTests []plan.TestCase for _, testResult := range report.TestResults { for _, example := range testResult.AssertionResults { if example.Status == "failed" { - failedTests = append(failedTests, example.Name) + failedTests = append(failedTests, plan.TestCase{ + // The scope and name has to match with the scope generated by Buildkite test collector. + // For more details, see: + // [Buildkite Test Collector - Jest implementation](https://github.com/buildkite/test-collector-javascript/blob/42b803a618a15a07edf0169038ef4b5eba88f98d/jest/reporter.js#L40) + Name: example.Title, + Scope: strings.Join(example.AncestorTitles, " "), + }) } } } @@ -111,8 +125,10 @@ func (j Jest) Run(testCases []string, retry bool) (RunResult, error) { } type JestExample struct { - Name string `json:"fullName"` - Status string `json:"status"` + Name string `json:"fullName"` + Status string `json:"status"` + Title string `json:"title"` + AncestorTitles []string `json:"ancestorTitles"` } type JestReport struct { diff --git a/internal/runner/jest_test.go b/internal/runner/jest_test.go index 897eab72..6f3ba455 100644 --- a/internal/runner/jest_test.go +++ b/internal/runner/jest_test.go @@ -8,6 +8,7 @@ import ( "syscall" "testing" + "github.com/buildkite/test-engine-client/internal/plan" "github.com/google/go-cmp/cmp" "github.com/kballard/go-shellquote" ) @@ -64,19 +65,21 @@ func TestJestRun(t *testing.T) { os.Remove(jest.ResultPath) }) - files := []string{"./testdata/jest/spells/expelliarmus.spec.js"} - got, err := jest.Run(files, false) + testCases := []plan.TestCase{ + {Path: "./testdata/jest/spells/expelliarmus.spec.js"}, + } + got, err := jest.Run(testCases, false) want := RunResult{ Status: RunStatusPassed, } if err != nil { - t.Errorf("Jest.Run(%q) error = %v", files, err) + t.Errorf("Jest.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", testCases, diff) } } @@ -94,19 +97,21 @@ func TestJestRun_Retry(t *testing.T) { os.Remove(jest.ResultPath) }) - files := []string{"testdata/jest/spells/expelliarmus.spec.js"} - got, err := jest.Run(files, true) + testCases := []plan.TestCase{ + {Name: "disarms the opponent"}, + } + got, err := jest.Run(testCases, true) want := RunResult{ Status: RunStatusPassed, } if err != nil { - t.Errorf("Jest.Run(%q) error = %v", files, err) + t.Errorf("Jest.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", testCases, diff) } } @@ -122,20 +127,24 @@ func TestJestRun_TestFailed(t *testing.T) { os.Remove(jest.ResultPath) }) - files := []string{"./testdata/jest/failure.spec.js"} - got, err := jest.Run(files, false) + testCases := []plan.TestCase{ + {Path: "./testdata/jest/failure.spec.js"}, + } + got, err := jest.Run(testCases, false) want := RunResult{ - Status: RunStatusFailed, - FailedTests: []string{"this will fail for sure"}, + Status: RunStatusFailed, + FailedTests: []plan.TestCase{ + {Scope: "this will fail", Name: "for sure"}, + }, } if err != nil { - t.Errorf("Jest.Run(%q) error = %v", files, err) + t.Errorf("Jest.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", testCases, diff) } } @@ -150,20 +159,20 @@ func TestJestRun_CommandFailed(t *testing.T) { os.Remove(jest.ResultPath) }) - files := []string{} - got, err := jest.Run(files, false) + testCases := []plan.TestCase{} + got, err := jest.Run(testCases, false) want := RunResult{ Status: RunStatusError, } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", testCases, diff) } exitError := new(exec.ExitError) if !errors.As(err, &exitError) { - t.Errorf("Jest.Run(%q) error type = %T (%v), want *exec.ExitError", files, err, err) + t.Errorf("Jest.Run(%q) error type = %T (%v), want *exec.ExitError", testCases, err, err) } } @@ -171,24 +180,26 @@ func TestJestRun_SignaledError(t *testing.T) { jest := NewJest(RunnerConfig{ TestCommand: "./testdata/segv.sh --outputFile {{resultPath}}", }) - files := []string{"./doesnt-matter.spec.js"} + testCases := []plan.TestCase{ + {Path: "./doesnt-matter.spec.js"}, + } - got, err := jest.Run(files, false) + got, err := jest.Run(testCases, false) want := RunResult{ Status: RunStatusError, } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Jest.Run(%q) diff (-got +want):\n%s", testCases, diff) } signalError := new(ProcessSignaledError) if !errors.As(err, &signalError) { - t.Errorf("Jest.Run(%q) error type = %T (%v), want *ErrProcessSignaled", files, err, err) + t.Errorf("Jest.Run(%q) error type = %T (%v), want *ErrProcessSignaled", testCases, err, err) } if signalError.Signal != syscall.SIGSEGV { - t.Errorf("Jest.Run(%q) signal = %d, want %d", files, syscall.SIGSEGV, signalError.Signal) + t.Errorf("Jest.Run(%q) signal = %d, want %d", testCases, syscall.SIGSEGV, signalError.Signal) } } diff --git a/internal/runner/playwright.go b/internal/runner/playwright.go index 469ee73b..fa3b7c2b 100644 --- a/internal/runner/playwright.go +++ b/internal/runner/playwright.go @@ -33,8 +33,13 @@ func NewPlaywright(p RunnerConfig) Playwright { return Playwright{p} } -func (p Playwright) Run(testCases []string, retry bool) (RunResult, error) { - cmdName, cmdArgs, err := p.commandNameAndArgs(p.TestCommand, testCases) +func (p Playwright) Run(testCases []plan.TestCase, retry bool) (RunResult, error) { + testPaths := make([]string, len(testCases)) + for i, tc := range testCases { + testPaths[i] = tc.Path + } + + cmdName, cmdArgs, err := p.commandNameAndArgs(p.TestCommand, testPaths) if err != nil { return RunResult{Status: RunStatusError}, fmt.Errorf("failed to build command: %w", err) } @@ -59,13 +64,9 @@ func (p Playwright) Run(testCases []string, retry bool) (RunResult, error) { } if report.Stats.Unexpected > 0 { - var failedTests []string + var failedTests []plan.TestCase for _, suite := range report.Suites { - for _, spec := range suite.Specs { - if !spec.Ok { - failedTests = append(failedTests, fmt.Sprintf("%s:%d", spec.File, spec.Line)) - } - } + failedTests = p.getFailedTestCasesFromSuite(suite, suite.Title) } return RunResult{Status: RunStatusFailed, FailedTests: failedTests}, nil } @@ -74,6 +75,38 @@ func (p Playwright) Run(testCases []string, retry bool) (RunResult, error) { return RunResult{Status: RunStatusError}, err } +// getFailedTestCasesFromSuite recursively traverses the Playwright report suite and returns a list of failed test cases. +// Playwright's report format is a tree structure, where each suite can contain multiple specs and sub-suites. +// The function traverses the tree and collects failed test cases from the leaf nodes. +func (p Playwright) getFailedTestCasesFromSuite(suite PlaywrightReportSuite, suiteName string) []plan.TestCase { + var failedTests []plan.TestCase + + for _, spec := range suite.Specs { + if !spec.Ok { + projectName := spec.Tests[0].ProjectName + failedTests = append(failedTests, plan.TestCase{ + Name: spec.Title, + Path: fmt.Sprintf("%s:%d", spec.File, spec.Line), + // The scope has to match with the scope generated by Buildkite test collector. + // In Buildkite test collector, the scope is generated using Playwright built-in reporter function, titlePath(). + // titlePath function returns an array of suite's title from the root suite down to the current test, + // which is then joined with a space separator to form the scope. + // For more details, see: + // [Buildkite Test Collector - Playwright implementation](https://github.com/buildkite/test-collector-javascript/blob/42b803a618a15a07edf0169038ef4b5eba88f98d/playwright/reporter.js#L47) + // [Playwright titlePath implementation](https://github.com/microsoft/playwright/blob/523e50088a7f982dd96aacdb260dfbd1189159b1/packages/playwright/src/common/test.ts#L126) + // [Playwright suite structure](https://playwright.dev/docs/api/class-suite) + Scope: fmt.Sprintf(" %s %s %s", projectName, suiteName, spec.Title), + }) + } + } + + for _, subSuite := range suite.Suites { + failedTests = append(failedTests, p.getFailedTestCasesFromSuite(subSuite, fmt.Sprintf("%s %s", suiteName, subSuite.Title))...) + } + + return failedTests +} + func (p Playwright) commandNameAndArgs(cmd string, testCases []string) (string, []string, error) { words, err := shellquote.Split(cmd) if err != nil { @@ -123,6 +156,10 @@ func (p Playwright) GetExamples(files []string) ([]plan.TestCase, error) { return nil, fmt.Errorf("not supported in Playwright") } +type PlaywrightTest struct { + ProjectName string +} + type PlaywrightSpec struct { File string Line int @@ -130,11 +167,13 @@ type PlaywrightSpec struct { Id string Title string Ok bool + Tests []PlaywrightTest } type PlaywrightReportSuite struct { - Title string - Specs []PlaywrightSpec + Title string + Specs []PlaywrightSpec + Suites []PlaywrightReportSuite } type PlaywrightReport struct { diff --git a/internal/runner/playwright_test.go b/internal/runner/playwright_test.go index ffcd3bcd..2c615b67 100644 --- a/internal/runner/playwright_test.go +++ b/internal/runner/playwright_test.go @@ -1,8 +1,10 @@ package runner import ( + "os" "testing" + "github.com/buildkite/test-engine-client/internal/plan" "github.com/google/go-cmp/cmp" ) @@ -14,19 +16,21 @@ func TestPlaywrightRun(t *testing.T) { ResultPath: "playwright.json", }) - files := []string{"./testdata/playwright/tests/example.spec.js"} - got, err := playwright.Run(files, false) + testCases := []plan.TestCase{ + {Path: "./testdata/playwright/tests/example.spec.js"}, + } + got, err := playwright.Run(testCases, false) want := RunResult{ Status: RunStatusPassed, } if err != nil { - t.Errorf("Playwright.Run(%q) error = %v", files, err) + t.Errorf("Playwright.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Playwright.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Playwright.Run(%q) diff (-got +want):\n%s", testCases, diff) } } @@ -37,20 +41,37 @@ func TestPlaywrightRun_TestFailed(t *testing.T) { ResultPath: "test-results/results.json", }) - files := []string{"./tests/failed.spec.js"} - got, err := playwright.Run(files, false) + t.Cleanup(func() { + os.Remove(playwright.ResultPath) + }) + + testCases := []plan.TestCase{ + {Path: "./tests/failed.spec.js"}, + } + got, err := playwright.Run(testCases, false) want := RunResult{ - Status: RunStatusFailed, - FailedTests: []string{"failed.spec.js:3"}, + Status: RunStatusFailed, + FailedTests: []plan.TestCase{ + { + Scope: " chromium failed.spec.js test group failed", + Path: "failed.spec.js:5", + Name: "failed", + }, + { + Scope: " firefox failed.spec.js test group failed", + Path: "failed.spec.js:5", + Name: "failed", + }, + }, } if err != nil { - t.Errorf("Playwright.Run(%q) error = %v", files, err) + t.Errorf("Playwright.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Playwright.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Playwright.Run(%q) diff (-got +want):\n%s", testCases, diff) } } diff --git a/internal/runner/result.go b/internal/runner/result.go index a0d64a78..96081d47 100644 --- a/internal/runner/result.go +++ b/internal/runner/result.go @@ -1,5 +1,7 @@ package runner +import "github.com/buildkite/test-engine-client/internal/plan" + type RunStatus string const ( @@ -10,5 +12,5 @@ const ( type RunResult struct { Status RunStatus - FailedTests []string + FailedTests []plan.TestCase } diff --git a/internal/runner/rspec.go b/internal/runner/rspec.go index 9a0a5993..7985c173 100644 --- a/internal/runner/rspec.go +++ b/internal/runner/rspec.go @@ -70,14 +70,19 @@ func (r Rspec) GetFiles() ([]string, error) { // output cannot be parsed. // // Test failure is not considered an error, and is instead returned as a RunResult. -func (r Rspec) Run(testCases []string, retry bool) (RunResult, error) { +func (r Rspec) Run(testCases []plan.TestCase, retry bool) (RunResult, error) { command := r.TestCommand if retry { command = r.RetryTestCommand } - commandName, commandArgs, err := r.commandNameAndArgs(command, testCases) + testPaths := make([]string, len(testCases)) + for i, tc := range testCases { + testPaths[i] = tc.Path + } + + commandName, commandArgs, err := r.commandNameAndArgs(command, testPaths) if err != nil { return RunResult{Status: RunStatusError}, fmt.Errorf("failed to build command: %w", err) } @@ -104,10 +109,10 @@ func (r Rspec) Run(testCases []string, retry bool) (RunResult, error) { } if report.Summary.FailureCount > 0 { - var failedTests []string + var failedTests []plan.TestCase for _, example := range report.Examples { if example.Status == "failed" { - failedTests = append(failedTests, example.Id) + failedTests = append(failedTests, mapExampleToTestCase(example)) } } return RunResult{Status: RunStatusFailed, FailedTests: failedTests}, nil @@ -214,13 +219,26 @@ func (r Rspec) GetExamples(files []string) ([]plan.TestCase, error) { var testCases []plan.TestCase for _, example := range report.Examples { - testCases = append(testCases, plan.TestCase{ - Identifier: example.Id, - Name: example.Description, - Path: example.Id, - Scope: example.FullDescription, - }) + testCases = append(testCases, mapExampleToTestCase(example)) } return testCases, nil } + +func mapExampleToTestCase(example RspecExample) plan.TestCase { + // The scope and name has to match with the scope generated by Buildkite test collector. + // In Buildkite test collector, the scope is generated from `example_group.metadata[:full_description]` + // that doesn't include the test description. + // However, the `example_group.metadata` attribute is not available in the RSpec JSON report. + // The RSpec JSON report only contains the `full_description` attribute that includes the test description. + // Therefore, we need to remove the test description from the `full_description` attribute to match the scope. + // For more details, see: + // [Buildkite Test Collector - RSpec implementation](https://github.com/buildkite/test-collector-ruby/blob/2d641486e42f666dd07ffed4cbf2cd0f9dc97619/lib/buildkite/test_collector/rspec_plugin/trace.rb#L27) + scope := strings.TrimSuffix(example.FullDescription, " "+example.Description) + return plan.TestCase{ + Identifier: example.Id, + Name: example.Description, + Path: example.Id, + Scope: scope, + } +} diff --git a/internal/runner/rspec_test.go b/internal/runner/rspec_test.go index 291081f3..afde2b3e 100644 --- a/internal/runner/rspec_test.go +++ b/internal/runner/rspec_test.go @@ -68,19 +68,21 @@ func TestRspecRun(t *testing.T) { rspec := NewRspec(RunnerConfig{ TestCommand: "rspec", }) - files := []string{"./testdata/rspec/spec/spells/expelliarmus_spec.rb"} - got, err := rspec.Run(files, false) + testCases := []plan.TestCase{ + {Path: "./testdata/rspec/spec/spells/expelliarmus_spec.rb"}, + } + got, err := rspec.Run(testCases, false) want := RunResult{ Status: RunStatusPassed, } if err != nil { - t.Errorf("Rspec.Run(%q) error = %v", files, err) + t.Errorf("Rspec.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", testCases, diff) } } @@ -91,19 +93,20 @@ func TestRspecRun_RetryCommand(t *testing.T) { RetryTestCommand: "rspec", }, } - files := []string{} - got, err := rspec.Run(files, true) + + testCases := []plan.TestCase{} + got, err := rspec.Run(testCases, true) want := RunResult{ Status: RunStatusPassed, } if err != nil { - t.Errorf("Rspec.Run(%q) error = %v", files, err) + t.Errorf("Rspec.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", testCases, diff) } } @@ -117,20 +120,29 @@ func TestRspecRun_TestFailedWithResultFile(t *testing.T) { os.Remove(rspec.ResultPath) }) - files := []string{"./testdata/rspec/spec/failure_spec.rb"} - got, err := rspec.Run(files, false) + testCases := []plan.TestCase{ + {Path: "./testdata/rspec/spec/failure_spec.rb"}, + } + got, err := rspec.Run(testCases, false) want := RunResult{ - Status: RunStatusFailed, - FailedTests: []string{"./testdata/rspec/spec/failure_spec.rb[1:1]"}, + Status: RunStatusFailed, + FailedTests: []plan.TestCase{ + { + Name: "fails", + Scope: "Failure", + Identifier: "./testdata/rspec/spec/failure_spec.rb[1:1]", + Path: "./testdata/rspec/spec/failure_spec.rb[1:1]", + }, + }, } if err != nil { - t.Errorf("Rspec.Run(%q) error = %v", files, err) + t.Errorf("Rspec.Run(%q) error = %v", testCases, err) } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", testCases, diff) } } @@ -143,20 +155,22 @@ func TestRspecRun_TestFailedWithoutResultFile(t *testing.T) { os.Remove(rspec.ResultPath) }) - files := []string{"./testdata/rspec/spec/failure_spec.rb"} - got, err := rspec.Run(files, false) + testCases := []plan.TestCase{ + {Path: "./testdata/rspec/spec/failure_spec.rb"}, + } + got, err := rspec.Run(testCases, false) want := RunResult{ Status: RunStatusError, } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", testCases, diff) } exitError := new(exec.ExitError) if !errors.As(err, &exitError) { - t.Errorf("Rspec.Run(%q) error type = %T (%v), want *exec.ExitError", files, err, err) + t.Errorf("Rspec.Run(%q) error type = %T (%v), want *exec.ExitError", testCases, err, err) } } @@ -166,20 +180,20 @@ func TestRspecRun_CommandFailed(t *testing.T) { TestCommand: "rspec --invalid-option", }, } - files := []string{} - got, err := rspec.Run(files, false) + testCases := []plan.TestCase{} + got, err := rspec.Run(testCases, false) want := RunResult{ Status: RunStatusError, } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", testCases, diff) } exitError := new(exec.ExitError) if !errors.As(err, &exitError) { - t.Errorf("Rspec.Run(%q) error type = %T (%v), want *exec.ExitError", files, err, err) + t.Errorf("Rspec.Run(%q) error type = %T (%v), want *exec.ExitError", testCases, err, err) } } @@ -187,24 +201,26 @@ func TestRspecRun_SignaledError(t *testing.T) { rspec := NewRspec(RunnerConfig{ TestCommand: "./testdata/segv.sh", }) - files := []string{"./testdata/rspec/spec/failure_spec.rb"} + testCases := []plan.TestCase{ + {Path: "./testdata/rspec/spec/failure_spec.rb"}, + } - got, err := rspec.Run(files, false) + got, err := rspec.Run(testCases, false) want := RunResult{ Status: RunStatusError, } if diff := cmp.Diff(got, want); diff != "" { - t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", files, diff) + t.Errorf("Rspec.Run(%q) diff (-got +want):\n%s", testCases, diff) } signalError := new(ProcessSignaledError) if !errors.As(err, &signalError) { - t.Errorf("Rspec.Run(%q) error type = %T (%v), want *ErrProcessSignaled", files, err, err) + t.Errorf("Rspec.Run(%q) error type = %T (%v), want *ErrProcessSignaled", testCases, err, err) } if signalError.Signal != syscall.SIGSEGV { - t.Errorf("Rspec.Run(%q) signal = %d, want %d", files, syscall.SIGSEGV, signalError.Signal) + t.Errorf("Rspec.Run(%q) signal = %d, want %d", testCases, syscall.SIGSEGV, signalError.Signal) } } @@ -299,13 +315,13 @@ func TestRspecGetExamples(t *testing.T) { Identifier: "./testdata/rspec/spec/spells/expelliarmus_spec.rb[1:1]", Name: "disarms the opponent", Path: "./testdata/rspec/spec/spells/expelliarmus_spec.rb[1:1]", - Scope: "Expelliarmus disarms the opponent", + Scope: "Expelliarmus", }, { Identifier: "./testdata/rspec/spec/spells/expelliarmus_spec.rb[1:2]", Name: "knocks the wand out of the opponents hand", Path: "./testdata/rspec/spec/spells/expelliarmus_spec.rb[1:2]", - Scope: "Expelliarmus knocks the wand out of the opponents hand", + Scope: "Expelliarmus", }, } @@ -325,13 +341,13 @@ func TestRspecGetExamples_WithOtherFormatters(t *testing.T) { Identifier: "./testdata/rspec/spec/spells/expelliarmus_spec.rb[1:1]", Name: "disarms the opponent", Path: "./testdata/rspec/spec/spells/expelliarmus_spec.rb[1:1]", - Scope: "Expelliarmus disarms the opponent", + Scope: "Expelliarmus", }, { Identifier: "./testdata/rspec/spec/spells/expelliarmus_spec.rb[1:2]", Name: "knocks the wand out of the opponents hand", Path: "./testdata/rspec/spec/spells/expelliarmus_spec.rb[1:2]", - Scope: "Expelliarmus knocks the wand out of the opponents hand", + Scope: "Expelliarmus", }, } @@ -378,7 +394,7 @@ func TestRspecGetExamples_WithSharedExamples(t *testing.T) { Identifier: "./testdata/rspec/spec/specs_with_shared_examples_spec.rb[1:1:1]", Name: "behaves like a shared example", Path: "./testdata/rspec/spec/specs_with_shared_examples_spec.rb[1:1:1]", - Scope: "Specs with shared examples behaves like shared behaves like a shared example", + Scope: "Specs with shared examples behaves like shared", }, } diff --git a/internal/runner/testdata/playwright/playwright.config.js b/internal/runner/testdata/playwright/playwright.config.js index 9b451ad4..b2d7be91 100644 --- a/internal/runner/testdata/playwright/playwright.config.js +++ b/internal/runner/testdata/playwright/playwright.config.js @@ -16,5 +16,15 @@ module.exports = defineConfig({ use: { baseURL: 'http://localhost:8080/', }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + ], }); diff --git a/internal/runner/testdata/playwright/tests/failed.spec.js b/internal/runner/testdata/playwright/tests/failed.spec.js index 76a6bf31..649da6ce 100644 --- a/internal/runner/testdata/playwright/tests/failed.spec.js +++ b/internal/runner/testdata/playwright/tests/failed.spec.js @@ -1,9 +1,10 @@ import { test, expect } from '@playwright/test'; -test('says good bye', async ({ page }) => { - await page.goto('/'); - await expect(page).toHaveText('good bye'); +test.describe('test group', () => { + test('failed', () => { + expect(1).toBe(2); + }) }); test('it passes', () => { diff --git a/main.go b/main.go index 17f73587..56ea033e 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,7 @@ import ( var Version = "" type TestRunner interface { - Run(testCases []string, retry bool) (runner.RunResult, error) + Run(testCases []plan.TestCase, retry bool) (runner.RunResult, error) GetExamples(files []string) ([]plan.TestCase, error) GetFiles() ([]string, error) Name() string @@ -78,13 +78,8 @@ func main() { thisNodeTask := testPlan.Tasks[strconv.Itoa(cfg.NodeIndex)] // execute tests - runnableTests := []string{} - for _, testCase := range thisNodeTask.Tests { - runnableTests = append(runnableTests, testCase.Path) - } - var timeline []api.Timeline - testResult, err := runTestsWithRetry(testRunner, &runnableTests, cfg.MaxRetries, &timeline) + testResult, err := runTestsWithRetry(testRunner, &thisNodeTask.Tests, cfg.MaxRetries, testPlan.MutedTests, &timeline) if err != nil { if ProcessSignaledError := new(runner.ProcessSignaledError); errors.As(err, &ProcessSignaledError) { @@ -134,7 +129,7 @@ func sendMetadata(ctx context.Context, apiClient *api.Client, cfg config.Config, } } -func runTestsWithRetry(testRunner TestRunner, testsCases *[]string, maxRetries int, timeline *[]api.Timeline) (runner.RunResult, error) { +func runTestsWithRetry(testRunner TestRunner, testsCases *[]plan.TestCase, maxRetries int, mutedTest []plan.TestCase, timeline *[]api.Timeline) (runner.RunResult, error) { attemptCount := 0 var testResult runner.RunResult @@ -157,6 +152,34 @@ func runTestsWithRetry(testRunner TestRunner, testsCases *[]string, maxRetries i testResult, err = testRunner.Run(*testsCases, attemptCount > 0) + // Filter out muted tests from the failed tests. + if len(mutedTest) > 0 && testResult.Status == runner.RunStatusFailed { + fmt.Println("⚠️ The following tests are muted and will not be retried or affect the test run result:") + for _, test := range mutedTest { + fmt.Printf("%s - %s %s\n", test.Path, test.Scope, test.Name) + } + + mutedTestMap := make(map[string]bool) + for _, test := range mutedTest { + scopeName := test.Scope + "/" + test.Name + mutedTestMap[scopeName] = true + } + + var failedTests []plan.TestCase + for _, test := range testResult.FailedTests { + scopeName := test.Scope + "/" + test.Name + if _, ok := mutedTestMap[scopeName]; !ok { + failedTests = append(failedTests, test) + } + } + + testResult.FailedTests = failedTests + + if len(failedTests) == 0 { + testResult.Status = runner.RunStatusPassed + } + } + if attemptCount == 0 { *timeline = append(*timeline, api.Timeline{ Event: "test_end", diff --git a/main_test.go b/main_test.go index 6f1853d7..07025b0f 100644 --- a/main_test.go +++ b/main_test.go @@ -28,9 +28,13 @@ func TestRunTestsWithRetry(t *testing.T) { }, } maxRetries := 3 - testCases := []string{"testdata/rspec/spec/fruits/apple_spec.rb"} + testCases := []plan.TestCase{ + { + Path: "./testdata/rspec/spec/fruits/apple_spec.rb", + }, + } timeline := []api.Timeline{} - testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, &timeline) + testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, []plan.TestCase{}, &timeline) t.Cleanup(func() { os.Remove(testRunner.ResultPath) @@ -67,9 +71,16 @@ func TestRunTestsWithRetry_TestPassedAfterRetry(t *testing.T) { }, } maxRetries := 2 - testCases := []string{"testdata/rspec/spec/fruits/apple_spec.rb", "testdata/rspec/spec/fruits/tomato_spec.rb"} + testCases := []plan.TestCase{ + { + Path: "./testdata/rspec/spec/fruits/apple_spec.rb", + }, + { + Path: "./testdata/rspec/spec/fruits/tomato_spec.rb", + }, + } timeline := []api.Timeline{} - testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, &timeline) + testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, []plan.TestCase{}, &timeline) t.Cleanup(func() { os.Remove(testRunner.ResultPath) @@ -83,7 +94,16 @@ func TestRunTestsWithRetry_TestPassedAfterRetry(t *testing.T) { t.Errorf("runTestsWithRetry(...) testResult.Status = %v, want %v", testResult.Status, runner.RunStatusPassed) } - if diff := cmp.Diff(testCases, []string{"./testdata/rspec/spec/fruits/tomato_spec.rb[1:2]"}); diff != "" { + retriedTestCases := []plan.TestCase{ + { + Scope: "Tomato", + Name: "is vegetable", + Path: "./testdata/rspec/spec/fruits/tomato_spec.rb[1:2]", + Identifier: "./testdata/rspec/spec/fruits/tomato_spec.rb[1:2]", + }, + } + + if diff := cmp.Diff(testCases, retriedTestCases); diff != "" { t.Errorf("testCases diff (-got +want):\n%s", diff) } @@ -109,9 +129,16 @@ func TestRunTestsWithRetry_TestFailedAfterRetry(t *testing.T) { }, } maxRetries := 2 - testCases := []string{"testdata/rspec/spec/fruits/apple_spec.rb", "testdata/rspec/spec/fruits/tomato_spec.rb"} + testCases := []plan.TestCase{ + { + Path: "testdata/rspec/spec/fruits/apple_spec.rb", + }, + { + Path: "testdata/rspec/spec/fruits/tomato_spec.rb", + }, + } timeline := []api.Timeline{} - testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, &timeline) + testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, []plan.TestCase{}, &timeline) t.Cleanup(func() { os.Remove(testRunner.ResultPath) @@ -125,11 +152,20 @@ func TestRunTestsWithRetry_TestFailedAfterRetry(t *testing.T) { t.Errorf("runTestsWithRetry(...) testResult.Status = %v, want %v", testResult.Status, runner.RunStatusFailed) } - if diff := cmp.Diff(testResult.FailedTests, []string{"./testdata/rspec/spec/fruits/tomato_spec.rb[1:2]"}); diff != "" { + wantFailedTests := []plan.TestCase{ + { + Scope: "Tomato", + Name: "is vegetable", + Path: "./testdata/rspec/spec/fruits/tomato_spec.rb[1:2]", + Identifier: "./testdata/rspec/spec/fruits/tomato_spec.rb[1:2]", + }, + } + + if diff := cmp.Diff(testResult.FailedTests, wantFailedTests); diff != "" { t.Errorf("runTestsWithRetry(...) testResult.FailedTests diff (-got +want):\n%s", diff) } - if diff := cmp.Diff(testCases, []string{"./testdata/rspec/spec/fruits/tomato_spec.rb[1:2]"}); diff != "" { + if diff := cmp.Diff(testCases, wantFailedTests); diff != "" { t.Errorf("testCases diff (-got +want):\n%s", diff) } @@ -146,6 +182,56 @@ func TestRunTestsWithRetry_TestFailedAfterRetry(t *testing.T) { } } +func TestRunTestsWithRetry_MutedTest(t *testing.T) { + testRunner := runner.Rspec{ + RunnerConfig: runner.RunnerConfig{ + TestCommand: "rspec --format json --out {{resultPath}} --format documentation", + ResultPath: "tmp/rspec.json", + RetryTestCommand: "rspec --format json --out {{resultPath}}", + }, + } + maxRetries := 1 + testCases := []plan.TestCase{ + { + Path: "./testdata/rspec/spec/fruits/apple_spec.rb", + }, + { + // File with failed tests that are muted + Path: "./testdata/rspec/spec/fruits/tomato_spec.rb", + }, + } + mutedTests := []plan.TestCase{ + {Path: "apple_spec.rb:6", Scope: "Apple", Name: "is red"}, + {Path: "tomato_spec.rb:6", Scope: "Tomato", Name: "is vegetable"}, + } + timeline := []api.Timeline{} + testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, mutedTests, &timeline) + + t.Cleanup(func() { + os.Remove(testRunner.ResultPath) + }) + + if err != nil { + t.Errorf("runTestsWithRetry(...) error = %v", err) + } + + if testResult.Status != runner.RunStatusPassed { + t.Errorf("runTestsWithRetry(...) testResult.Status = %v, want %v", testResult.Status, runner.RunStatusPassed) + } + + if len(timeline) != 2 { + t.Errorf("timeline length = %v, want %d", len(timeline), 2) + } + + events := []string{} + for _, event := range timeline { + events = append(events, event.Event) + } + if diff := cmp.Diff(events, []string{"test_start", "test_end"}); diff != "" { + t.Errorf("timeline events diff (-got +want):\n%s", diff) + } +} + func TestRunTestsWithRetry_Error(t *testing.T) { testRunner := runner.Rspec{ RunnerConfig: runner.RunnerConfig{ @@ -153,9 +239,11 @@ func TestRunTestsWithRetry_Error(t *testing.T) { }, } maxRetries := 2 - testCases := []string{"testdata/rspec/spec/fruits/fig_spec.rb"} + testCases := []plan.TestCase{ + {Path: "testdata/rspec/spec/fruits/fig_spec.rb"}, + } timeline := []api.Timeline{} - testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, &timeline) + testResult, err := runTestsWithRetry(testRunner, &testCases, maxRetries, []plan.TestCase{}, &timeline) exitError := new(exec.ExitError) if !errors.As(err, &exitError) { @@ -519,19 +607,19 @@ func TestCreateRequestParams(t *testing.T) { Identifier: "./testdata/rspec/spec/fruits/banana_spec.rb[1:1]", Name: "is yellow", Path: "./testdata/rspec/spec/fruits/banana_spec.rb[1:1]", - Scope: "Banana is yellow", + Scope: "Banana", }, { Identifier: "./testdata/rspec/spec/fruits/banana_spec.rb[1:2:1]", Name: "is green", Path: "./testdata/rspec/spec/fruits/banana_spec.rb[1:2:1]", - Scope: "Banana when not ripe is green", + Scope: "Banana when not ripe", }, { Identifier: "./testdata/rspec/spec/fruits/fig_spec.rb[1:1]", Name: "is purple", Path: "./testdata/rspec/spec/fruits/fig_spec.rb[1:1]", - Scope: "Fig is purple", + Scope: "Fig", }, }, },