From c7bfdda1aa522358fc530e981db51dd8a605ff2a Mon Sep 17 00:00:00 2001 From: Yasin Turan Date: Thu, 18 Jan 2024 21:15:49 +0000 Subject: [PATCH] Add metadata reader benchmarks Signed-off-by: Yasin Turan --- metadata/metadata.go | 8 +- metadata/reader_test.go | 260 +++++++++++++++++++++++++++++++++++----- metadata/util_test.go | 208 +++++++++----------------------- util/testutil/tar.go | 131 +++++++++++++++++++- util/testutil/util.go | 20 +++- 5 files changed, 440 insertions(+), 187 deletions(-) diff --git a/metadata/metadata.go b/metadata/metadata.go index b6a5c8864..8724e0346 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -41,7 +41,7 @@ import ( "github.com/awslabs/soci-snapshotter/ztoc/compression" ) -// Attr reprensents the attributes of a node. +// Attr represents the attributes of a node. type Attr struct { // Size, for regular files, is the logical size of the file. Size int64 @@ -102,7 +102,7 @@ type Options struct { Telemetry *Telemetry } -// Option is an option to configure the behaviour of reader. +// Option is an option to configure the behavior of reader. type Option func(o *Options) error // WithTelemetry option specifies the telemetry hooks @@ -113,10 +113,10 @@ func WithTelemetry(telemetry *Telemetry) Option { } } -// A func which takes start time and records the diff +// MeasureLatencyHook is a func which takes start time and records the diff type MeasureLatencyHook func(time.Time) -// A struct which defines telemetry hooks. By implementing these hooks you should be able to record +// Telemetry defines telemetry hooks. By implementing these hooks you should be able to record // the latency metrics of the respective steps of SOCI open operation. type Telemetry struct { InitMetadataStoreLatency MeasureLatencyHook // measure time to initialize metadata store (in milliseconds) diff --git a/metadata/reader_test.go b/metadata/reader_test.go index 3b5379579..640384dac 100644 --- a/metadata/reader_test.go +++ b/metadata/reader_test.go @@ -33,47 +33,253 @@ package metadata import ( - "io" + "compress/gzip" + "fmt" + _ "net/http/pprof" "os" "testing" + "time" + "github.com/awslabs/soci-snapshotter/util/testutil" "github.com/awslabs/soci-snapshotter/ztoc" - bolt "go.etcd.io/bbolt" + "golang.org/x/sync/errgroup" ) +var allowedPrefix = [4]string{"", "./", "/", "../"} + +var srcCompressions = map[string]int{ + "gzip-nocompression": gzip.NoCompression, + "gzip-bestspeed": gzip.BestSpeed, + "gzip-bestcompression": gzip.BestCompression, + "gzip-defaultcompression": gzip.DefaultCompression, + "gzip-huffmanonly": gzip.HuffmanOnly, +} + func TestMetadataReader(t *testing.T) { - testReader(t, newTestableReader) + sampleTime := time.Now().Truncate(time.Second) + tests := []struct { + name string + in []testutil.TarEntry + want []check + }{ + { + name: "files", + in: []testutil.TarEntry{ + testutil.File("foo", "foofoo", testutil.WithFileMode(0644|os.ModeSetuid)), + testutil.Dir("bar/"), + testutil.File("bar/baz.txt", "bazbazbaz", testutil.WithFileOwner(1000, 1000)), + testutil.File("xxx.txt", "xxxxx", testutil.WithFileModTime(sampleTime)), + testutil.File("y.txt", "", testutil.WithFileXattrs(map[string]string{"testkey": "testval"})), + }, + want: []check{ + numOfNodes(6), // root dir + 1 dir + 4 files + hasFile("foo", 6), + hasMode("foo", 0644|os.ModeSetuid), + hasFile("bar/baz.txt", 9), + hasOwner("bar/baz.txt", 1000, 1000), + hasFile("xxx.txt", 5), + hasModTime("xxx.txt", sampleTime), + hasFile("y.txt", 0), + // For details on the keys of Xattrs, see https://pkg.go.dev/archive/tar#Header + hasXattrs("y.txt", map[string]string{"testkey": "testval"}), + }, + }, + { + name: "dirs", + in: []testutil.TarEntry{ + testutil.Dir("foo/", testutil.WithDirMode(os.ModeDir|0600|os.ModeSticky)), + testutil.Dir("foo/bar/", testutil.WithDirOwner(1000, 1000)), + testutil.File("foo/bar/baz.txt", "testtest"), + testutil.File("foo/bar/xxxx", "x"), + testutil.File("foo/bar/yyy", "yyy"), + testutil.Dir("foo/a/", testutil.WithDirModTime(sampleTime)), + testutil.Dir("foo/a/1/", testutil.WithDirXattrs(map[string]string{"testkey": "testval"})), + testutil.File("foo/a/1/2", "1111111111"), + }, + want: []check{ + numOfNodes(9), // root dir + 4 dirs + 4 files + hasDirChildren("foo", "bar", "a"), + hasDirChildren("foo/bar", "baz.txt", "xxxx", "yyy"), + hasDirChildren("foo/a", "1"), + hasDirChildren("foo/a/1", "2"), + hasMode("foo", os.ModeDir|0600|os.ModeSticky), + hasOwner("foo/bar", 1000, 1000), + hasModTime("foo/a", sampleTime), + hasXattrs("foo/a/1", map[string]string{"testkey": "testval"}), + hasFile("foo/bar/baz.txt", 8), + hasFile("foo/bar/xxxx", 1), + hasFile("foo/bar/yyy", 3), + hasFile("foo/a/1/2", 10), + }, + }, + { + name: "hardlinks", + in: []testutil.TarEntry{ + testutil.File("foo", "foofoo", testutil.WithFileOwner(1000, 1000)), + testutil.Dir("bar/"), + testutil.Link("bar/foolink", "foo"), + testutil.Link("bar/foolink2", "bar/foolink"), + testutil.Dir("bar/1/"), + testutil.File("bar/1/baz.txt", "testtest"), + testutil.Link("barlink", "bar/1/baz.txt"), + testutil.Symlink("foosym", "bar/foolink2"), + }, + want: []check{ + numOfNodes(6), // root dir + 2 dirs + 1 flie(linked) + 1 file(linked) + 1 symlink + hasFile("foo", 6), + hasOwner("foo", 1000, 1000), + hasFile("bar/foolink", 6), + hasOwner("bar/foolink", 1000, 1000), + hasFile("bar/foolink2", 6), + hasOwner("bar/foolink2", 1000, 1000), + hasFile("bar/1/baz.txt", 8), + hasFile("barlink", 8), + hasDirChildren("bar", "foolink", "foolink2", "1"), + hasDirChildren("bar/1", "baz.txt"), + sameNodes("foo", "bar/foolink", "bar/foolink2"), + sameNodes("bar/1/baz.txt", "barlink"), + linkName("foosym", "bar/foolink2"), + hasNumLink("foo", 3), // parent dir + 2 links + hasNumLink("barlink", 2), // parent dir + 1 link + hasNumLink("bar", 3), // parent + "." + child's ".." + }, + }, + { + name: "various files", + in: []testutil.TarEntry{ + testutil.Dir("bar/"), + testutil.File("bar/../bar///////////////////foo", ""), + testutil.Chardev("bar/cdev", 10, 11), + testutil.Blockdev("bar/bdev", 100, 101), + testutil.Fifo("bar/fifo"), + }, + want: []check{ + numOfNodes(6), // root dir + 1 file + 1 dir + 1 cdev + 1 bdev + 1 fifo + hasFile("bar/foo", 0), + hasChardev("bar/cdev", 10, 11), + hasBlockdev("bar/bdev", 100, 101), + hasFifo("bar/fifo"), + }, + }, + } + for _, tt := range tests { + for _, prefix := range allowedPrefix { + prefix := prefix + for srcCompresionName, srcCompression := range srcCompressions { + t.Run(tt.name+"-"+srcCompresionName, func(t *testing.T) { + opts := []testutil.BuildTarOption{ + testutil.WithPrefix(prefix), + } + + ztoc, sr, err := ztoc.BuildZtocReader(t, tt.in, srcCompression, 64, opts...) + if err != nil { + t.Fatalf("failed to build ztoc: %v", err) + } + telemetry, checkCalled := newCalledTelemetry() + + // create a metadata reader + r, err := newTestableReader(sr, ztoc.TOC, WithTelemetry(telemetry)) + if err != nil { + t.Fatalf("failed to create new reader: %v", err) + } + defer r.Close() + t.Logf("vvvvv Node tree vvvvv") + t.Logf("[%d] ROOT", r.RootID()) + dumpNodes(t, r, r.RootID(), 1) + t.Logf("^^^^^^^^^^^^^^^^^^^^^") + for _, want := range tt.want { + want(t, r) + } + if err := checkCalled(); err != nil { + t.Errorf("telemetry failure: %v", err) + } + }) + } + } + } } -func newTestableReader(sr *io.SectionReader, toc ztoc.TOC, opts ...Option) (testableReader, error) { - f, err := os.CreateTemp("", "readertestdb") +func BenchmarkMetadataReader(b *testing.B) { + testCases := []struct { + name string + entries int + }{ + { + name: "Create metadata.Reader with few TOC entries", + entries: 1000, + }, + { + name: "Create metadata.Reader with a good amount TOC entries", + entries: 10_000, + }, + { + name: "Create metadata.Reader with many TOC entries", + entries: 50_000, + }, + { + name: "Create metadata.Reader with an enormous amount of TOC entries", + entries: 100_000, + }, + } + + for _, tc := range testCases { + tempDB, clean, err := newTempDB() + defer clean() + if err != nil { + b.Fatalf("failed to initialize temp db: %v", err) + } + toc, err := generateTOC(tc.entries) + if err != nil { + b.Fatalf("failed to generate TOC: %v", err) + } + b.ResetTimer() + b.Run(tc.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := NewReader(tempDB, nil, toc); err != nil { + b.Fatalf("failed to create new reader: %v", err) + } + } + + }) + } +} + +func BenchmarkConcurrentMetadataReader(b *testing.B) { + smallTOC, err := generateTOC(1000) if err != nil { - return nil, err + b.Fatalf("failed to generate TOC: %v", err) } - defer os.Remove(f.Name()) - db, err := bolt.Open(f.Name(), 0600, nil) + mediumTOC, err := generateTOC(10_000) if err != nil { - return nil, err + b.Fatalf("failed to generate TOC: %v", err) } - r, err := NewReader(db, sr, toc, opts...) + largeTOC, err := generateTOC(50_000) if err != nil { - return nil, err + b.Fatalf("failed to generate TOC: %v", err) } - return &testableReadCloser{ - testableReader: r.(*reader), - closeFn: func() error { - db.Close() - return os.Remove(f.Name()) - }, - }, nil -} - -type testableReadCloser struct { - testableReader - closeFn func() error -} + tempDB, clean, err := newTempDB() + defer clean() + if err != nil { + b.Fatalf("failed to initialize temp db: %v", err) + } + tocs := []ztoc.TOC{smallTOC, mediumTOC, largeTOC} + var eg errgroup.Group + b.ResetTimer() + b.Run("Write small, medium and large TOC concurrently", func(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, toc := range tocs { + toc := toc + eg.Go(func() error { + if _, err := NewReader(tempDB, nil, toc); err != nil { + return fmt.Errorf("failed to create new reader: %v", err) + } + return nil + }) + } + if err := eg.Wait(); err != nil { + b.Fatal(err) + } -func (r *testableReadCloser) Close() error { - r.closeFn() - return r.testableReader.Close() + } + }) } diff --git a/metadata/util_test.go b/metadata/util_test.go index 133dbbf03..ef4752baa 100644 --- a/metadata/util_test.go +++ b/metadata/util_test.go @@ -46,168 +46,62 @@ import ( "github.com/awslabs/soci-snapshotter/util/testutil" "github.com/awslabs/soci-snapshotter/ztoc" + "go.etcd.io/bbolt" ) -var allowedPrefix = [4]string{"", "./", "/", "../"} - -var srcCompressions = map[string]int{ - "gzip-nocompression": gzip.NoCompression, - "gzip-bestspeed": gzip.BestSpeed, - "gzip-bestcompression": gzip.BestCompression, - "gzip-defaultcompression": gzip.DefaultCompression, - "gzip-huffmanonly": gzip.HuffmanOnly, -} - -type readerFactory func(sr *io.SectionReader, toc ztoc.TOC, opts ...Option) (r testableReader, err error) +type closeDB func() error type testableReader interface { Reader NumOfNodes() (i int, _ error) } -// testReader tests Reader returns correct file metadata. -func testReader(t *testing.T, factory readerFactory) { - sampleTime := time.Now().Truncate(time.Second) - tests := []struct { - name string - in []testutil.TarEntry - want []check - }{ - { - name: "files", - in: []testutil.TarEntry{ - testutil.File("foo", "foofoo", testutil.WithFileMode(0644|os.ModeSetuid)), - testutil.Dir("bar/"), - testutil.File("bar/baz.txt", "bazbazbaz", testutil.WithFileOwner(1000, 1000)), - testutil.File("xxx.txt", "xxxxx", testutil.WithFileModTime(sampleTime)), - testutil.File("y.txt", "", testutil.WithFileXattrs(map[string]string{"testkey": "testval"})), - }, - want: []check{ - numOfNodes(6), // root dir + 1 dir + 4 files - hasFile("foo", 6), - hasMode("foo", 0644|os.ModeSetuid), - hasFile("bar/baz.txt", 9), - hasOwner("bar/baz.txt", 1000, 1000), - hasFile("xxx.txt", 5), - hasModTime("xxx.txt", sampleTime), - hasFile("y.txt", 0), - // For details on the keys of Xattrs, see https://pkg.go.dev/archive/tar#Header - hasXattrs("y.txt", map[string]string{"testkey": "testval"}), - }, - }, - { - name: "dirs", - in: []testutil.TarEntry{ - testutil.Dir("foo/", testutil.WithDirMode(os.ModeDir|0600|os.ModeSticky)), - testutil.Dir("foo/bar/", testutil.WithDirOwner(1000, 1000)), - testutil.File("foo/bar/baz.txt", "testtest"), - testutil.File("foo/bar/xxxx", "x"), - testutil.File("foo/bar/yyy", "yyy"), - testutil.Dir("foo/a/", testutil.WithDirModTime(sampleTime)), - testutil.Dir("foo/a/1/", testutil.WithDirXattrs(map[string]string{"testkey": "testval"})), - testutil.File("foo/a/1/2", "1111111111"), - }, - want: []check{ - numOfNodes(9), // root dir + 4 dirs + 4 files - hasDirChildren("foo", "bar", "a"), - hasDirChildren("foo/bar", "baz.txt", "xxxx", "yyy"), - hasDirChildren("foo/a", "1"), - hasDirChildren("foo/a/1", "2"), - hasMode("foo", os.ModeDir|0600|os.ModeSticky), - hasOwner("foo/bar", 1000, 1000), - hasModTime("foo/a", sampleTime), - hasXattrs("foo/a/1", map[string]string{"testkey": "testval"}), - hasFile("foo/bar/baz.txt", 8), - hasFile("foo/bar/xxxx", 1), - hasFile("foo/bar/yyy", 3), - hasFile("foo/a/1/2", 10), - }, - }, - { - name: "hardlinks", - in: []testutil.TarEntry{ - testutil.File("foo", "foofoo", testutil.WithFileOwner(1000, 1000)), - testutil.Dir("bar/"), - testutil.Link("bar/foolink", "foo"), - testutil.Link("bar/foolink2", "bar/foolink"), - testutil.Dir("bar/1/"), - testutil.File("bar/1/baz.txt", "testtest"), - testutil.Link("barlink", "bar/1/baz.txt"), - testutil.Symlink("foosym", "bar/foolink2"), - }, - want: []check{ - numOfNodes(6), // root dir + 2 dirs + 1 flie(linked) + 1 file(linked) + 1 symlink - hasFile("foo", 6), - hasOwner("foo", 1000, 1000), - hasFile("bar/foolink", 6), - hasOwner("bar/foolink", 1000, 1000), - hasFile("bar/foolink2", 6), - hasOwner("bar/foolink2", 1000, 1000), - hasFile("bar/1/baz.txt", 8), - hasFile("barlink", 8), - hasDirChildren("bar", "foolink", "foolink2", "1"), - hasDirChildren("bar/1", "baz.txt"), - sameNodes("foo", "bar/foolink", "bar/foolink2"), - sameNodes("bar/1/baz.txt", "barlink"), - linkName("foosym", "bar/foolink2"), - hasNumLink("foo", 3), // parent dir + 2 links - hasNumLink("barlink", 2), // parent dir + 1 link - hasNumLink("bar", 3), // parent + "." + child's ".." - }, - }, - { - name: "various files", - in: []testutil.TarEntry{ - testutil.Dir("bar/"), - testutil.File("bar/../bar///////////////////foo", ""), - testutil.Chardev("bar/cdev", 10, 11), - testutil.Blockdev("bar/bdev", 100, 101), - testutil.Fifo("bar/fifo"), - }, - want: []check{ - numOfNodes(6), // root dir + 1 file + 1 dir + 1 cdev + 1 bdev + 1 fifo - hasFile("bar/foo", 0), - hasChardev("bar/cdev", 10, 11), - hasBlockdev("bar/bdev", 100, 101), - hasFifo("bar/fifo"), - }, - }, +// newTestableReader creates a test bbolt db as-well as a metadata.Reader given a TOC. +func newTestableReader(sr *io.SectionReader, toc ztoc.TOC, opts ...Option) (testableReader, error) { + db, clean, err := newTempDB() + if err != nil { + clean() + return nil, err } - for _, tt := range tests { - for _, prefix := range allowedPrefix { - prefix := prefix - for srcCompresionName, srcCompression := range srcCompressions { - t.Run(tt.name+"-"+srcCompresionName, func(t *testing.T) { - opts := []testutil.BuildTarOption{ - testutil.WithPrefix(prefix), - } - - ztoc, sr, err := ztoc.BuildZtocReader(t, tt.in, srcCompression, 64, opts...) - if err != nil { - t.Fatalf("failed to build ztoc: %v", err) - } - telemetry, checkCalled := newCalledTelemetry() - - // create a metadata reader - r, err := factory(sr, ztoc.TOC, WithTelemetry(telemetry)) - if err != nil { - t.Fatalf("failed to create new reader: %v", err) - } - defer r.Close() - t.Logf("vvvvv Node tree vvvvv") - t.Logf("[%d] ROOT", r.RootID()) - dumpNodes(t, r, r.RootID(), 1) - t.Logf("^^^^^^^^^^^^^^^^^^^^^") - for _, want := range tt.want { - want(t, r) - } - if err := checkCalled(); err != nil { - t.Errorf("telemetry failure: %v", err) - } - }) - } - } + r, err := NewReader(db, sr, toc, opts...) + if err != nil { + return nil, err } + return &testableReadCloser{ + testableReader: r.(*reader), + closeFn: clean, + }, nil +} + +type testableReadCloser struct { + testableReader + closeFn closeDB +} + +func (r *testableReadCloser) Close() error { + r.closeFn() + return r.testableReader.Close() +} + +// newTempDB creates a test bbolt db. +func newTempDB() (*bbolt.DB, func() error, error) { + f, err := os.CreateTemp("", "readertestdb") + if err != nil { + return nil, func() error { return nil }, err + } + db, err := bbolt.Open(f.Name(), 0600, nil) + if err != nil { + return nil, func() error { + return os.Remove(f.Name()) + }, err + } + return db, func() error { + err := db.Close() + if err != nil { + return err + } + return os.Remove(f.Name()) + }, err } func newCalledTelemetry() (telemetry *Telemetry, check func() error) { @@ -540,3 +434,13 @@ func lookup(r testableReader, name string) (uint32, error) { id, _, err := r.GetChild(pid, base) return id, err } + +// generateTOC generates a random TOC with a given amount of entries. +func generateTOC(numEntries int, opts ...testutil.TarEntriesOption) (ztoc.TOC, error) { + tarEntries, err := testutil.GenerateTarEntries(numEntries, opts...) + if err != nil { + return ztoc.TOC{}, err + } + ztoc, _, err := ztoc.BuildZtocReader(nil, tarEntries, gzip.DefaultCompression, 64) + return ztoc.TOC, err +} diff --git a/util/testutil/tar.go b/util/testutil/tar.go index e06a99dff..f18d2a3be 100644 --- a/util/testutil/tar.go +++ b/util/testutil/tar.go @@ -189,7 +189,7 @@ func BuildTarZstd(ents []TarEntry, compressionLevel int, opts ...BuildTarOption) // WriteTarToTempFile writes the contents of a tar archive to a specified path and // return the temp filename and the tar data (as []byte). // -// It's the caller's responsibility to remove the genreated temp file. +// It's the caller's responsibility to remove the generated temp file. func WriteTarToTempFile(tarNamePattern string, tarReader io.Reader) (string, []byte, error) { tarFile, err := os.CreateTemp("", tarNamePattern) if err != nil { @@ -493,3 +493,132 @@ func permAndExtraMode2TarMode(fm os.FileMode) (tm int64) { } return } + +type TarEntriesOptions struct { + // maxChildren controls the maximum number of children a directory could have. + maxChildren int + // regProbability controls the ratio of regular files in the TAR. + regProbability float32 +} + +type TarEntriesOption func(te *TarEntriesOptions) + +func WithMaxChildren(m int) TarEntriesOption { + return func(te *TarEntriesOptions) { + te.maxChildren = m + } +} + +func WithRegProbability(p float32) TarEntriesOption { + return func(te *TarEntriesOptions) { + te.regProbability = p + } +} + +func parseEntriesOptions(n int, te *TarEntriesOptions) error { + switch { + case n < 0: + return fmt.Errorf("entry count must not be less than 0") + case te.maxChildren < 0: + return fmt.Errorf("max children must not be less than 0") + case te.maxChildren > n: + return fmt.Errorf("cannot have more children than the required entry count") + } + + if te.regProbability < 0 { + te.regProbability = 0 + } else if te.regProbability == 0 { + te.regProbability = 0.5 + } + if te.maxChildren == 0 { + te.maxChildren = (n / 2) + 1 + } + return nil +} + +// GenerateRandomTarEntries generates a random set of tar entries of some given size. It creates +// an m-ary tree of file nodes and corresponding TarEntries for each. +func GenerateTarEntries(entriesCount int, opts ...TarEntriesOption) ([]TarEntry, error) { + te := &TarEntriesOptions{} + for _, opt := range opts { + opt(te) + } + if err := parseEntriesOptions(entriesCount, te); err != nil { + return nil, err + } + + var tarEntries []TarEntry + + rootNode := &node{path: "", leaf: false} + nodeQueue := make([]*node, entriesCount+1) + nodeQueue[0] = rootNode + insertionIndex := 0 + for insertionIndex < len(nodeQueue)-1 { + // POP() the first item in the queue + nd := nodeQueue[0] + nodeQueue = nodeQueue[1:] + // If the node is a leaf, continue. + if nd.isLeaf() { + insertionIndex = insertionIndex - 1 + continue + } + remainingEmptyNodes := len(nodeQueue) - insertionIndex + // Get children count [1,maxChildren]. The children count must not + // exceed the remaining empty nodes, so we take the min. + m := min(randRangeInc(1, te.maxChildren), remainingEmptyNodes) + endIndex := insertionIndex + m + nonLeafIndex := randRangeInc(insertionIndex, endIndex-1) + + // Starting from the insertionIndex, create m children and add them to the queue. + for k := insertionIndex; k < endIndex; k++ { + childNode := &node{} + // At least 1 child node needs to a non-leaf, unless it is the last node in the tree. + if k == nonLeafIndex && k != len(nodeQueue)-1 { + childNode.leaf = false + } else { + childNode.leaf = shouldBeLeaf(te.regProbability) + } + path := RandString(10) + if childNode.isLeaf() { + childNode.path = fmt.Sprintf("%s%s.txt", nd.path, path) + tarEntries = append(tarEntries, File(childNode.path, "")) + } else { + childNode.path = fmt.Sprintf("%s%s/", nd.path, path) + tarEntries = append(tarEntries, Dir(childNode.path)) + } + // ENQUEUE() childNode + nodeQueue[k] = childNode + } + insertionIndex = (insertionIndex - 1) + m + } + + return tarEntries, nil +} + +// randRangeInc returns a random range between [min,max] inclusive. +func randRangeInc(min int, max int) int { + if min == max { + return min + } + return r.Intn((max+1)-min) + min +} + +func shouldBeLeaf(p float32) bool { + return r.Float32() < p +} + +type node struct { + path string + leaf bool +} + +func (n *node) isLeaf() bool { + return n.leaf +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/util/testutil/util.go b/util/testutil/util.go index d753e6234..7710d44ec 100644 --- a/util/testutil/util.go +++ b/util/testutil/util.go @@ -127,6 +127,12 @@ func (tsr *ThreadsafeRandom) Intn(n int) int { return tsr.r.Intn(n) } +func (tsr *ThreadsafeRandom) Float32() float32 { + tsr.l.Lock() + defer tsr.l.Unlock() + return tsr.r.Float32() +} + func (tsr *ThreadsafeRandom) Int63() int64 { tsr.l.Lock() defer tsr.l.Unlock() @@ -139,6 +145,8 @@ func (tsr *ThreadsafeRandom) Read(b []byte) (int, error) { return tsr.r.Read(b) } +const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + " " + var r = NewThreadsafeRandom() // RandomUInt64 returns a random uint64 value generated from /dev/uramdom. @@ -164,9 +172,6 @@ func RandomByteData(size int64) []byte { // RandomByteDataRange returns a byte slice with `size` between minBytes and maxBytes exclusive populated with random data func RandomByteDataRange(minBytes int, maxBytes int) []byte { - const charset = "abcdefghijklmnopqrstuvwxyz" + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + " " - r := NewThreadsafeRandom() randByteNum := r.Intn(maxBytes-minBytes) + minBytes randBytes := make([]byte, randByteNum) @@ -181,3 +186,12 @@ func RandomDigest() string { d := digest.FromBytes(RandomByteData(10)) return d.String() } + +// RandString returns a random string of length n. +func RandString(n int) string { + randBytes := make([]byte, n) + for i := range randBytes { + randBytes[i] = charset[rand.Intn(len(charset))] + } + return string(randBytes) +}