Repository: cucumber/godog
Pull request #668 (2024-11-27 20:14:28)

Print step declaration line instead of handler declaration line
### 🤔 What's changed?

Pretty printing results now prints the line where the step is declared instead of the line where the handler is declared.

### ⚡️ What's your motivation? 

Printing the file and line number of handlers often produces meaningless results. For example:
* Anonymous functions: `ctx.Step("...", func () {...})` always point to line 0: `some_file_test.go:0`
* Instance receiver methods: `ctx.Step("...", instance.Method)` always point to: `<autogenerated>:1`. 

Pointing to the step declaration does require an additional click to get to the handler code, so an alternative approach would be to improve the reflection process to do a better job locating the handler code.

### 🏷️ What kind of change is this?

<!--- Delete any options that are not relevant -->

- :bank: Refactoring/debt/DX (improvement to code design, tooling, etc. without changing behaviour)

This might be considered breaking if anyone is relying on the previous specific log format in CI tasks.

### ♻️ Anything particular you want feedback on?

<!-- 
Is there anything in this change you're unsure about, or would 
particularly like reviewers to give you feedback on?
-->

### 📋 Checklist:

<!--- 
This is to help you remember all the little things we often forget to do!

Feel free to delete any tasks that are not relevant, or add new ones.
-->

- [x] I agree to respect and uphold the [Cucumber Community Code of Conduct](https://github.com/cucumber/.github/tree/main?tab=coc-ov-file)
- [ ] I've changed the behaviour of the code
  - [ ] I have added/updated tests to cover my changes.
- [ ] My change requires a change to the documentation.
  - [ ] I have updated the documentation accordingly.
- [x] Users should know about my change
  - [x] I have added an entry to the "Unreleased" section of the [**CHANGELOG**](../blob/main/CHANGELOG.md), linking to this pull request.

----

*This text was originally generated from a [template](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/about-issue-and-pull-request-templates), then edited by hand. [You can modify the template here.](https://github.com/cucumber/.github/edit/main/.github/PULL_REQUEST_TEMPLATE.md)*



Patch:
diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml
index ef0c6da..80d012f 100644
--- a/.github/workflows/gorelease.yml
+++ b/.github/workflows/gorelease.yml
@@ -33,7 +33,7 @@ jobs:
           OUTPUT=$(gorelease 2>&1 || exit 0)
           echo "${OUTPUT}"
           OUTPUT="${OUTPUT//$'\n'/%0A}"
-          echo "::set-output name=report::$OUTPUT"
+          echo "report=$OUTPUT" >> $GITHUB_OUTPUT
       - name: Comment Report
         continue-on-error: true
         uses: marocchino/sticky-pull-request-comment@v2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d6c7e1..7f2413a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,16 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
 
 ## Unreleased
 
+### Added
+- Step text is added to "step is undefined" error - ([669](https://github.com/cucumber/godog/pull/669) - [vearutop](https://github.com/vearutop))
+
+### Changed
+- Replace deprecated `::set-output` - ([681](https://github.com/cucumber/godog/pull/681) - [nodeg](https://github.com/nodeg))
+
+### Fixed
+- fix(errors): fix(errors): Fix expected Step argument count for steps with `context.Context` ([679](https://github.com/cucumber/godog/pull/679) - [tigh-latte](https://github.com/tigh-latte))
 - fix(formatter): On concurrent execution, execute formatter at end of Scenario - ([645](https://github.com/cucumber/godog/pull/645) - [tigh-latte](https://github.com/tigh-latte))
+- Pretty printing results now prints the line where the step is declared instead of the line where the handler is declared. ([668](https://github.com/cucumber/godog/pull/668) - [spencerc](https://github.com/SpencerC))
 
 ## [v0.15.0]
 
diff --git a/Makefile b/Makefile
index aefccb1..06c95c4 100644
--- a/Makefile
+++ b/Makefile
@@ -23,8 +23,8 @@ check-go-version:
 test: check-go-version
 	@echo "running all tests"
 	@go fmt ./...
-	@go run honnef.co/go/tools/cmd/staticcheck@v0.4.7 github.com/cucumber/godog
-	@go run honnef.co/go/tools/cmd/staticcheck@v0.4.7 github.com/cucumber/godog/cmd/godog
+	@go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 github.com/cucumber/godog
+	@go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 github.com/cucumber/godog/cmd/godog
 	go vet ./...
 	go test -race ./...
 	go run ./cmd/godog -f progress -c 4
diff --git a/README.md b/README.md
index fbe765f..dceacf1 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Godog merges specification and test documentation into one cohesive whole.
 ### Living documentation
 
 Because they're automatically tested by Godog, your specifications are
-always bang up-to-date.
+always being up-to-date.
 
 ### Focus on the customer
 
diff --git a/_examples/go.mod b/_examples/go.mod
index b9792ba..adc37a0 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/DATA-DOG/go-txdb v0.1.6
 	github.com/cucumber/godog v0.15.0
 	github.com/go-sql-driver/mysql v1.7.1
-	github.com/spf13/pflag v1.0.5
+	github.com/spf13/pflag v1.0.7
 	github.com/stretchr/testify v1.8.2
 )
 
diff --git a/_examples/go.sum b/_examples/go.sum
index f31bb39..fbc2a57 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -45,6 +45,10 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
diff --git a/go.mod b/go.mod
index cb94034..c0b11c9 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/cucumber/gherkin/go/v26 v26.2.0
 	github.com/hashicorp/go-memdb v1.3.4
 	github.com/spf13/cobra v1.7.0
-	github.com/spf13/pflag v1.0.5
+	github.com/spf13/pflag v1.0.7
 	github.com/stretchr/testify v1.8.2
 )
 
diff --git a/go.sum b/go.sum
index 768a562..d27fbf6 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,10 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
 github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
diff --git a/internal/formatters/fmt.go b/internal/formatters/fmt.go
index 3efe1f4..5530c0c 100644
--- a/internal/formatters/fmt.go
+++ b/internal/formatters/fmt.go
@@ -78,9 +78,7 @@ func mustConvertStringToInt(s string) int {
 func DefinitionID(sd *models.StepDefinition) string {
 	ptr := sd.HandlerValue.Pointer()
 	f := runtime.FuncForPC(ptr)
-	file, line := f.FileLine(ptr)
-	dir := filepath.Dir(file)
-
+	dir := filepath.Dir(sd.File)
 	fn := strings.Replace(f.Name(), dir, "", -1)
 	var parts []string
 	for _, gr := range matchFuncDefRef.FindAllStringSubmatch(fn, -1) {
@@ -100,7 +98,7 @@ func DefinitionID(sd *models.StepDefinition) string {
 		fn = strings.Replace(fn, "..", ".", -1)
 	}
 
-	return fmt.Sprintf("%s:%d -> %s", filepath.Base(file), line, fn)
+	return fmt.Sprintf("%s:%d -> %s", filepath.Base(sd.File), sd.Line, fn)
 }
 
 var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`)
diff --git a/internal/formatters/fmt_junit.go b/internal/formatters/fmt_junit.go
index d7b2517..85acabe 100644
--- a/internal/formatters/fmt_junit.go
+++ b/internal/formatters/fmt_junit.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/cucumber/godog/formatters"
+	"github.com/cucumber/godog/internal/models"
 	"github.com/cucumber/godog/internal/utils"
 )
 
@@ -47,6 +48,40 @@ func junitTimeDuration(from, to time.Time) string {
 	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
+		}
+	}()
+	res = f.Storage.MustGetPickleStepResultsByPickleID(pickleID)
+	return
+}
+
 func (f *JUnit) buildJUNITPackageSuite() JunitPackageSuite {
 	features := f.Storage.MustGetFeatures()
 	sort.Sort(sortFeaturesByName(features))
@@ -79,33 +114,37 @@ func (f *JUnit) buildJUNITPackageSuite() JunitPackageSuite {
 		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)
 
diff --git a/internal/models/stepdef.go b/internal/models/stepdef.go
index 90ffef5..7c2e973 100644
--- a/internal/models/stepdef.go
+++ b/internal/models/stepdef.go
@@ -27,6 +27,8 @@ type StepDefinition struct {
 
 	Args         []interface{}
 	HandlerValue reflect.Value
+	File         string
+	Line         int
 
 	// multistep related
 	Nested    bool
@@ -54,7 +56,7 @@ func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}
 	}
 
 	if len(sd.Args) < numIn {
-		return ctx, fmt.Errorf("%w: expected %d arguments, matched %d from step", ErrUnmatchedStepArgumentNumber, typ.NumIn(), len(sd.Args))
+		return ctx, fmt.Errorf("%w: expected %d arguments, matched %d from step", ErrUnmatchedStepArgumentNumber, numIn, len(sd.Args))
 	}
 
 	for i := 0; i < numIn; i++ {
@@ -110,6 +112,56 @@ func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}
 				return ctx, fmt.Errorf(`%w %d: "%s" to int8: %s`, ErrCannotConvert, i, s, err)
 			}
 			values = append(values, reflect.ValueOf(int8(v)))
+		case reflect.Uint:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 0)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(uint(v)))
+		case reflect.Uint64:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 64)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint64: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(v))
+		case reflect.Uint32:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 32)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint32: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(uint32(v)))
+		case reflect.Uint16:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 16)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint16: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(uint16(v)))
+		case reflect.Uint8:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 8)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint8: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(uint8(v)))
 		case reflect.String:
 			s, err := sd.shouldBeString(i)
 			if err != nil {
diff --git a/suite.go b/suite.go
index 9a38729..6ca1bf5 100644
--- a/suite.go
+++ b/suite.go
@@ -159,7 +159,9 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
 
 		// Check for any calls to Fail on dogT
 		if err == nil {
-			err = getTestingT(ctx).isFailed()
+			if t := getTestingT(ctx); t != nil {
+				err = t.isFailed()
+			}
 		}
 
 		status := StepUndefined
@@ -258,6 +260,8 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
 				},
 				Args:         match.Args,
 				HandlerValue: match.HandlerValue,
+				File:         match.File,
+				Line:         match.Line,
 				Nested:       match.Nested,
 				Undefined:    undef,
 			}
@@ -270,7 +274,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
 		s.storage.MustInsertPickleStepResult(sr)
 
 		s.fmt.Undefined(pickle, step, match.GetInternalStepDefinition())
-		return ctx, ErrUndefined
+		return ctx, fmt.Errorf("%w: %s", ErrUndefined, step.Text)
 	}
 
 	if scenarioErr != nil {
@@ -461,7 +465,7 @@ func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context.
 		}
 
 		if def == nil {
-			return ctx, ErrUndefined
+			return ctx, fmt.Errorf("%w: %s", ErrUndefined, text)
 		} else {
 			ctx, err = s.runSubStep(ctx, text, def)
 			if err != nil {
@@ -530,6 +534,8 @@ func (s *suite) matchStepTextAndType(text string, stepType messages.PickleStepTy
 				},
 				Args:         args,
 				HandlerValue: h.HandlerValue,
+				File:         h.File,
+				Line:         h.Line,
 				Nested:       h.Nested,
 			}
 
@@ -609,7 +615,7 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) {
 		s.storage.MustInsertPickleResult(pr)
 
 		s.fmt.Pickle(pickle)
-		return ErrUndefined
+		return fmt.Errorf("%w: no steps in scenario", ErrUndefined)
 	}
 
 	// Before scenario hooks are called in context of first evaluated step


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5a3662f..55d6d0d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -30,7 +30,7 @@ jobs:
         run: gofmt -d -e . 2>&1 | tee outfile && test -z "$(cat outfile)" && rm outfile
       - name: Run staticcheck
         if: matrix.go-version == 'stable'
-        uses: dominikh/staticcheck-action@v1.3.1
+        uses: dominikh/staticcheck-action@v1.4.0
         with:
           version: "latest"
           install-go: false
diff --git a/internal/formatters/fmt_base_test.go b/internal/formatters/fmt_base_test.go
index ef6e8d2..f14efcf 100644
--- a/internal/formatters/fmt_base_test.go
+++ b/internal/formatters/fmt_base_test.go
@@ -42,7 +42,7 @@ And step failed f2s3:4
 
 Scenario: f2s4
 When step passed f2s4:1
-Then step is undefined f2s4:2
+Then something unknown happens f2s4:2
 And step passed f2s4:3
 `)},
 	)
@@ -116,8 +116,8 @@ scenario "f2s3" passed
 
 step invoked: "f2s4:1", passed
 step "step passed f2s4:1" finished with status passed
-.Ustep "step is undefined f2s4:2" finished with status undefined
-scenario "f2s4" ended with error "step is undefined"
+.Ustep "something unknown happens f2s4:2" finished with status undefined
+scenario "f2s4" ended with error "step is undefined: something unknown happens f2s4:2"
 -step "step passed f2s4:3" finished with status skipped
  13
 
@@ -139,12 +139,12 @@ scenario "f2s4" ended with error "step is undefined"
 
 You can implement step definitions for undefined steps with these snippets:
 
-func stepIsUndefinedFS(arg1, arg2, arg3 int) error {
+func somethingUnknownHappensFS(arg1, arg2, arg3 int) error {
 	return godog.ErrPending
 }
 
 func InitializeScenario(ctx *godog.ScenarioContext) {
-	ctx.Step(`+"`"+`^step is undefined f(\d+)s(\d+):(\d+)$`+"`"+`, stepIsUndefinedFS)
+	ctx.Step(`+"`"+`^something unknown happens f(\d+)s(\d+):(\d+)$`+"`"+`, somethingUnknownHappensFS)
 }
 
 `, out.String())
diff --git a/internal/formatters/fmt_junit_test.go b/internal/formatters/fmt_junit_test.go
new file mode 100644
index 0000000..b66867f
--- /dev/null
+++ b/internal/formatters/fmt_junit_test.go
@@ -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"`
+}
diff --git a/internal/formatters/formatter-tests/features/stop_on_first_failure.feature b/internal/formatters/formatter-tests/features/stop_on_first_failure.feature
new file mode 100644
index 0000000..32f9618
--- /dev/null
+++ b/internal/formatters/formatter-tests/features/stop_on_first_failure.feature
@@ -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 
\ No newline at end of file
diff --git a/internal/formatters/formatter-tests/junit,pretty/stop_on_first_failure b/internal/formatters/formatter-tests/junit,pretty/stop_on_first_failure
new file mode 100644
index 0000000..1f802ac
--- /dev/null
+++ b/internal/formatters/formatter-tests/junit,pretty/stop_on_first_failure
@@ -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
diff --git a/internal/formatters/formatter-tests/junit/stop_on_first_failure b/internal/formatters/formatter-tests/junit/stop_on_first_failure
new file mode 100644
index 0000000..debf57a
--- /dev/null
+++ b/internal/formatters/formatter-tests/junit/stop_on_first_failure
@@ -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>
\ No newline at end of file
diff --git a/internal/formatters/formatter-tests/pretty/stop_on_first_failure b/internal/formatters/formatter-tests/pretty/stop_on_first_failure
new file mode 100644
index 0000000..dcc2a0d
--- /dev/null
+++ b/internal/formatters/formatter-tests/pretty/stop_on_first_failure
@@ -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
diff --git a/internal/models/stepdef_test.go b/internal/models/stepdef_test.go
index 318f375..484fe3d 100644
--- a/internal/models/stepdef_test.go
+++ b/internal/models/stepdef_test.go
@@ -259,6 +259,26 @@ func TestArgumentCountChecks(t *testing.T) {
 	assert.Nil(t, err)
 }
 
+func TestArgumentCountChecksWithContext(t *testing.T) {
+	wasCalled := false
+	fn := func(ctx context.Context, a int, b int) {
+		wasCalled = true
+	}
+
+	def := &models.StepDefinition{
+		StepDefinition: formatters.StepDefinition{
+			Handler: fn,
+		},
+		HandlerValue: reflect.ValueOf(fn),
+	}
+
+	def.Args = []interface{}{"1"}
+	_, err := def.Run(context.Background())
+	assert.False(t, wasCalled)
+	assert.Equal(t, `func expected more arguments than given: expected 2 arguments, matched 1 from step`, err.(error).Error())
+	assert.True(t, errors.Is(err.(error), models.ErrUnmatchedStepArgumentNumber))
+}
+
 func TestShouldSupportIntTypes(t *testing.T) {
 	var aActual int64
 	var bActual int32
@@ -305,6 +325,55 @@ func TestShouldSupportIntTypes(t *testing.T) {
 	assert.Equal(t, `cannot convert argument 0: "11111111111111111111111111111111" to int64: strconv.ParseInt: parsing "11111111111111111111111111111111": value out of range`, err.(error).Error())
 }
 
+func TestShouldSupportUintTypes(t *testing.T) {
+	var aActual uint64
+	var bActual uint32
+	var cActual uint16
+	var dActual uint8
+
+	fn := func(a uint64, b uint32, c uint16, d uint8) {
+		aActual = a
+		bActual = b
+		cActual = c
+		dActual = d
+	}
+
+	def := &models.StepDefinition{
+		StepDefinition: formatters.StepDefinition{
+			Handler: fn,
+		},
+		HandlerValue: reflect.ValueOf(fn),
+	}
+
+	def.Args = []interface{}{"1", "2", "3", "4"}
+	_, err := def.Run(context.Background())
+	assert.Nil(t, err)
+	assert.Equal(t, uint64(1), aActual)
+	assert.Equal(t, uint32(2), bActual)
+	assert.Equal(t, uint16(3), cActual)
+	assert.Equal(t, uint8(4), dActual)
+
+	// 256 doesn't fit in uint8
+	def.Args = []interface{}{"1", "2", "3", "256"}
+	_, err = def.Run(context.Background())
+	assert.Equal(t, `cannot convert argument 3: "256" to uint8: strconv.ParseUint: parsing "256": value out of range`, err.(error).Error())
+
+	// 65536 doesn't fit in uint16
+	def.Args = []interface{}{"1", "2", "65536", "4"}
+	_, err = def.Run(context.Background())
+	assert.Equal(t, `cannot convert argument 2: "65536" to uint16: strconv.ParseUint: parsing "65536": value out of range`, err.(error).Error())
+
+	// 4294967296 too large for uint32
+	def.Args = []interface{}{"1", "4294967296", "3", "4"}
+	_, err = def.Run(context.Background())
+	assert.Equal(t, `cannot convert argument 1: "4294967296" to uint32: strconv.ParseUint: parsing "4294967296": value out of range`, err.(error).Error())
+
+	// 18446744073709551616 too large for uint64
+	def.Args = []interface{}{"18446744073709551616", "2", "3", "4"}
+	_, err = def.Run(context.Background())
+	assert.Equal(t, `cannot convert argument 0: "18446744073709551616" to uint64: strconv.ParseUint: parsing "18446744073709551616": value out of range`, err.(error).Error())
+}
+
 func TestShouldSupportFloatTypes(t *testing.T) {
 	var aActual float64
 	var bActual float32
diff --git a/suite_context_test.go b/suite_context_test.go
index 9b042d8..675389f 100644
--- a/suite_context_test.go
+++ b/suite_context_test.go
@@ -1008,13 +1008,13 @@ func TestTestSuite_Run(t *testing.T) {
 		{
 			name: "undefined_then_pass_no_strict_doesnt_fail_scenario", afterStepCnt: 2, beforeStepCnt: 2, noStrict: true, suitePasses: true,
 			body: `
-					When step is undefined
+					When something unknown happens
 					Then step passes`,
 			log: `
 					>>>> Before suite
 					>> Before scenario "test"
-					Before step "step is undefined"
-					After step "step is undefined", error: step is undefined, status: undefined
+					Before step "something unknown happens"
+					After step "something unknown happens", error: step is undefined: something unknown happens, status: undefined
 					Before step "step passes"
 					After step "step passes", error: <nil>, status: skipped
 					<< After scenario "test", error: <nil>
@@ -1023,14 +1023,14 @@ func TestTestSuite_Run(t *testing.T) {
 		{
 			name: "undefined_then_pass_fails_scenario", afterStepCnt: 2, beforeStepCnt: 2,
 			body: `
-					When step is undefined
+					When something unknown happens
 					Then step passes`,
 			log: `
 					>>>> Before suite
 					>> Before scenario "test"
-					Before step "step is undefined"
-					After step "step is undefined", error: step is undefined, status: undefined
-					<< After scenario "test", error: step is undefined
+					Before step "something unknown happens"
+					After step "something unknown happens", error: step is undefined: something unknown happens, status: undefined
+					<< After scenario "test", error: step is undefined: something unknown happens
 					Before step "step passes"
 					After step "step passes", error: <nil>, status: skipped
 					<<<< After suite`,
@@ -1039,15 +1039,15 @@ func TestTestSuite_Run(t *testing.T) {
 			name: "fail_then_undefined_fails_scenario", afterStepCnt: 2, beforeStepCnt: 2,
 			body: `
 					When step fails
-					Then step is undefined`,
+					Then something unknown happens`,
 			log: `
 					>>>> Before suite
 					>> Before scenario "test"
 					Before step "step fails"
 					After step "step fails", error: oops, status: failed
 					<< After scenario "test", error: oops
-					Before step "step is undefined"
-					After step "step is undefined", error: step is undefined, status: undefined
+					Before step "something unknown happens"
+					After step "something unknown happens", error: step is undefined: something unknown happens, status: undefined
 					<<<< After suite`,
 		},
 		{
diff --git a/test_context.go b/test_context.go
index 8156c6d..add9f47 100644
--- a/test_context.go
+++ b/test_context.go
@@ -5,12 +5,14 @@ import (
 	"fmt"
 	"reflect"
 	"regexp"
+	"runtime"
+
+	messages "github.com/cucumber/messages/go/v21"
 
 	"github.com/cucumber/godog/formatters"
 	"github.com/cucumber/godog/internal/builder"
 	"github.com/cucumber/godog/internal/flags"
 	"github.com/cucumber/godog/internal/models"
-	messages "github.com/cucumber/messages/go/v21"
 )
 
 // GherkinDocument represents gherkin document.
@@ -342,6 +344,10 @@ func (ctx ScenarioContext) stepWithKeyword(expr interface{}, stepFunc interface{
 		Nested:       isNested,
 	}
 
+	// Get the file and line number of the call that created this step with a
+	// call to one of the Step, Given, When, or Then wrappers.
+	_, def.File, def.Line, _ = runtime.Caller(2)
+
 	// stash the step
 	ctx.suite.steps = append(ctx.suite.steps, def)
 }