add functions/vendor files

This commit is contained in:
Reed Allman
2017-06-11 02:05:36 -07:00
parent 6ee9c1fa0a
commit f2c7aa5ee6
7294 changed files with 1629834 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
package statsdtest
import (
"errors"
"sync"
)
// RecordingSender implements statsd.Sender but parses individual Stats into a
// buffer that can be later inspected instead of sending to some server. It
// should constructed with NewRecordingSender().
type RecordingSender struct {
m sync.Mutex
buffer Stats
closed bool
}
// NewRecordingSender creates a new RecordingSender for use by a statsd.Client.
func NewRecordingSender() *RecordingSender {
rs := &RecordingSender{}
rs.buffer = make(Stats, 0)
return rs
}
// GetSent returns the stats that have been sent. Locks and copies the current
// state of the sent Stats.
//
// The entire buffer of Stat objects (including Stat.Raw is copied).
func (rs *RecordingSender) GetSent() Stats {
rs.m.Lock()
defer rs.m.Unlock()
results := make(Stats, len(rs.buffer))
for i, e := range rs.buffer {
results[i] = e
results[i].Raw = make([]byte, len(e.Raw))
for j, b := range e.Raw {
results[i].Raw[j] = b
}
}
return results
}
// ClearSent locks the sender and clears any Stats that have been recorded.
func (rs *RecordingSender) ClearSent() {
rs.m.Lock()
defer rs.m.Unlock()
rs.buffer = rs.buffer[:0]
}
// Send parses the provided []byte into stat objects and then appends these to
// the buffer of sent stats. Buffer operations are synchronized so it is safe
// to call this from multiple goroutines (though contenion will impact
// performance so don't use this during a benchmark). Send treats '\n' as a
// delimiter between multiple sats in the same []byte.
//
// Calling after the Sender has been closed will return an error (and not
// mutate the buffer).
func (rs *RecordingSender) Send(data []byte) (int, error) {
sent := ParseStats(data)
rs.m.Lock()
defer rs.m.Unlock()
if rs.closed {
return 0, errors.New("writing to a closed sender")
}
rs.buffer = append(rs.buffer, sent...)
return len(data), nil
}
// Close marks this sender as closed. Subsequent attempts to Send stats will
// result in an error.
func (rs *RecordingSender) Close() error {
rs.m.Lock()
defer rs.m.Unlock()
rs.closed = true
return nil
}

View File

@@ -0,0 +1,57 @@
package statsdtest
import (
"fmt"
"reflect"
"strconv"
"testing"
"time"
"github.com/cactus/go-statsd-client/statsd"
)
func TestRecordingSenderIsSender(t *testing.T) {
// This ensures that if the Sender interface changes in the future we'll get
// compile time failures should the RecordingSender not be updated to meet
// the new definition. This keeps changes from inadvertently breaking tests
// of folks that use go-statsd-client.
var _ statsd.Sender = NewRecordingSender()
}
func TestRecordingSender(t *testing.T) {
start := time.Now()
rs := new(RecordingSender)
statter, err := statsd.NewClientWithSender(rs, "test")
if err != nil {
t.Errorf("failed to construct client")
return
}
statter.Inc("stat", 4444, 1.0)
statter.Dec("stat", 5555, 1.0)
statter.Set("set-stat", "some string", 1.0)
d := time.Since(start)
statter.TimingDuration("timing", d, 1.0)
sent := rs.GetSent()
if len(sent) != 4 {
// just dive out because everything else relies on ordering
t.Fatalf("Did not capture all stats sent; got: %s", sent)
}
ms := float64(d) / float64(time.Millisecond)
// somewhat fragile in that it assums float rendering within client *shrug*
msStr := string(strconv.AppendFloat([]byte(""), ms, 'f', -1, 64))
expected := Stats{
{[]byte("test.stat:4444|c"), "test.stat", "4444", "c", "", true},
{[]byte("test.stat:-5555|c"), "test.stat", "-5555", "c", "", true},
{[]byte("test.set-stat:some string|s"), "test.set-stat", "some string", "s", "", true},
{[]byte(fmt.Sprintf("test.timing:%s|ms", msStr)), "test.timing", msStr, "ms", "", true},
}
if !reflect.DeepEqual(sent, expected) {
t.Errorf("got: %s, want: %s", sent, expected)
}
}

View File

@@ -0,0 +1,140 @@
package statsdtest
import (
"bytes"
"fmt"
"strings"
)
// Stat contains the raw and extracted stat information from a stat that was
// sent by the RecordingSender. Raw will always have the content that was
// consumed for this specific stat and Parsed will be set if no errors were hit
// pulling information out of it.
type Stat struct {
Raw []byte
Stat string
Value string
Tag string
Rate string
Parsed bool
}
// String fulfils the stringer interface
func (s *Stat) String() string {
return fmt.Sprintf("%s %s %s", s.Stat, s.Value, s.Rate)
}
// ParseStats takes a sequence of bytes destined for a Statsd server and parses
// it out into one or more Stat structs. Each struct includes both the raw
// bytes (copied, so the src []byte may be reused if desired) as well as each
// component it was able to parse out. If parsing was incomplete Stat.Parsed
// will be set to false but no error is returned / kept.
func ParseStats(src []byte) Stats {
d := make([]byte, len(src))
for i, b := range src {
d[i] = b
}
// standard protocol indicates one stat per line
entries := bytes.Split(d, []byte{'\n'})
result := make(Stats, len(entries))
for i, e := range entries {
result[i] = Stat{Raw: e}
ss := &result[i]
// : deliniates the stat name from the stat data
marker := bytes.IndexByte(e, ':')
if marker == -1 {
continue
}
ss.Stat = string(e[0:marker])
// stat data folows ':' with the form {value}|{type tag}[|@{sample rate}]
e = e[marker+1:]
marker = bytes.IndexByte(e, '|')
if marker == -1 {
continue
}
ss.Value = string(e[:marker])
e = e[marker+1:]
marker = bytes.IndexByte(e, '|')
if marker == -1 {
// no sample rate
ss.Tag = string(e)
} else {
ss.Tag = string(e[:marker])
e = e[marker+1:]
if len(e) == 0 || e[0] != '@' {
// sample rate should be prefixed with '@'; bail otherwise
continue
}
ss.Rate = string(e[1:])
}
ss.Parsed = true
}
return result
}
// Stats is a slice of Stat
type Stats []Stat
// Unparsed returns any stats that were unable to be completely parsed.
func (s Stats) Unparsed() Stats {
var r Stats
for _, e := range s {
if !e.Parsed {
r = append(r, e)
}
}
return r
}
// CollectNamed returns all data sent for a given stat name.
func (s Stats) CollectNamed(statName string) Stats {
return s.Collect(func(e Stat) bool {
return e.Stat == statName
})
}
// Collect gathers all stats that make some predicate true.
func (s Stats) Collect(pred func(Stat) bool) Stats {
var r Stats
for _, e := range s {
if pred(e) {
r = append(r, e)
}
}
return r
}
// Values returns the values associated with this Stats object.
func (s Stats) Values() []string {
if len(s) == 0 {
return nil
}
r := make([]string, len(s))
for i, e := range s {
r[i] = e.Value
}
return r
}
// String fulfils the stringer interface
func (s Stats) String() string {
if len(s) == 0 {
return ""
}
r := make([]string, len(s))
for i, e := range s {
r[i] = e.String()
}
return strings.Join(r, "\n")
}

View File

@@ -0,0 +1,154 @@
package statsdtest
import (
"bytes"
"reflect"
"testing"
)
type parsingTestCase struct {
name string
sent [][]byte
expected Stats
}
var (
badStatNameOnly = []byte("foo.bar.baz:")
bsnoStat = Stat{
Raw: badStatNameOnly,
Stat: "foo.bar.baz",
Parsed: false,
}
gaugeWithoutRate = []byte("foo.bar.baz:1.000|g")
gworStat = Stat{
Raw: gaugeWithoutRate,
Stat: "foo.bar.baz",
Value: "1.000",
Tag: "g",
Parsed: true,
}
counterWithRate = []byte("foo.bar.baz:1.000|c|@0.75")
cwrStat = Stat{
Raw: counterWithRate,
Stat: "foo.bar.baz",
Value: "1.000",
Tag: "c",
Rate: "0.75",
Parsed: true,
}
stringStat = []byte(":some string value|s")
sStat = Stat{
Raw: stringStat,
Stat: "",
Value: "some string value",
Tag: "s",
Parsed: true,
}
badValue = []byte("asoentuh")
bvStat = Stat{Raw: badValue}
testCases = []parsingTestCase{
{name: "no stat data",
sent: [][]byte{badStatNameOnly},
expected: Stats{bsnoStat}},
{name: "trivial case",
sent: [][]byte{gaugeWithoutRate},
expected: Stats{gworStat}},
{name: "multiple simple",
sent: [][]byte{gaugeWithoutRate, counterWithRate},
expected: Stats{gworStat, cwrStat}},
{name: "mixed good and bad",
sent: [][]byte{badValue, badValue, stringStat, badValue, counterWithRate, badValue},
expected: Stats{bvStat, bvStat, sStat, bvStat, cwrStat, bvStat}},
}
)
func TestParseBytes(t *testing.T) {
for _, tc := range testCases {
got := ParseStats(bytes.Join(tc.sent, []byte("\n")))
want := tc.expected
if !reflect.DeepEqual(got, want) {
t.Errorf("%s: got: %+v, want: %+v", tc.name, got, want)
}
}
}
func TestStatsUnparsed(t *testing.T) {
start := Stats{bsnoStat, gworStat, bsnoStat, bsnoStat, cwrStat}
got := start.Unparsed()
want := Stats{bsnoStat, bsnoStat, bsnoStat}
if !reflect.DeepEqual(got, want) {
t.Errorf("got: %+v, want: %+v", got, want)
}
}
func TestStatsCollectNamed(t *testing.T) {
type test struct {
name string
start Stats
want Stats
matchOn string
}
cases := []test{
{"No matches",
Stats{bsnoStat, cwrStat},
nil,
"foo"},
{"One match",
Stats{bsnoStat, Stat{Stat: "foo"}, cwrStat},
Stats{Stat{Stat: "foo"}},
"foo"},
{"Two matches",
Stats{bsnoStat, Stat{Stat: "foo"}, cwrStat},
Stats{bsnoStat, cwrStat},
"foo.bar.baz"},
}
for _, c := range cases {
got := c.start.CollectNamed(c.matchOn)
if !reflect.DeepEqual(got, c.want) {
t.Errorf("%s: got: %+v, want: %+v", c.name, got, c.want)
}
}
}
func TestStatsCollect(t *testing.T) {
type test struct {
name string
start Stats
want Stats
pred func(Stat) bool
}
cases := []test{
{"Not called",
Stats{},
nil,
func(_ Stat) bool { t.Errorf("should not be called"); return true }},
{"Matches value = 1.000",
Stats{bsnoStat, gworStat, cwrStat, sStat, bsnoStat},
Stats{gworStat, cwrStat},
func(s Stat) bool { return s.Value == "1.000" }},
}
for _, c := range cases {
got := c.start.Collect(c.pred)
if !reflect.DeepEqual(got, c.want) {
t.Errorf("%s: got: %+v, want: %+v", c.name, got, c.want)
}
}
}
func TestStatsValues(t *testing.T) {
start := Stats{bsnoStat, sStat, gworStat}
got := start.Values()
want := []string{bsnoStat.Value, sStat.Value, gworStat.Value}
if !reflect.DeepEqual(got, want) {
t.Errorf("got: %+v, want: %+v", got, want)
}
}