Skip to content

Commit

Permalink
safety guard to avoid typos with testcase environment variables
Browse files Browse the repository at this point in the history
  • Loading branch information
adamluzsi committed May 29, 2024
1 parent dc2c5ba commit c794c30
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export PATH="${WDP}/.tools:${PATH}"

export GO111MODULE=on
export CGO_ENABLED=1
export TESTCASE_PP_DEBUG=TRUE
export TESTCASE_DEBUG=TRUE
5 changes: 3 additions & 2 deletions Spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/internal/doubles"
"go.llib.dev/testcase/internal/environ"
"go.llib.dev/testcase/internal/spechelper"
"go.llib.dev/testcase/random"
"go.llib.dev/testcase/sandbox"
Expand Down Expand Up @@ -1361,8 +1362,8 @@ func TestSpec_Test_whenTestingTBIsGivenThatDoesNotSupportTBRunner_executesOnFini
}

func TestSpec_Test_testingTBNoTBRunner_ordered(t *testing.T) {
testcase.SetEnv(t, testcase.EnvKeySeed, "42")
testcase.SetEnv(t, testcase.EnvKeyOrdering, string(testcase.OrderingAsRandom))
testcase.SetEnv(t, environ.KeySeed, "42")
testcase.SetEnv(t, environ.KeyOrdering, string(testcase.OrderingAsRandom))
testcase.NewSpec(t).Context("", func(s *testcase.Spec) {
var out []int
s.Test(``, func(t *testcase.T) { out = append(out, 1) })
Expand Down
3 changes: 2 additions & 1 deletion T_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/contracts"
"go.llib.dev/testcase/internal/doubles"
"go.llib.dev/testcase/internal/environ"
"go.llib.dev/testcase/sandbox"

"go.llib.dev/testcase/random"
Expand Down Expand Up @@ -349,7 +350,7 @@ func TestT_Random(t *testing.T) {
}

t.Run(`when environment value is set`, func(t *testing.T) {
testcase.SetEnv(t, testcase.EnvKeySeed, `42`)
testcase.SetEnv(t, environ.KeySeed, `42`)
s := testcase.NewSpec(t)
s.Test(``, func(t *testcase.T) {
t.Must.NotEmpty(t.Random)
Expand Down
3 changes: 2 additions & 1 deletion TableTest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (

"go.llib.dev/testcase"
"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/internal/environ"
)

func TestTableTest_orderIsDeterministic(t *testing.T) {
t.Setenv(testcase.EnvKeySeed, "42")
t.Setenv(environ.KeySeed, "42")

var cases = map[string]int{
"1": 1,
Expand Down
14 changes: 0 additions & 14 deletions env.go

This file was deleted.

58 changes: 58 additions & 0 deletions internal/environ/environ.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package environ

import (
"os"
"slices"
"strings"

"go.llib.dev/testcase/internal"
)

// KeySeed is the environment variable key that will be checked for a pseudo random seed,
// which will be used to randomize the order of executions between test cases.
const KeySeed = `TESTCASE_SEED`

// KeyOrdering is the environment variable key that will be checked for testCase determine
// what order of execution should be used between test cases in a testing group.
// The default sorting behavior is pseudo random based on an the seed.
//
// Mods:
// - defined: execute testCase in the order which they are being defined
// - random: pseudo random based ordering between tests.
const KeyOrdering = `TESTCASE_ORDERING`
const KeyOrdering2 = `TESTCASE_ORDER`

const KeyDebug = "TESTCASE_DEBUG"

var acceptedKeys = []string{
KeySeed,
KeyOrdering,
KeyOrdering2,
KeyDebug,
}

func init() { CheckEnvKeys() }

func CheckEnvKeys() {
var got bool
for _, envPair := range os.Environ() {
ekv := strings.SplitN(envPair, "=", 2) // best effort to split, but it might not be platform agnostic
if len(ekv) != 2 {
continue
}
key, _ := ekv[0], ekv[1]

if !strings.HasPrefix(key, "TESTCASE_") {
continue
}

if !slices.Contains(acceptedKeys, key) {
got = true
internal.Warn("unrecognised testcase variable:", key)
}
}
if got {
internal.Warn("check if you might have a typo.")
internal.Warn("accepted environment variables:", strings.Join(acceptedKeys, ", "))
}
}
32 changes: 32 additions & 0 deletions internal/environ/environ_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package environ_test

import (
"fmt"
"testing"

"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/internal"
"go.llib.dev/testcase/internal/env"
"go.llib.dev/testcase/internal/environ"
"go.llib.dev/testcase/random"
)

func Test_checkEnvKeys(t *testing.T) {
t.Run("when invalid testcase env variable is present in the env", func(t *testing.T) {
out := internal.StubWarn(t)
rnd := random.New(random.CryptoSeed{})
key := fmt.Sprintf("TESTCASE_%s", rnd.StringNC(rnd.IntB(0, 10), random.CharsetASCII()))
env.SetEnv(t, key, rnd.StringNC(5, random.CharsetASCII()))
environ.CheckEnvKeys()
assert.NotEmpty(t, out.String())
assert.Contain(t, out.String(), key)
assert.Contain(t, out.String(), "typo")
})
t.Run("when only valid env variables are present in the env", func(t *testing.T) {
out := internal.StubWarn(t)
env.SetEnv(t, environ.KeySeed, "123")
env.SetEnv(t, environ.KeyOrdering, "defined")
environ.CheckEnvKeys()
assert.Empty(t, out.String())
})
}
3 changes: 2 additions & 1 deletion internal/spechelper/ordering.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal"
"go.llib.dev/testcase/internal/environ"
)

func OrderAsDefined(tb testing.TB) {
internal.SetupCacheFlush(tb)
testcase.SetEnv(tb, testcase.EnvKeyOrdering, string(testcase.OrderingAsDefined))
testcase.SetEnv(tb, environ.KeyOrdering, string(testcase.OrderingAsDefined))
}
24 changes: 24 additions & 0 deletions internal/warn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package internal

import (
"bytes"
"fmt"
"io"
"os"
"testing"
)

var warnOutput io.Writer = os.Stderr

func Warn(vs ...any) {
out := append([]any{"[WARN]", "[TESTCASE]"}, vs...)
fmt.Fprintln(warnOutput, out...)
}

func StubWarn(tb testing.TB) *bytes.Buffer {
original := warnOutput
tb.Cleanup(func() { warnOutput = original })
var buf bytes.Buffer
warnOutput = &buf
return &buf
}
3 changes: 2 additions & 1 deletion ordering.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"go.llib.dev/testcase/internal"
"go.llib.dev/testcase/internal/environ"
)

func newOrderer(tb testing.TB, seed int64) orderer {
Expand Down Expand Up @@ -77,7 +78,7 @@ func getGlobalOrderMod(tb testing.TB) testOrderingMod {
}

func getOrderingModFromENV() testOrderingMod {
mod, ok := os.LookupEnv(EnvKeyOrdering)
mod, ok := os.LookupEnv(environ.KeyOrdering)
if !ok {
return OrderingAsRandom
}
Expand Down
12 changes: 9 additions & 3 deletions ordering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/internal"
"go.llib.dev/testcase/internal/environ"
"go.llib.dev/testcase/random"
)

var ord = Var[orderer]{ID: `orderer`}
Expand Down Expand Up @@ -177,9 +179,13 @@ func TestNewOrderer(t *testing.T) {
internal.SetupCacheFlush(t)
})

orderingEnvKey := Let[string](s, func(t *T) string {
return random.Pick(t.Random, environ.KeyOrdering, environ.KeyOrdering2)
})

s.When(`mod is unknown`, func(s *Spec) {
s.Before(func(t *T) {
SetEnv(t, EnvKeyOrdering, `unknown`)
SetEnv(t, orderingEnvKey.Get(t), `unknown`)
})

s.Then(`it will panic`, func(t *T) {
Expand All @@ -189,7 +195,7 @@ func TestNewOrderer(t *testing.T) {

s.When(`mod is random`, func(s *Spec) {
s.Before(func(t *T) {
SetEnv(t, EnvKeyOrdering, string(OrderingAsRandom))
SetEnv(t, orderingEnvKey.Get(t), string(OrderingAsRandom))
})

s.Then(`random orderer provided`, func(t *T) {
Expand All @@ -201,7 +207,7 @@ func TestNewOrderer(t *testing.T) {

s.When(`mod set ordering as tests are defined`, func(s *Spec) {
s.Before(func(t *T) {
SetEnv(t, EnvKeyOrdering, string(OrderingAsDefined))
SetEnv(t, environ.KeyOrdering, string(OrderingAsDefined))
})

s.Then(`null orderer provided`, func(t *T) {
Expand Down
2 changes: 1 addition & 1 deletion pp/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
var debug = false

func init() {
v, ok := os.LookupEnv("TESTCASE_PP_DEBUG")
v, ok := os.LookupEnv("TESTCASE_DEBUG")
if !ok {
return
}
Expand Down
10 changes: 6 additions & 4 deletions seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,28 @@ package testcase

import (
"fmt"
"go.llib.dev/testcase/random"
"math/rand"
"os"
"strconv"
"testing"
"time"

"go.llib.dev/testcase/random"

"go.llib.dev/testcase/internal"
"go.llib.dev/testcase/internal/environ"
)

func makeSeed() (int64, error) {
rawSeed, injectedRandomSeedIsSet := os.LookupEnv(EnvKeySeed)
rawSeed, injectedRandomSeedIsSet := os.LookupEnv(environ.KeySeed)
if !injectedRandomSeedIsSet {
salt := rand.New(random.CryptoSeed{}).Int63()
base := time.Now().UnixNano()
return base + salt, nil
}
seed, err := strconv.ParseInt(rawSeed, 10, 64)
if err != nil {
return 0, fmt.Errorf("%s has invalid seed integer value: %s", EnvKeySeed, rawSeed)
return 0, fmt.Errorf("%s has invalid seed integer value: %s", environ.KeySeed, rawSeed)
}
return seed, nil
}
Expand All @@ -33,7 +35,7 @@ func seedForSpec(tb testing.TB) (_seed int64) {
tb.Helper()
if tb.Failed() {
// Help developers to know the seed of the failed test execution.
internal.Log(tb, fmt.Sprintf(`%s=%d`, EnvKeySeed, _seed))
internal.Log(tb, fmt.Sprintf(`%s=%d`, environ.KeySeed, _seed))
}
})
}
Expand Down

0 comments on commit c794c30

Please sign in to comment.