Skip to content

Mark junit test cases as skipped if no pickle step results available #597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 54 additions & 15 deletions internal/formatters/fmt_junit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"time"

"github.com/cucumber/godog/formatters"
"github.com/cucumber/godog/internal/models"
"github.com/cucumber/godog/internal/utils"
)

Expand Down Expand Up @@ -47,6 +48,40 @@
return strconv.FormatFloat(to.Sub(from).Seconds(), 'f', -1, 64)
}

// getPickleResult deals with the fact that if there's no result due to 'StopOnFirstFailure' being
// set, MustGetPickleResult panics.
func (f *JUnit) getPickleResult(pickleID string) (res *models.PickleResult) {
defer func() {
if r := recover(); r != nil {
res = nil
}
}()
pr := f.Storage.MustGetPickleResult(pickleID)
res = &pr
return
}

func (f *JUnit) getPickleStepResult(stepID string) (res *models.PickleStepResult) {
defer func() {
if r := recover(); r != nil {
res = nil
}
}()
psr := f.Storage.MustGetPickleStepResult(stepID)
res = &psr
return
}

func (f *JUnit) getPickleStepResultsByPickleID(pickleID string) (res []models.PickleStepResult) {
defer func() {
if r := recover(); r != nil {
res = nil
}

Check warning on line 79 in internal/formatters/fmt_junit.go

View check run for this annotation

Codecov / codecov/patch

internal/formatters/fmt_junit.go#L78-L79

Added lines #L78 - L79 were not covered by tests
}()
res = f.Storage.MustGetPickleStepResultsByPickleID(pickleID)
return
}

func (f *JUnit) buildJUNITPackageSuite() JunitPackageSuite {
features := f.Storage.MustGetFeatures()
sort.Sort(sortFeaturesByName(features))
Expand Down Expand Up @@ -79,33 +114,37 @@
var outlineNo = make(map[string]int)
for idx, pickle := range pickles {
tc := junitTestCase{}

pickleResult := f.Storage.MustGetPickleResult(pickle.Id)

if idx == 0 {
firstPickleStartedAt = pickleResult.StartedAt
tc.Name = pickle.Name
if testcaseNames[tc.Name] > 1 {
outlineNo[tc.Name] = outlineNo[tc.Name] + 1
tc.Name += fmt.Sprintf(" #%d", outlineNo[tc.Name])
}

lastPickleFinishedAt = pickleResult.StartedAt
pickleResult := f.getPickleResult(pickle.Id)
if pickleResult == nil {
tc.Status = skipped.String()
} else {
if idx == 0 {
firstPickleStartedAt = pickleResult.StartedAt
}
lastPickleFinishedAt = pickleResult.StartedAt
}

if len(pickle.Steps) > 0 {
lastStep := pickle.Steps[len(pickle.Steps)-1]
lastPickleStepResult := f.Storage.MustGetPickleStepResult(lastStep.Id)
lastPickleFinishedAt = lastPickleStepResult.FinishedAt
if lastPickleStepResult := f.getPickleStepResult(lastStep.Id); lastPickleStepResult != nil {
lastPickleFinishedAt = lastPickleStepResult.FinishedAt
}
}

tc.Time = junitTimeDuration(pickleResult.StartedAt, lastPickleFinishedAt)

tc.Name = pickle.Name
if testcaseNames[tc.Name] > 1 {
outlineNo[tc.Name] = outlineNo[tc.Name] + 1
tc.Name += fmt.Sprintf(" #%d", outlineNo[tc.Name])
if pickleResult != nil {
tc.Time = junitTimeDuration(pickleResult.StartedAt, lastPickleFinishedAt)
}

ts.Tests++
suite.Tests++

pickleStepResults := f.Storage.MustGetPickleStepResultsByPickleID(pickle.Id)
pickleStepResults := f.getPickleStepResultsByPickleID(pickle.Id)
for _, stepResult := range pickleStepResults {
pickleStep := f.Storage.MustGetPickleStep(stepResult.PickleStepID)

Expand Down
138 changes: 138 additions & 0 deletions internal/formatters/fmt_junit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package formatters_test

import (
"bytes"
"encoding/xml"
"fmt"
"testing"

"github.com/cucumber/godog"
)

func Test_JUnitFormatter_StopOnFirstFailure(t *testing.T) {
featureFile := "formatter-tests/features/stop_on_first_failure.feature"

// First, verify the normal output (without StopOnFirstFailure)
var normalBuf bytes.Buffer
normalOpts := godog.Options{
Format: "junit",
Paths: []string{featureFile},
Output: &normalBuf,
Strict: true,
}

normalSuite := godog.TestSuite{
Name: "Normal Run",
ScenarioInitializer: func(sc *godog.ScenarioContext) {
setupStopOnFailureSteps(sc)
},
Options: &normalOpts,
}
if status := normalSuite.Run(); status != 1 {
t.Fatalf("Expected suite to have status 1, but got %d", status)
}

// Parse the XML output
var normalResult JunitPackageSuite
err := xml.Unmarshal(normalBuf.Bytes(), &normalResult)
if err != nil {
t.Fatalf("Failed to parse XML output: %v", err)
}

// Now run with StopOnFirstFailure
var stopBuf bytes.Buffer
stopOpts := godog.Options{
Format: "junit",
Paths: []string{featureFile},
Output: &stopBuf,
Strict: true,
StopOnFailure: true,
}

stopSuite := godog.TestSuite{
Name: "Stop On First Failure",
ScenarioInitializer: func(sc *godog.ScenarioContext) {
setupStopOnFailureSteps(sc)
},
Options: &stopOpts,
}
if status := stopSuite.Run(); status != 1 {
t.Fatalf("Expected suite to have status 1, but got %d", status)
}

// Parse the XML output
var stopResult JunitPackageSuite
err = xml.Unmarshal(stopBuf.Bytes(), &stopResult)
if err != nil {
t.Fatalf("Failed to parse XML output: %v", err)
}

// Verify the second test case is marked as skipped when StopOnFirstFailure is enabled
if len(stopResult.TestSuites) == 0 || len(stopResult.TestSuites[0].TestCases) < 2 {
t.Fatal("Expected at least 2 test cases in the results")
}

// In a normal run, second test case should not be skipped
if normalResult.TestSuites[0].TestCases[1].Status == "skipped" {
t.Errorf("In normal run, second test case should not be skipped")
}

// In stop on failure run, second test case should be skipped
if stopResult.TestSuites[0].TestCases[1].Status != "skipped" {
t.Errorf("In stop on failure run, second test case should be skipped, but got %s",
stopResult.TestSuites[0].TestCases[1].Status)
}
}

// setupStopOnFailureSteps registers the step definitions for the stop-on-failure test
func setupStopOnFailureSteps(sc *godog.ScenarioContext) {
sc.Step(`^a passing step$`, func() error {
return nil
})
sc.Step(`^a failing step$`, func() error {
return fmt.Errorf("step failed")
})
}

// JunitPackageSuite represents the JUnit XML structure for test suites
type JunitPackageSuite struct {
XMLName xml.Name `xml:"testsuites"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"`
Skipped int `xml:"skipped,attr"`
Failures int `xml:"failures,attr"`
Errors int `xml:"errors,attr"`
Time string `xml:"time,attr"`
TestSuites []*JunitTestSuite `xml:"testsuite"`
}

type JunitTestSuite struct {
XMLName xml.Name `xml:"testsuite"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"`
Skipped int `xml:"skipped,attr"`
Failures int `xml:"failures,attr"`
Errors int `xml:"errors,attr"`
Time string `xml:"time,attr"`
TestCases []*JunitTestCase `xml:"testcase"`
}

type JunitTestCase struct {
XMLName xml.Name `xml:"testcase"`
Name string `xml:"name,attr"`
Status string `xml:"status,attr"`
Time string `xml:"time,attr"`
Failure *JunitFailure `xml:"failure,omitempty"`
Error []*JunitError `xml:"error,omitempty"`
}

type JunitFailure struct {
Message string `xml:"message,attr"`
Type string `xml:"type,attr,omitempty"`
}

type JunitError struct {
XMLName xml.Name `xml:"error,omitempty"`
Message string `xml:"message,attr"`
Type string `xml:"type,attr"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Feature: Stop on first failure

Scenario: First scenario - should run and fail
Given a passing step
When a failing step
Then a passing step

Scenario: Second scenario - should be skipped
Given a passing step
Then a passing step
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<bold-white>Feature:</bold-white> Stop on first failure

<bold-white>Scenario:</bold-white> First scenario - should run and fail <bold-black># formatter-tests/features/stop_on_first_failure.feature:3</bold-black>
<green>Given</green> <green>a passing step</green> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef</bold-black>
<red>When</red> <red>a failing step</red> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.failingStepDef</bold-black>
<bold-red>step failed</bold-red>
<cyan>Then</cyan> <cyan>a passing step</cyan> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef</bold-black>

<bold-white>Scenario:</bold-white> Second scenario - should be skipped <bold-black># formatter-tests/features/stop_on_first_failure.feature:8</bold-black>
<green>Given</green> <green>a passing step</green> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef</bold-black>
<green>Then</green> <green>a passing step</green> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef</bold-black>
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="junit,pretty" tests="2" skipped="0" failures="1" errors="0" time="0">
<testsuite name="Stop on first failure" tests="2" skipped="0" failures="1" errors="0" time="0">
<testcase name="First scenario - should run and fail" status="failed" time="0">
<failure message="Step a failing step: step failed"></failure>
<error message="Step a passing step" type="skipped"></error>
</testcase>
<testcase name="Second scenario - should be skipped" status="passed" time="0"></testcase>
</testsuite>
</testsuites>
--- <red>Failed steps:</red>

<red>Scenario: First scenario - should run and fail</red> <bold-black># formatter-tests/features/stop_on_first_failure.feature:3</bold-black>
<red>When a failing step</red> <bold-black># formatter-tests/features/stop_on_first_failure.feature:5</bold-black>
<red>Error: </red><bold-red>step failed</bold-red>


2 scenarios (<green>1 passed</green>, <red>1 failed</red>)
5 steps (<green>3 passed</green>, <red>1 failed</red>, <cyan>1 skipped</cyan>)
0s
10 changes: 10 additions & 0 deletions internal/formatters/formatter-tests/junit/stop_on_first_failure
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="junit" tests="2" skipped="0" failures="1" errors="0" time="0">
<testsuite name="Stop on first failure" tests="2" skipped="0" failures="1" errors="0" time="0">
<testcase name="First scenario - should run and fail" status="failed" time="0">
<failure message="Step a failing step: step failed"></failure>
<error message="Step a passing step" type="skipped"></error>
</testcase>
<testcase name="Second scenario - should be skipped" status="passed" time="0"></testcase>
</testsuite>
</testsuites>
22 changes: 22 additions & 0 deletions internal/formatters/formatter-tests/pretty/stop_on_first_failure
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<bold-white>Feature:</bold-white> Stop on first failure

<bold-white>Scenario:</bold-white> First scenario - should run and fail <bold-black># formatter-tests/features/stop_on_first_failure.feature:3</bold-black>
<green>Given</green> <green>a passing step</green> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef</bold-black>
<red>When</red> <red>a failing step</red> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.failingStepDef</bold-black>
<bold-red>step failed</bold-red>
<cyan>Then</cyan> <cyan>a passing step</cyan> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef</bold-black>

<bold-white>Scenario:</bold-white> Second scenario - should be skipped <bold-black># formatter-tests/features/stop_on_first_failure.feature:8</bold-black>
<green>Given</green> <green>a passing step</green> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef</bold-black>
<green>Then</green> <green>a passing step</green> <bold-black># fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef</bold-black>

--- <red>Failed steps:</red>

<red>Scenario: First scenario - should run and fail</red> <bold-black># formatter-tests/features/stop_on_first_failure.feature:3</bold-black>
<red>When a failing step</red> <bold-black># formatter-tests/features/stop_on_first_failure.feature:5</bold-black>
<red>Error: </red><bold-red>step failed</bold-red>


2 scenarios (<green>1 passed</green>, <red>1 failed</red>)
5 steps (<green>3 passed</green>, <red>1 failed</red>, <cyan>1 skipped</cyan>)
0s
Loading