Skip to content

Conversation

@pedjak
Copy link
Contributor

@pedjak pedjak commented Nov 28, 2025

Description

Replace traditional Go e2e tests with Godog (Cucumber for Go) to improve test readability and maintainability through behavior-driven development.

Benefits:

  • Living Documentation: Test scenarios serve as up-to-date documentation of system behavior
  • Better Collaboration: Product owners can read and validate test scenarios
  • Reduced Duplication: Reusable step definitions eliminate code repetition
  • Improved Maintainability: Changes to common patterns happen in one place
  • Clearer Intent: Gherkin syntax makes test purpose immediately obvious
  • Easier Debugging: Clear separation between what is tested (features) and how (steps)
  • Concurrent Execution: Set the ground work for running tests in parallel

Changes:

  • Convert existing test scenarios to Gherkin feature files
  • Implement reusable step definitions in steps/steps.go
  • Add scenario hooks for setup/teardown and feature gate detection
  • Provide comprehensive documentation in test/e2e/README.md
  • Remove legacy test files (cluster_extension_install_test.go, etc.)
  • Added detailed README covering:
    • Architecture and design patterns
    • How to write new tests
    • Running tests with various options
    • Best practices and troubleshooting
  • Go test driving code reduced by ~1000 lines

Migration Notes

  • No changes to test behavior or coverage - this is purely a refactoring
  • All existing test scenarios are preserved with equivalent Gherkin implementations
  • Test execution remains the same via make test-e2e

Assisted-By: Claude noreply@anthropic.com

Reviewer Checklist

  • API Go Documentation
  • Tests: Unit Tests (and E2E Tests, if appropriate)
  • Comprehensive Commit Messages
  • Links to related GitHub Issue(s)

Copilot AI review requested due to automatic review settings November 28, 2025 15:11
@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Nov 28, 2025
@netlify
Copy link

netlify bot commented Nov 28, 2025

Deploy Preview for olmv1 ready!

Name Link
🔨 Latest commit bf10225
🔍 Latest deploy log https://app.netlify.com/projects/olmv1/deploys/6941be61c0b20b00083c1700
😎 Deploy Preview https://deploy-preview-2365--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the e2e test suite from traditional Go testing framework to Godog (BDD/Cucumber framework), enabling behavior-driven development with Gherkin feature files. The migration maintains test coverage while reorganizing tests into feature files with step definitions.

Key Changes

  • Replaced traditional Go test functions with Godog scenarios and step definitions
  • Added Gherkin .feature files describing test behavior in a more readable format
  • Introduced new test infrastructure (steps.go, hooks.go) to support BDD testing

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
test/e2e/features_test.go New test entry point initializing Godog suite with scenario and suite initializers
test/e2e/features/steps/steps.go Implements step definitions mapping Gherkin steps to Go functions
test/e2e/features/steps/hooks.go Provides scenario lifecycle hooks and feature gate detection
test/e2e/features/*.feature Gherkin feature files defining test scenarios (install, update, recover, metrics)
test/e2e/features/steps/testdata/*.yaml YAML templates for test resources (catalogs, RBAC)
test/e2e/*_test.go Removed traditional test files migrated to feature files
test/e2e/network_policy_test.go Added client initialization and helper function
go.mod, go.sum Added Cucumber/Godog dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from 730ba01 to 0482745 Compare November 28, 2025 15:53
@codecov
Copy link

codecov bot commented Nov 28, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.05%. Comparing base (39718ba) to head (bf10225).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2365      +/-   ##
==========================================
- Coverage   73.07%   73.05%   -0.03%     
==========================================
  Files         100      100              
  Lines        7641     7641              
==========================================
- Hits         5584     5582       -2     
- Misses       1622     1623       +1     
- Partials      435      436       +1     
Flag Coverage Δ
e2e 43.81% <ø> (-0.92%) ⬇️
experimental-e2e 48.77% <ø> (-0.58%) ⬇️
unit 57.11% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI review requested due to automatic review settings December 1, 2025 18:01
@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from 0482745 to 7038e17 Compare December 1, 2025 18:01
@pedjak pedjak changed the title wip: migrate e2e tests to godog framework (aka BDD/Cucumber) Migrate e2e tests to Godog BDD framework Dec 1, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 148 to 200
if stdErr := string(func() *exec.ExitError {
target := &exec.ExitError{}
_ = errors.As(err, &target)
return target
}().Stderr); !strings.Contains(stdErr, errMsg) {
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential nil pointer dereference when extracting stderr from error. If the error is not of type *exec.ExitError, this will panic. Consider adding a nil check:

waitFor(ctx, func() bool {
    _, err := kubectlWithInput(yamlContent, "apply", "-f", "-")
    if err == nil {
        return false
    }
    var exitErr *exec.ExitError
    if errors.As(err, &exitErr) && strings.Contains(string(exitErr.Stderr), errMsg) {
        return true
    }
    return false
})
Suggested change
if stdErr := string(func() *exec.ExitError {
target := &exec.ExitError{}
_ = errors.As(err, &target)
return target
}().Stderr); !strings.Contains(stdErr, errMsg) {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
stdErr := string(exitErr.Stderr)
if !strings.Contains(stdErr, errMsg) {
return false
}
return true
}
return false

Copilot uses AI. Check for mistakes.
Comment on lines 112 to 113
if _, err := kubectl("delete", r.kind, r.name, "-n", sc.namespace); err != nil {
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(err.(*exec.ExitError).Stderr))
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential nil pointer dereference when asserting error type. If the error is not of type *exec.ExitError, this will panic when accessing .Stderr. Consider checking if the type assertion succeeded:

if _, err := kubectl("delete", r.kind, r.name, "-n", sc.namespace); err != nil {
    var exitErr *exec.ExitError
    stderr := ""
    if errors.As(err, &exitErr) {
        stderr = string(exitErr.Stderr)
    }
    logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderr)
}

Copilot uses AI. Check for mistakes.
}

func scenarioCtx(ctx context.Context) *scenarioContext {
return ctx.Value(scenarioContextKey).(*scenarioContext)
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential nil pointer dereference. If ctx.Value(scenarioContextKey) returns nil, this will panic. Consider adding a nil check and returning a helpful error:

func scenarioCtx(ctx context.Context) *scenarioContext {
    val := ctx.Value(scenarioContextKey)
    if val == nil {
        panic("scenario context not found in context")
    }
    sc, ok := val.(*scenarioContext)
    if !ok {
        panic("scenario context has wrong type")
    }
    return sc
}
Suggested change
return ctx.Value(scenarioContextKey).(*scenarioContext)
val := ctx.Value(scenarioContextKey)
if val == nil {
panic("scenario context not found in context")
}
sc, ok := val.(*scenarioContext)
if !ok {
panic("scenario context has wrong type")
}
return sc

Copilot uses AI. Check for mistakes.
@pedjak pedjak changed the title Migrate e2e tests to Godog BDD framework 🌱 Migrate e2e tests to Godog BDD framework Dec 1, 2025
@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from 7038e17 to 844eaa1 Compare December 1, 2025 18:14
@pedjak pedjak marked this pull request as ready for review December 1, 2025 18:14
@pedjak pedjak requested a review from a team as a code owner December 1, 2025 18:14
Copilot AI review requested due to automatic review settings December 1, 2025 18:14
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Dec 1, 2025
@openshift-ci openshift-ci bot requested review from oceanc80 and perdasilva December 1, 2025 18:15
@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from 844eaa1 to 63e2440 Compare December 1, 2025 18:20
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 114 to 133
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(func() *exec.ExitError {
target := &exec.ExitError{}
_ = errors.As(err, &target)
return target
}().Stderr))
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential nil pointer dereference. If errors.As(err, &target) returns false, target will be nil and accessing target.Stderr will cause a panic.

Suggested fix:

var stderrStr string
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
    stderrStr = string(exitErr.Stderr)
}
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderrStr)
Suggested change
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(func() *exec.ExitError {
target := &exec.ExitError{}
_ = errors.As(err, &target)
return target
}().Stderr))
var stderrStr string
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
stderrStr = string(exitErr.Stderr)
}
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderrStr)

Copilot uses AI. Check for mistakes.
Copy link
Member

@joelanford joelanford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a really nice improvement overall. Just some minor comments/questions.

- "sleep"
args:
- "1000"
image: busybox:1.36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the same image we've always used?

I recall there being issues in the past with rate limiting from Docker Hub.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is the one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we were hitting rate-limiting because we had the image untagged/set to latest, so it would pull every time no matter what. This is tagged so it should be fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true - we may want to copy it over to quay.io/operator-framework or something (assuming there's no licensing issue with that) to avoid those issues (not for this PR tho...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use registry.k8s.io/e2e-test-images/busybox:1.36.1-1 for downstream testing, it ought to be compatible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use registry.k8s.io/e2e-test-images/busybox:1.36.1-1 for downstream testing, it ought to be compatible.

I did not modify this part at all in this PR, see https://github.com/operator-framework/operator-controller/blob/main/test/e2e/cluster_extension_install_test.go#L722

"""
Then ClusterExtension reports Progressing as True with Reason Retrying:
"""
error upgrading from currently installed version "1.0.0": no bundles found for package "test" matching version "1.2.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR, but something we need to fix separately. This message makes it sound like 1.2.0 just doesn't exist. But it does. It just isn't a successor of the currently installed version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #2385

"github.com/spf13/pflag"
ctrl "sigs.k8s.io/controller-runtime"
//ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid new zap dependency? I think we're using klog in our main.go's. Can we use that here too?


func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context.Context, error) {
sc := scenarioCtx(ctx)
for _, p := range sc.backGroundCmds {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kill and wait processes concurrently? Or does order matter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I think we do not need to wait at all.

}
forDeletion = append(forDeletion, sc.addedResources...)
forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"})
for _, r := range forDeletion {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: Can we delete objects concurrently?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could try, but not sure what we are gonna gain with it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deletion can sometimes take a little while if finalizers need to be processed. I assume we want foreground deletion so that we can be sure cleanup is complete before we move on. Seems like it could speed up cleanup considerably in those cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we could do it.

error for resolved bundle "single-namespace-operator.1.0.0" with version "1.0.0":
invalid ClusterExtension configuration: invalid configuration: required field "watchNamespace" is missing
"""
When ClusterExtension is updated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could strategic merge patch instead and have a smaller yaml to make it more clear what's changing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be an improvement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could strategic merge patch instead and have a smaller yaml to make it more clear what's changing?

We could ofcourse, we craft the grammar and the semantic: how would like to look like? If we would writing user docs, how this should be read by users?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the underlying user action would be something like kubectl patch - so maybe And user patches ClusterExtension with kubectl or something like that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep it declarative as much as possible, hence mentioning kubectl would not be good.

Comment on lines +68 to +102
And resource is applied
"""
apiVersion: v1
kind: Namespace
metadata:
name: single-namespace-operator-target
"""
And ClusterExtension is applied
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just nothing the inconsistency here:

  • resource is applied
    vs
  • ClusterExtension is applied

The first one is generic, the second one is indicating a very specific resource. What's the reasoning behind this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first one is generic, the second one is indicating a very specific resource. What's the reasoning behind this?

Improved readability/focus on what matters. The fact that we use the same go code under the hood for both step is not important here - the reader should immediately understand what a step is about. Hence, we could even replace the generic step "resource is applied" with something like ([[:alnum:]]+) is applied or even ([[:alnum:]]+) is available so that we better document what is going on. Also, in this particular case, we could even create very concrete step namespace ([[:alnum:]]+) is available that is going to assure that the given namespace is created if not exists already.

error for resolved bundle "single-namespace-operator.1.0.0" with version "1.0.0":
invalid ClusterExtension configuration: invalid configuration: required field "watchNamespace" is missing
"""
When ClusterExtension is updated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be an improvement.

forDeletion = append(forDeletion, sc.addedResources...)
forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"})
for _, r := range forDeletion {
if _, err := kubectl("delete", r.kind, r.name, "-n", sc.namespace); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no distinction here between namespace-scoped resources (e.g. SAs), and cluster-scoped resources (e.g. CE). 'kubectl' may not complain about the -n argument for cluster-scoped resources, but it's basically wrong.

Comment on lines 130 to 167
result := strings.ReplaceAll(content, "$TEST_NAMESPACE", sc.namespace)
result = strings.ReplaceAll(result, "$NAME", sc.clusterExtensionName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see us wanting to expand this list, or even to make this a bit more dynamic. But probably not now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also noting an inconsistency in substitution mechanisms. Here it's $VAR, where as at: https://github.com/operator-framework/operator-controller/pull/2365/files#diff-37528e433a53ab946ef66fda327001b3a125c05c7ac9dfd2b49529fbfdc50cd3R378 it's {var}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also noting an inconsistency in substitution mechanisms. Here it's $VAR, where as at: https://github.com/operator-framework/operator-controller/pull/2365/files#diff-37528e433a53ab946ef66fda327001b3a125c05c7ac9dfd2b49529fbfdc50cd3R378 it's {var}

IMO, bash-style like variables are understandable/known for a wider audience. We could use those in testdata templates as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both are known, and in BASH, the recommendation is to use ${VAR} vs $VAR, so going with {VAR} is not that much different.

)

func kubectl(args ...string) (string, error) {
cmd := exec.Command("kubectl", args...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we run these tests in other environments, we might need to consider the use of oc as a substitute for kubectl. Here and elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added --k8s.cli command arg that can tweak that.

@jianzhangbjz
Copy link
Member

Hi team, what’s the reason for choosing Cucumber instead of Ginkgo? What’s the background behind this decision? Thanks! I’m asking because the OpenShift QE team used the Cucumber framework for E2E testing before 2020, but it took the team almost a year to fully migrate to Ginkgo. The main reasons for choosing Ginkgo include:

  1. Go-Native design, which fits better with Go-based projects.
  2. Expressive BDD-Style Syntax that is cleaner and easier to maintain.
  3. More powerful and stable test organization compared to Cucumber.
  4. Rich Assertion Library (Gomega) that simplifies writing test expectations.
  5. Built-in Parallelization for faster test execution.
  6. Better for Unit and Integration Tests
  7. Widely adopted in the Kubernetes ecosystem, making it a de-facto standard.
  8. Progressive enhancement, with active development and community support.

@pedjak
Copy link
Contributor Author

pedjak commented Dec 2, 2025

Hi team, what’s the reason for choosing Cucumber instead of Ginkgo?

@jianzhangbjz thanks for reaching out. I think I have summarized the motivation in the PR description, let me know if some of the points you raised are unanswered. IMO Ginko for e2e is still less readable for non-devs (and for devs as well)

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 26 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

```go
func BundleInstalled(ctx context.Context, name, version string) error {
sc := scenarioCtx(ctx)
waitFor(ctx, func () bool {
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in function signature - there's an extra space in 'func () bool'. Should be 'func() bool' without the space.

Suggested change
waitFor(ctx, func () bool {
waitFor(ctx, func() bool {

Copilot uses AI. Check for mistakes.
All asynchronous operations use `waitFor` with consistent timeout (300s) and tick (1s):

```go
waitFor(ctx, func () bool {
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same typo as above - extra space in 'func () bool'.

Suggested change
waitFor(ctx, func () bool {
waitFor(ctx, func() bool {

Copilot uses AI. Check for mistakes.
Comment on lines 480 to 484
return fmt.Errorf("failed to apply RBAC configuration: %v: %s", err, string(func() *exec.ExitError {
target := &exec.ExitError{}
_ = errors.As(err, &target)
return target
}().Stderr))
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another instance of the same nil pointer dereference pattern with exec.ExitError type assertion on line 484.

Suggested change
return fmt.Errorf("failed to apply RBAC configuration: %v: %s", err, string(func() *exec.ExitError {
target := &exec.ExitError{}
_ = errors.As(err, &target)
return target
}().Stderr))
var exitErr *exec.ExitError
stderr := ""
if errors.As(err, &exitErr) && exitErr != nil {
stderr = string(exitErr.Stderr)
}
return fmt.Errorf("failed to apply RBAC configuration: %v: %s", err, stderr)

Copilot uses AI. Check for mistakes.
Comment on lines 150 to 154
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(func() *exec.ExitError {
target := &exec.ExitError{}
_ = errors.As(err, &target)
return target
}().Stderr))
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another nil pointer dereference with the exec.ExitError type assertion on line 154.

Suggested change
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", string(func() *exec.ExitError {
target := &exec.ExitError{}
_ = errors.As(err, &target)
return target
}().Stderr))
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", func() string {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr != nil {
return string(exitErr.Stderr)
}
return ""
}())

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@tmshort tmshort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's generally fine, just a few small nits.

I think there's a semantic issue with "When", "Then" and "And", which doesn't matter in terms of the tests working, but instead how the tests are understood by humans.

It appeared to me that "When" represents a case where the tests does something (i.e. a test action), whereas "Then" and "And" represents a condition to be tested for.

This wasn't always consistent, as some actions seems to be under Then/Ands.

return resp, nil
}

func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint string, controllerName string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that his is using port-forwarding to scrape metrics, rather than a pod in the cluster (as the original test does).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the advantage is that we do not need to pull an additional image + deploy another container.

Copy link
Contributor

@tmshort tmshort Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, it's simply noting the change to the test process.

Comment on lines +394 to +413
func getComponentNamespace(t *testing.T, client, selector string) string {
cmd := exec.Command(client, "get", "pods", "--all-namespaces", "--selector="+selector, "--output=jsonpath={.items[0].metadata.namespace}") //nolint:gosec // just gathering pods for a given selector
output, err := cmd.CombinedOutput()
require.NoError(t, err, "Error determining namespace: %s", string(output))

namespace := string(bytes.TrimSpace(output))
if namespace == "" {
t.Fatal("No namespace found for selector " + selector)
}
return namespace
}

func init() {
cfg = ctrl.GetConfigOrDie()

var err error
utilruntime.Must(apiextensionsv1.AddToScheme(scheme.Scheme))
c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
utilruntime.Must(err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's just moving the function from a deleted file, that should be fine.

Comment on lines +43 to +44
When resource "deployment/test-operator" reports as ready
Then ClusterExtension is available
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But When seems to be more of an action the test takes, rather than the checking of a condition.

@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from fffd715 to a7f8e25 Compare December 15, 2025 21:05
@pedjak
Copy link
Contributor Author

pedjak commented Dec 15, 2025

This wasn't always consistent, as some actions seems to be under Then/Ands.

If there are multiple actions then scenario are structured like:

When action1
And action2
And action3

Similar for where there are multiple checks:

Then check1
And check2
And check3

Copilot AI review requested due to automatic review settings December 15, 2025 21:11
@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from a7f8e25 to 8dac36a Compare December 15, 2025 21:11
@tmshort
Copy link
Contributor

tmshort commented Dec 15, 2025

This wasn't always consistent, as some actions seems to be under Then/Ands.

If there are multiple actions then scenario are structured like:

When action1
And action2
And action3

Similar for where there are multiple checks:

Then check1
And check2
And check3

There still seem to be inconsistencies, however, but I will have to go through them since the latest changes,

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 26 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
}

func k8scli(args ...string) (string, error) {
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name k8scli uses lowercase for CLI, which is inconsistent with typical Go naming conventions. Consider renaming to k8sCLI or k8sClient for better clarity and consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +585 to +611
if err := portForwardCmd.Process.Kill(); err != nil {
return err
}
if _, err := portForwardCmd.Process.Wait(); err != nil {
return err
}
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After killing the port-forward process, the code waits for it to exit. However, if Kill fails, the code still calls Wait on a process that might not exist or was not killed successfully, potentially causing a hang or panic. Check the error from Kill and handle it appropriately before calling Wait.

Copilot uses AI. Check for mistakes.
Background:
Given OLM is available
And "test" catalog serves bundles
And Service account "olm-sa" with needed permissions is available in test namespace
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Kubernetes context, "ServiceAccount" should be written as one word (PascalCase) to match the API resource type name.

Suggested change
And Service account "olm-sa" with needed permissions is available in test namespace
And ServiceAccount "olm-sa" with needed permissions is available in test namespace

Copilot uses AI. Check for mistakes.
}
podNameCmd := []string{"get", "pod", "-n", olmNamespace, "-o", "jsonpath={.items}"}
for k, v := range service.Spec.Selector {
podNameCmd = append(podNameCmd, fmt.Sprintf("--selector=%s=%s", k, v))
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nolint:gosec comment disables security scanning but the justification is incomplete. The command builds a pod selector from service.Spec.Selector which comes from a service fetched from the cluster. While this is generally safe, a more specific justification would be helpful, such as "selector values are validated by Kubernetes API".

Suggested change
podNameCmd = append(podNameCmd, fmt.Sprintf("--selector=%s=%s", k, v))
podNameCmd = append(podNameCmd, fmt.Sprintf("--selector=%s=%s", k, v)) //nolint:gosec // selector values are validated by Kubernetes API, so this is safe

Copilot uses AI. Check for mistakes.
@pedjak pedjak requested a review from tmshort December 16, 2025 08:15
@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from 8dac36a to 1121604 Compare December 16, 2025 13:50
@tmshort
Copy link
Contributor

tmshort commented Dec 16, 2025

The code looks ok, but when I run this locally on Fedora 42 x86_64 via make test-e2e, I'm not seeing any output. Seems to be hanging. I'm also seeing two zombied kubectl processes.

3366938 pts/13   S+     0:00  |   \_ make test-e2e
3393333 pts/13   Sl+    0:00  |       \_ go test -count=1 -v ./test/e2e/...
3393342 pts/13   Sl+    0:05  |           \_ /home/tshort/sdk/go1.25.3/bin/go test -count=1 -v ./test/e2e/...
3406425 pts/13   Sl+    0:02  |               \_ /tmp/go-build554489889/b001/e2e.test -test.paniconexit0 -test.timeout=10m0s -test.count=1 -test.v=true
3409322 pts/13   Z+     0:00  |                   \_ [kubectl] <defunct>
3411033 pts/13   Z+     0:00  |                   \_ [kubectl] <defunct>

...and it finally finished, but the output comes out all at once, which isn't acceptable. We should really be able to see the progress of the test as it runs.

Ah, it was terminated via SIGQUIT for taking too long...

SIGQUIT: quit
PC=0x48b6c1 m=0 sigcode=0

...

*** Test killed with quit: ran too long (11m0s).
FAIL    github.com/operator-framework/operator-controller/test/e2e      660.022s
?       github.com/operator-framework/operator-controller/test/e2e/steps        [no test files]
FAIL
make: *** [Makefile:218: e2e] Error 1

I rand this twice... I'd like someone else to run this locally to ensure it's not just me.

Copilot AI review requested due to automatic review settings December 16, 2025 18:59
@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from 1121604 to c17e8d4 Compare December 16, 2025 18:59
@pedjak
Copy link
Contributor Author

pedjak commented Dec 16, 2025

The code looks ok, but when I run this locally on Fedora 42 x86_64 via make test-e2e, I'm not seeing any output. Seems to be hanging. I'm also seeing two zombied kubectl processes

I can confirm you the same - I though first it was maybe because make buffers the output, but I fixed by slightly changing how we invoke tests to:

go test -count=1 -v ./test/e2e/features_test.go

pushed the change.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if c.Name == "manager" {
for _, arg := range c.Args {
if matches := featureGatePattern.FindStringSubmatch(arg); matches != nil {
v, _ := strconv.ParseBool(matches[2])
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error from strconv.ParseBool is ignored, which could lead to silent failures when parsing feature gate values. If parsing fails, the feature gate will remain at its default value without any indication of an error.

Suggested change
v, _ := strconv.ParseBool(matches[2])
v, err := strconv.ParseBool(matches[2])
if err != nil {
panic(fmt.Errorf("invalid boolean value for feature gate %q: %v", matches[1], err))
}

Copilot uses AI. Check for mistakes.
Comment on lines 81 to 82
if err := json.Unmarshal([]byte(raw), &dl); err != nil {
return
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error from json.Unmarshal is silently ignored with a return statement. If unmarshaling fails, the function continues with an empty deployment list, potentially missing the OLM deployment. Consider logging the error or handling it more explicitly.

Copilot uses AI. Check for mistakes.
Comment on lines +592 to +607
if err := portForwardCmd.Process.Kill(); err != nil {
return err
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error returned from Process.Kill is not checked. If killing the process fails, the function continues to call Process.Wait on line 595, which could cause unexpected behavior or panics if the process handle is invalid.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +41
var (
cfg *rest.Config
c client.Client
)
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global variables cfg and c are declared here but were previously declared in e2e_suite_test.go (which is being removed). This creates duplicate declarations across the package. These variables should be consolidated or their scope reconsidered to avoid confusion.

Copilot uses AI. Check for mistakes.
}
sc.metricsResponse = make(map[string]string)
for _, p := range pods {
portForwardCmd := exec.Command(k8sCli, "port-forward", "-n", p.Namespace, fmt.Sprintf("pod/%s", p.Name), fmt.Sprintf("8443:%d", metricsPort)) //nolint:gosec // perfectly safe to start port-forwarder for provided controller name
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gosec warning is suppressed with a misleading comment. The command uses fmt.Sprintf to construct arguments including user-provided values (p.Name, metricsPort), which could be exploited if these values are not properly validated. The port comes from service.Spec.Ports which should be safe, but the pod name originates from the cluster and should be validated.

Copilot uses AI. Check for mistakes.
@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from c17e8d4 to f07ab4f Compare December 16, 2025 19:41
Replace traditional Go e2e tests with Godog (Cucumber for Go) to improve
test readability and maintainability through behavior-driven development.

Changes:
- Convert existing test scenarios to Gherkin feature files
- Implement reusable step definitions in steps/steps.go
- Add scenario hooks for setup/teardown and feature gate detection
- Provide comprehensive documentation in test/e2e/README.md
- Remove legacy test files (cluster_extension_install_test.go, etc.)

Benefits:
- Human-readable test scenarios serve as living documentation
- Better separation between test specification and implementation
- Easier collaboration between technical and non-technical stakeholders
- Reduced code duplication through reusable step definitions

Assisted-By: "Claude <noreply@anthropic.com>"
Copilot AI review requested due to automatic review settings December 16, 2025 20:17
@pedjak pedjak force-pushed the e2e-spec-by-example-godog branch from f07ab4f to bf10225 Compare December 16, 2025 20:17
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +38 to +41
var (
cfg *rest.Config
c client.Client
)
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global variables cfg and c are redeclared in this file, shadowing the same variables that were previously declared in e2e_suite_test.go. Since e2e_suite_test.go has been removed, these variables are now initialized twice (once here and once in network_policy_test.go). Consider consolidating the initialization into a shared test setup file or package-level init function to avoid duplication and potential confusion.

Suggested change
var (
cfg *rest.Config
c client.Client
)
// cfg and c are initialized in the shared test setup file (e.g., suite_test.go)

Copilot uses AI. Check for mistakes.
namespace string
clusterExtensionName string
removedResources []unstructured.Unstructured
backGroundCmds []*exec.Cmd
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field name backGroundCmds uses inconsistent casing - it should be either backgroundCmds (camelCase) or BackgroundCmds (if exported). The uppercase 'G' in the middle is non-standard Go naming convention.

Suggested change
backGroundCmds []*exec.Cmd
backgroundCmds []*exec.Cmd

Copilot uses AI. Check for mistakes.
@pedjak pedjak changed the title 🌱 Migrate e2e tests to Godog BDD framework 🌱 OPRUN-4261 Migrate e2e tests to Godog BDD framework Dec 16, 2025
@tmshort
Copy link
Contributor

tmshort commented Dec 16, 2025

It's working in my local repo now.
I think we want to create a Jira tickets for follow-on work:

  • restoring color output
  • getting component namespaces from BeforeSuite()
  • anything else we want to clean up
  • any updates needed for downstreaming

@tmshort
Copy link
Contributor

tmshort commented Dec 16, 2025

/lgtm

@openshift-ci openshift-ci bot added the lgtm Indicates that a PR is ready to be merged. label Dec 16, 2025
@openshift-merge-bot openshift-merge-bot bot merged commit aaffdeb into operator-framework:main Dec 16, 2025
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. lgtm Indicates that a PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants