go-sdk icon indicating copy to clipboard operation
go-sdk copied to clipboard

[BUG] testing.TestProvider can't reliably be used when tests invoke separate goroutines

Open hairyhenderson opened this issue 2 months ago • 1 comments

Observed behavior

Currently the testing.TestProvider makes use of goroutine-local variables to store the current test's name, so that the provider can be scoped to a given test.

This has the unfortunate side-effect that tests that need to launch goroutines may panic with the unable to detect test name; be sure to call UsingFlags in the scope of a test (in T.run)!") message.

Expected Behavior

Ideally, the TestProvider should be able to operate reliably when goroutines are involved

Steps to reproduce

No response

hairyhenderson avatar Oct 07 '25 00:10 hairyhenderson

Thanks for reporting this. Would you be able to provide a minimal reproducible example demonstrating this bug?

sahidvelji avatar Oct 23 '25 13:10 sahidvelji

sorry for the delay - I wasn't able to reproduce this recently (I lost the code where this was originally happening).

As far as I can remember, I think my issue was that I was calling oftesting.NewTestProvider() for each test, meaning the providers map wasn't being shared.

Instead, using a package-global testProvider is more reliable.

I'm going to close this for now, and if I run into this again I'll re-open

hairyhenderson avatar Nov 14 '25 00:11 hairyhenderson

Alright, finally I ran into this again - the key is the bug is triggered when a goroutine is involved in the test.

Here's code to reproduce the bug:

package ofbug

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/open-feature/go-sdk/openfeature"
	"github.com/open-feature/go-sdk/openfeature/memprovider"
	oftesting "github.com/open-feature/go-sdk/openfeature/testing"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type ofBugHandler struct {
	handlerDone chan struct{}
}

func (d *ofBugHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	if openfeature.NewDefaultClient().Boolean(ctx, "myflag", false, openfeature.TransactionContext(ctx)) {
		fmt.Println("evaluated to true")
	}

	ticker := time.NewTicker(500 * time.Millisecond)
	defer ticker.Stop()
	for {
		select {
		case <-r.Context().Done():
			w.WriteHeader(http.StatusServiceUnavailable)
			close(d.handlerDone)
			return
		case <-ticker.C:
			w.WriteHeader(http.StatusOK)
			close(d.handlerDone)
			return
		}
	}
}

func TestServeHTTP_HandlerDone(t *testing.T) {
	ctx := t.Context()

	testProvider := oftesting.NewTestProvider()
	testProvider.UsingFlags(t, map[string]memprovider.InMemoryFlag{
		"myflag": {
			DefaultVariant: "defaultVariant",
			Variants:       map[string]any{"defaultVariant": true},
		},
	})
	_ = openfeature.SetProviderAndWait(testProvider)

	handlerDone := make(chan struct{})
	d := &ofBugHandler{handlerDone: handlerDone}

	req := httptest.NewRequestWithContext(ctx, http.MethodGet, "/drain", nil)
	w := httptest.NewRecorder()

	// Start the handler in a goroutine for _reasons_
	// This is what triggers the TestProvider bug
	go func() {
		d.ServeHTTP(w, req)
	}()

	timedout := false

	// Wait for handler to complete
	select {
	case <-handlerDone:
		assert.Equal(t, http.StatusOK, w.Code)
	case <-time.After(2 * time.Second):
		t.Log("drain not completed within timeout")
		timedout = true
	}

	assert.Equal(t, http.StatusOK, w.Code)
	require.False(t, timedout)
}

This panics with:

panic: unable to detect test name; be sure to call `UsingFlags` in the scope of a test (in T.run)!

goroutine 24 [running]:
github.com/open-feature/go-sdk/openfeature/testing.TestProvider.getProvider({{}, 0x500000000?})
        github.com/open-feature/[email protected]/openfeature/testing/testprovider.go:78 +0x13c
github.com/open-feature/go-sdk/openfeature/testing.TestProvider.BooleanEvaluation({{}, 0x100f8a3c0?}, {0x100dd09f0, 0x140000ee2d0}, {0x100ce9c48, 0x6}, 0x0, 0x140000a3740)
        github.com/open-feature/[email protected]/openfeature/testing/testprovider.go:47 +0x54
github.com/open-feature/go-sdk/openfeature.(*Client).evaluate(0x140000e22d0, {0x100dd09f0, 0x140000ee2d0}, {0x100ce9c48, 0x6}, 0x0, {0x100d6c160, 0x100d48640}, {{0x0?, 0x0?}, ...}, ...)
        github.com/open-feature/[email protected]/openfeature/client.go:743 +0xbc4
github.com/open-feature/go-sdk/openfeature.(*Client).BooleanValueDetails(0x140000e22d0, {0x100dd09f0, 0x140000ee2d0}, {0x100ce9c48, 0x6}, 0x0, {{0x0?, 0x0?}, 0x0?}, {0x0, ...})
        github.com/open-feature/[email protected]/openfeature/client.go:389 +0x18c
github.com/open-feature/go-sdk/openfeature.(*Client).BooleanValue(0x100f7a1a0?, {0x100dd09f0?, 0x140000ee2d0?}, {0x100ce9c48?, 0x0?}, 0x0, {{0x0?, 0x0?}, 0x0?}, {0x0?, ...})
        github.com/open-feature/[email protected]/openfeature/client.go:296 +0x40
github.com/open-feature/go-sdk/openfeature.(*Client).Boolean(...)
        github.com/open-feature/[email protected]/openfeature/client.go:579
ofbug.(*ofBugHandler).ServeHTTP(0x140000a80e8, {0x100dd0610, 0x1400009ec40}, 0x140000f4780)
        ofbug/bug_test.go:24 +0x190
ofbug.TestServeHTTP_HandlerDone.func1()
        ofbug/bug_test.go:65 +0x2c
created by ofbug.TestServeHTTP_HandlerDone in goroutine 22
        ofbug/bug_test.go:64 +0x2c0
FAIL    ofbug   0.299s
FAIL

hairyhenderson avatar Nov 26 '25 21:11 hairyhenderson

I can't find the original discussion about this but it is designed to be run in goroutine of the test.

https://github.com/open-feature/go-sdk/blob/40fe27eca801dc7ffb4865e403bc2f407fddc339/openfeature/testing/testprovider.go#L34-L37

erka avatar Nov 26 '25 22:11 erka