Skip to content

Commit ecf5559

Browse files
pedjakclaude
andcommitted
feat: add make targets for running single e2e scenarios
Add `make e2e/<feature>/<scenario-prefix>` for running individual Godog scenarios by name prefix against an already-running cluster. The prefix is resolved to godog's file:line syntax using the Gherkin parser. Add `e2e-setup` and `experimental-e2e-setup` targets to create persistent KIND clusters for iterative development, with matching `e2e-teardown` and `experimental-e2e-teardown` for cleanup. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c3caa22 commit ecf5559

5 files changed

Lines changed: 161 additions & 2 deletions

File tree

AGENTS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,25 @@ make test-regression
107107
make test
108108
```
109109

110+
### Iterative E2E Development
111+
112+
```bash
113+
# Set up a persistent e2e cluster (does not tear down after tests)
114+
make e2e-setup # Standard features
115+
make experimental-e2e-setup # Experimental features
116+
117+
# Run e2e scenarios against the running cluster
118+
make e2e/install # All scenarios in install.feature
119+
make e2e/install/Install # Scenarios starting with "Install"
120+
make "e2e/install/Install latest" # Exact prefix with spaces
121+
make e2e/install E2E_TIMEOUT=30m # Override timeout
122+
make e2e/install KUBECONFIG=~/.kube/config # Override kubeconfig
123+
124+
# Tear down the e2e cluster when done
125+
make e2e-teardown # Standard cluster
126+
make experimental-e2e-teardown # Experimental cluster
127+
```
128+
110129
### Linting & Verification
111130

112131
```bash

Makefile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,35 @@ kind-clean-%: e2e-coverage-%
449449
test-e2e: kind-clean-e2e #HELP Run e2e test suite on local kind cluster
450450
test-experimental-e2e: kind-clean-experimental-e2e #HELP Run experimental e2e test suite on local kind cluster
451451

452+
.PHONY: e2e-setup
453+
e2e-setup: E2E_SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST)
454+
e2e-setup: E2E_RELEASE_MANIFEST := $(STANDARD_RELEASE_MANIFEST)
455+
e2e-setup: wait-e2e #EXHELP Create a KIND cluster with standard OLM deployed for iterative e2e testing.
456+
457+
.PHONY: experimental-e2e-setup
458+
experimental-e2e-setup: KIND_CONFIG := ./kind-config/kind-config-2node.yaml
459+
experimental-e2e-setup: E2E_SOURCE_MANIFEST := $(EXPERIMENTAL_E2E_MANIFEST)
460+
experimental-e2e-setup: E2E_RELEASE_MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST)
461+
experimental-e2e-setup: wait-experimental-e2e #EXHELP Create a KIND cluster with experimental OLM deployed for iterative e2e testing.
462+
463+
.PHONY: e2e-teardown
464+
e2e-teardown: $(KIND) #EXHELP Delete the standard e2e KIND cluster.
465+
$(KIND) delete cluster --name operator-controller-e2e
466+
467+
.PHONY: experimental-e2e-teardown
468+
experimental-e2e-teardown: $(KIND) #EXHELP Delete the experimental e2e KIND cluster.
469+
$(KIND) delete cluster --name operator-controller-experimental-e2e
470+
471+
.PHONY: e2e/%
472+
e2e/%: E2E_TIMEOUT ?= 20m
473+
e2e/%: KUBECONFIG ?= $(KUBECONFIG_DIR)/operator-controller-e2e.kubeconfig
474+
e2e/%: #EXHELP Run e2e scenario against a cluster created by e2e-setup or experimental-e2e-setup (tear down with e2e-teardown).
475+
@feature=$$(echo "$*" | cut -d/ -f1); \
476+
scenario=$$(echo "$*" | cut -d/ -f2-); \
477+
if [ "$$scenario" = "$$feature" ]; then scenario=""; fi; \
478+
KUBECONFIG=$(KUBECONFIG) go test -count=1 -v ./test/e2e/features_test.go \
479+
-timeout $(E2E_TIMEOUT) \
480+
-args --godog.concurrency=1 --e2e.scenario="$$scenario" "features/$$feature.feature"
452481

453482
.PHONY: test-extension-developer-e2e
454483
test-extension-developer-e2e: SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST)

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ require (
88
github.com/blang/semver/v4 v4.0.0
99
github.com/cert-manager/cert-manager v1.20.2
1010
github.com/containerd/containerd v1.7.33
11+
github.com/cucumber/gherkin/go/v26 v26.2.0
1112
github.com/cucumber/godog v0.15.1
13+
github.com/cucumber/messages/go/v21 v21.0.1
1214
github.com/evanphx/json-patch v5.9.11+incompatible
1315
github.com/fsnotify/fsnotify v1.10.1
1416
github.com/go-logr/logr v1.4.3
@@ -91,8 +93,6 @@ require (
9193
github.com/containerd/typeurl/v2 v2.2.3 // indirect
9294
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
9395
github.com/containers/ocicrypt v1.3.0 // indirect
94-
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
95-
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
9696
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
9797
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
9898
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect

test/e2e/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,48 @@ Note that when this is done the `make` target will no longer automatically split
270270
GODOG_ARGS="--godog.tags=~@Serial --godog.concurrency=100" make test-experimental-e2e
271271
```
272272

273+
### Iterative Development
274+
275+
For iterating on individual scenarios without full suite setup/teardown each time,
276+
use the persistent cluster and single-scenario targets:
277+
278+
**1. Set up a persistent cluster (once):**
279+
280+
```bash
281+
make e2e-setup # Standard features
282+
make experimental-e2e-setup # Experimental features
283+
```
284+
285+
This builds images, creates a KIND cluster, deploys OLM, and waits for readiness.
286+
The cluster persists until explicitly torn down.
287+
288+
**2. Run individual scenarios:**
289+
290+
```bash
291+
# Run all scenarios in a feature file
292+
make e2e/install
293+
294+
# Run scenarios matching a name prefix (case-insensitive)
295+
make e2e/install/Install # all "Install ..." scenarios
296+
make "e2e/install/Install latest" # prefix with spaces (use quotes)
297+
make e2e/install/Boxcutter # single matching scenario
298+
299+
# Override timeout or kubeconfig
300+
make e2e/install/Install E2E_TIMEOUT=30m
301+
make e2e/install KUBECONFIG=~/.kube/config
302+
```
303+
304+
The prefix matches scenario names from the start. If multiple scenarios match, all
305+
of them run. If no scenario matches, the command fails with a list of available
306+
scenario names.
307+
308+
**3. Tear down when done:**
309+
310+
```bash
311+
make e2e-teardown # Standard cluster
312+
make experimental-e2e-teardown # Experimental cluster
313+
```
314+
273315
### Run Specific Feature
274316

275317
```bash
@@ -302,6 +344,8 @@ Available formats: `pretty`, `cucumber`, `progress`, `junit`
302344

303345
**Custom Flags:**
304346

347+
- `--e2e.scenario=<prefix>`: Run scenarios whose name starts with the given prefix (case-insensitive).
348+
Used by `make e2e/<feature>/<prefix>` internally.
305349
- `--log.debug`: Enable debug logging (development mode)
306350
- `--k8s.cli=<path>`: Specify path to Kubernetes CLI (default: `kubectl`)
307351
- Useful for using `oc` or a specific kubectl binary

test/e2e/features_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import (
44
"fmt"
55
"log"
66
"os"
7+
"strings"
78
"testing"
89

10+
gherkin "github.com/cucumber/gherkin/go/v26"
911
"github.com/cucumber/godog"
1012
"github.com/cucumber/godog/colors"
13+
messages "github.com/cucumber/messages/go/v21"
1114
"github.com/spf13/pflag"
1215

1316
"github.com/operator-framework/operator-controller/test/e2e/steps"
@@ -23,15 +26,33 @@ var opts = godog.Options{
2326
Strict: true,
2427
}
2528

29+
var scenarioFilter string
30+
2631
func init() {
2732
godog.BindCommandLineFlags("godog.", &opts)
33+
pflag.StringVar(&scenarioFilter, "e2e.scenario", "", "scenario name prefix (case-insensitive)")
2834
}
2935

3036
func TestMain(m *testing.M) {
3137
// parse CLI arguments
3238
pflag.Parse()
3339
opts.Paths = pflag.Args()
3440

41+
if scenarioFilter != "" {
42+
if len(opts.Paths) != 1 {
43+
log.Fatalf("--e2e.scenario requires exactly one feature file path, got %d", len(opts.Paths))
44+
}
45+
lines, err := findScenarioFirstLineNumberByPrefix(opts.Paths[0], scenarioFilter)
46+
if err != nil {
47+
log.Fatal(err)
48+
}
49+
basePath := opts.Paths[0]
50+
opts.Paths = make([]string, len(lines))
51+
for i, line := range lines {
52+
opts.Paths[i] = fmt.Sprintf("%s:%d", basePath, line)
53+
}
54+
}
55+
3556
// run tests
3657
sc := godog.TestSuite{
3758
TestSuiteInitializer: InitializeSuite,
@@ -62,6 +83,52 @@ func TestMain(m *testing.M) {
6283
}
6384
}
6485

86+
func findScenarioFirstLineNumberByPrefix(featurePath, prefix string) ([]int, error) {
87+
f, err := os.Open(featurePath)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to open %s: %w", featurePath, err)
90+
}
91+
defer f.Close()
92+
93+
doc, err := gherkin.ParseGherkinDocument(f, (&messages.Incrementing{}).NewId)
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to parse %s: %w", featurePath, err)
96+
}
97+
98+
if doc.Feature == nil {
99+
return nil, fmt.Errorf("no Feature found in %s", featurePath)
100+
}
101+
102+
prefix = strings.ToLower(strings.TrimSpace(prefix))
103+
var matches []int
104+
var allNames []string
105+
106+
matchScenario := func(sc *messages.Scenario) {
107+
allNames = append(allNames, sc.Name)
108+
if strings.HasPrefix(strings.ToLower(sc.Name), prefix) {
109+
matches = append(matches, int(sc.Location.Line))
110+
}
111+
}
112+
for _, child := range doc.Feature.Children {
113+
if child.Scenario != nil {
114+
matchScenario(child.Scenario)
115+
}
116+
if child.Rule != nil {
117+
for _, rc := range child.Rule.Children {
118+
if rc.Scenario != nil {
119+
matchScenario(rc.Scenario)
120+
}
121+
}
122+
}
123+
}
124+
125+
if len(matches) == 0 {
126+
return nil, fmt.Errorf("no scenario matching prefix %q in %s\navailable scenarios:\n %s",
127+
prefix, featurePath, strings.Join(allNames, "\n "))
128+
}
129+
return matches, nil
130+
}
131+
65132
func InitializeSuite(tc *godog.TestSuiteContext) {
66133
tc.BeforeSuite(steps.BeforeSuite)
67134
}

0 commit comments

Comments
 (0)