Skip to content

Commit

Permalink
Add GenerateAlphaNumericID for test-proxy to recording package (#20009)
Browse files Browse the repository at this point in the history
* Add GenerateAlphaNumericID for test-proxy to recording package

* refine with review comment
  • Loading branch information
tadelesh authored Feb 22, 2023
1 parent 8297762 commit 33d689e
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 44 deletions.
144 changes: 100 additions & 44 deletions sdk/internal/recording/recording.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,38 +237,8 @@ func (r *Recording) UUID() uuid.UUID {
// GenerateAlphaNumericID will generate a recorded random alpha numeric id
// if the recording has a randomSeed already set, the value will be generated from that seed, else a new random seed will be used
func (r *Recording) GenerateAlphaNumericID(prefix string, length int, lowercaseOnly bool) (string, error) {

if length <= len(prefix) {
return "", errors.New("length must be greater than prefix")
}

r.initRandomSource()

sb := strings.Builder{}
sb.Grow(length)
sb.WriteString(prefix)
i := length - len(prefix) - 1
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for cache, remain := r.src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = r.src.Int63(), letterIdxMax
}
if lowercaseOnly {
if idx := int(cache & letterIdxMask); idx < len(alphanumericLowercaseBytes) {
sb.WriteByte(alphanumericLowercaseBytes[idx])
i--
}
} else {
if idx := int(cache & letterIdxMask); idx < len(alphanumericBytes) {
sb.WriteByte(alphanumericBytes[idx])
i--
}
}
cache >>= letterIdxBits
remain--
}
str := sb.String()
return str, nil
return generateAlphaNumericID(prefix, length, lowercaseOnly, r.src)
}

// getRequiredEnv gets an environment variable by name and returns an error if it is not found
Expand Down Expand Up @@ -496,22 +466,27 @@ func init() {
}
}

var recordMode string
var rootCAs *x509.CertPool
var (
recordMode string
rootCAs *x509.CertPool
)

const (
RecordingMode = "record"
PlaybackMode = "playback"
LiveMode = "live"
IDHeader = "x-recording-id"
ModeHeader = "x-recording-mode"
UpstreamURIHeader = "x-recording-upstream-base-uri"
RecordingMode = "record"
PlaybackMode = "playback"
LiveMode = "live"
IDHeader = "x-recording-id"
ModeHeader = "x-recording-mode"
UpstreamURIHeader = "x-recording-upstream-base-uri"
recordingRandSeedVarKey = "randSeed"
)

type recordedTest struct {
recordingId string
liveOnly bool
variables map[string]interface{}
recordingId string
liveOnly bool
variables map[string]interface{}
recordingSeed int64
recordingRandSrc rand.Source
}

// testMap maps test names to metadata
Expand All @@ -534,6 +509,11 @@ func (t *testMap) Store(name string, data recordedTest) {
t.m.Store(name, data)
}

// Remove delete metadata for the named test
func (t *testMap) Remove(name string) {
t.m.Delete(name)
}

var testSuite = testMap{&sync.Map{}}

var client = http.Client{
Expand Down Expand Up @@ -778,6 +758,12 @@ func Stop(t *testing.T, options *RecordingOptions) error {
// test should only be run live, don't want to generate recording
return nil
}
if testStruct.recordingSeed != 0 {
if options.Variables == nil {
options.Variables = map[string]interface{}{}
}
options.Variables[recordingRandSeedVarKey] = strconv.FormatInt(testStruct.recordingSeed, 10)
}
}

url := fmt.Sprintf("%v/%v/stop", options.baseURL(), recordMode)
Expand All @@ -801,6 +787,7 @@ func Stop(t *testing.T, options *RecordingOptions) error {
return errors.New("Recording ID was never set. Did you call StartRecording?")
}
req.Header.Set(IDHeader, recTest.recordingId)
testSuite.Remove(t.Name())
resp, err := client.Do(req)
if resp.StatusCode != 200 {
b, err := io.ReadAll(resp.Body)
Expand All @@ -814,7 +801,76 @@ func Stop(t *testing.T, options *RecordingOptions) error {
return err
}

// This looks up an environment variable and if it is not found, returns the recordedValue
func getRandomSource(t *testing.T) rand.Source {
if testStruct, ok := testSuite.Load(t.Name()); ok {
if testStruct.recordingRandSrc != nil {
return testStruct.recordingRandSrc
}
}

var seed int64
var err error

variables := GetVariables(t)
seedString, ok := variables[recordingRandSeedVarKey]
if ok {
seed, err = strconv.ParseInt(seedString.(string), 10, 64)
}

// We did not have a random seed already stored; create a new one
if !ok || err != nil || GetRecordMode() == "live" {
seed = time.Now().Unix()
}

source := rand.NewSource(seed)
if testStruct, ok := testSuite.Load(t.Name()); ok {
testStruct.recordingSeed = seed
testStruct.recordingRandSrc = source
testSuite.Store(t.Name(), testStruct)
}

return source
}

// GenerateAlphaNumericID will generate a recorded random alpha numeric id.
// When live mode or the recording has a randomSeed already set, the value will be generated from that seed, else a new random seed will be used.
func GenerateAlphaNumericID(t *testing.T, prefix string, length int, lowercaseOnly bool) (string, error) {
return generateAlphaNumericID(prefix, length, lowercaseOnly, getRandomSource(t))
}

func generateAlphaNumericID(prefix string, length int, lowercaseOnly bool, randomSource rand.Source) (string, error) {
if length <= len(prefix) {
return "", errors.New("length must be greater than prefix")
}

sb := strings.Builder{}
sb.Grow(length)
sb.WriteString(prefix)
i := length - len(prefix) - 1
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for cache, remain := randomSource.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = randomSource.Int63(), letterIdxMax
}
if lowercaseOnly {
if idx := int(cache & letterIdxMask); idx < len(alphanumericLowercaseBytes) {
sb.WriteByte(alphanumericLowercaseBytes[idx])
i--
}
} else {
if idx := int(cache & letterIdxMask); idx < len(alphanumericBytes) {
sb.WriteByte(alphanumericBytes[idx])
i--
}
}
cache >>= letterIdxBits
remain--
}
str := sb.String()
return str, nil
}

// GetEnvVariable looks up an environment variable and if it is not found, returns the recordedValue
func GetEnvVariable(varName string, recordedValue string) string {
val, ok := os.LookupEnv(varName)
if !ok || GetRecordMode() == PlaybackMode {
Expand All @@ -835,7 +891,7 @@ func LiveOnly(t *testing.T) {
}
}

// Function for sleeping during a test for `duration` seconds. This method will only execute when
// Sleep during a test for `duration` seconds. This method will only execute when
// AZURE_RECORD_MODE = "record", if a test is running in playback this will be a noop.
func Sleep(duration time.Duration) {
if GetRecordMode() != PlaybackMode {
Expand Down
43 changes: 43 additions & 0 deletions sdk/internal/recording/recording_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -693,3 +694,45 @@ func TestRace(t *testing.T) {
})
}
}

func Test_generateAlphaNumericID(t *testing.T) {
seed1 := int64(1234567)
seed2 := int64(7654321)
randomSource1 := rand.NewSource(seed1)
randomSource2 := rand.NewSource(seed2)
randomSource3 := rand.NewSource(seed2)
rand1, err := generateAlphaNumericID("test", 10, false, randomSource1)
require.NoError(t, err)
require.Equal(t, 10, len(rand1))
require.Equal(t, "test", rand1[0:4])
rand2, err := generateAlphaNumericID("test", 10, false, randomSource2)
require.NoError(t, err)
rand3, err := generateAlphaNumericID("test", 10, false, randomSource3)
require.NoError(t, err)
require.Equal(t, rand2, rand3)
require.NotEqual(t, rand1, rand2)
}

func TestGenerateAlphaNumericID(t *testing.T) {
recordMode = RecordingMode
err := Start(t, packagePath, nil)
require.NoError(t, err)
rand1, err := GenerateAlphaNumericID(t, "test", 10, false)
require.NoError(t, err)
rand2, err := GenerateAlphaNumericID(t, "test", 10, false)
require.NoError(t, err)
require.NotEqual(t, rand1, rand2)
err = Stop(t, nil)
require.NoError(t, err)
recordMode = PlaybackMode
err = Start(t, packagePath, nil)
require.NoError(t, err)
rand3, err := GenerateAlphaNumericID(t, "test", 10, false)
require.NoError(t, err)
rand4, err := GenerateAlphaNumericID(t, "test", 10, false)
require.NoError(t, err)
require.Equal(t, rand1, rand3)
require.Equal(t, rand2, rand4)
err = Stop(t, nil)
require.NoError(t, err)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Entries": [],
"Variables": {
"randSeed": "1676365801"
}
}

0 comments on commit 33d689e

Please sign in to comment.