v2/internal/testcommon/resource_namer.go (110 lines of code) (raw):

/* Copyright (c) Microsoft Corporation. Licensed under the MIT license. */ package testcommon import ( crypto "crypto/rand" "hash/fnv" "math/big" "math/rand" "strings" "time" "github.com/google/uuid" ) type ResourceNameConfig struct { runes []rune prefix string randomChars int separator string mode ResourceNamerMode } type ResourceNamerMode string const ( ResourceNamerModeRandomBasedOnTestName = ResourceNamerMode("basedOnTestName") ResourceNamerModeRandom = ResourceNamerMode("random") ) type ResourceNamer struct { ResourceNameConfig rand *rand.Rand } // WithTestName returns a new ResourceNamer configured based on the provided test name. // The mode of the original resource namer is preserved. func (n ResourceNamer) WithTestName(testName string) ResourceNamer { return n.NewResourceNamer(testName) } // NewResourceNamer returns a ResourceNamer that generates random // suffixes based upon the test name func (rnc ResourceNameConfig) NewResourceNamer(testName string) ResourceNamer { // Calculate an initial seed based on the test name hasher := fnv.New64() n, err := hasher.Write([]byte(testName)) if n != len(testName) || err != nil { panic("failed to write hash") } //nolint:gosec // don't care about int overflow seed := int64(hasher.Sum64()) // This seed is enough to get the same sequence of "random" values every time. // If we want a different sequence every time, include time as part of the seed too if rnc.mode == ResourceNamerModeRandom { t := time.Now().UnixNano() seed = seed ^ t } src := rand.NewSource(seed) //nolint:gosec // do not need/want cryptographic randomness here r := rand.New(src) return ResourceNamer{ ResourceNameConfig: rnc, rand: r, } } func NewResourceNameConfig(prefix string, separator string, randomChars int, mode ResourceNamerMode) *ResourceNameConfig { return &ResourceNameConfig{ runes: []rune("abcdefghijklmnopqrstuvwxyz"), prefix: prefix, randomChars: randomChars, separator: separator, mode: mode, } } // WithSeparator returns a copy of the ResourceNamer with the given separator func (n ResourceNamer) WithSeparator(separator string) ResourceNamer { n.separator = separator return n } // WithNumRandomChars returns a copy of the ResourceNamer which will generate names with // a random string of the given length included func (n ResourceNamer) WithNumRandomChars(num int) ResourceNamer { n.randomChars = num return n } func (n ResourceNamer) makeRandomStringOfLength(num int, runes []rune) string { result := make([]rune, num) for i := range result { result[i] = runes[n.rand.Intn(len(runes))] } return string(result) } func (n ResourceNamer) makeSecureStringOfLength(num int, runes []rune) string { result := make([]rune, num) for i := range result { // Ignoring error here since big.NewInt does not return `0 <= x < length` idx, _ := crypto.Int(crypto.Reader, big.NewInt(int64(len(runes)))) result[i] = runes[idx.Int64()] } return string(result) } func (n ResourceNamer) generateName(prefix string, num int) string { result := n.makeRandomStringOfLength(num, n.runes) var s []string // TODO: Do we need a check here for if both result and prefix are empty? if result == "" { s = []string{n.prefix, prefix} } else if prefix == "" { s = []string{n.prefix, result} } else { s = []string{n.prefix, prefix, result} } return strings.Join(s, n.separator) } func (n ResourceNamer) GenerateName(prefix string) string { return n.generateName(prefix, n.randomChars) } func (n ResourceNamer) GenerateNameOfLength(length int) string { return n.generateName("", length) } func (n ResourceNamer) GeneratePassword() string { return n.GeneratePasswordOfLength(n.randomChars) } var runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~!@#$%^*()._-") // GeneratePasswordOfLength generates and returns a non-deterministic password. // This method does not use any seed value, so the returned password is never stable. func (n ResourceNamer) GeneratePasswordOfLength(length int) string { // This pass + <content> + pass pattern is to make it so that matchers can reliably find and prune // generated passwords from the recordings. If you change it make sure to change the passwordMatcher // in test_context.go as well. return "pass" + n.makeSecureStringOfLength(length, runes) + "pass" } // GenerateSecretOfLength generates and returns a non-deterministic secret. // This method does not use any seed value, so the returned password is never stable. func (n ResourceNamer) GenerateSecretOfLength(length int) string { return n.makeSecureStringOfLength(length, runes) } func (n ResourceNamer) GenerateUUID() (uuid.UUID, error) { return uuid.NewRandomFromReader(n.rand) }