relay/relaytest/mock_stats.go (142 lines of code) (raw):

// Copyright (c) 2015 Uber Technologies, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package relaytest import ( "fmt" "sort" "strings" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uber/tchannel-go/relay" ) // MockCallStats is a testing spy for the CallStats interface. type MockCallStats struct { // Store ints and slices instead of bools and strings so that we can assert // the actual sequence of calls (in case we expect to call both Succeeded // and Failed). The real implementation will have the first writer win. succeeded int failedMsgs []string ended int sent int received int wg *sync.WaitGroup } // Succeeded marks the RPC as succeeded. func (m *MockCallStats) Succeeded() { m.succeeded++ } // Failed marks the RPC as failed for the provided reason. func (m *MockCallStats) Failed(reason string) { m.failedMsgs = append(m.failedMsgs, reason) } // SentBytes tracks the sent bytes. func (m *MockCallStats) SentBytes(size uint16) { m.sent += int(size) } // ReceivedBytes tracks the received bytes. func (m *MockCallStats) ReceivedBytes(size uint16) { m.received += int(size) } // End halts timer and metric collection for the RPC. func (m *MockCallStats) End() { m.ended++ m.wg.Done() } // FluentMockCallStats wraps the MockCallStats in a fluent API that's convenient for tests. type FluentMockCallStats struct { *MockCallStats } // Succeeded marks the RPC as succeeded. func (f *FluentMockCallStats) Succeeded() *FluentMockCallStats { f.MockCallStats.Succeeded() return f } // Failed marks the RPC as failed. func (f *FluentMockCallStats) Failed(reason string) *FluentMockCallStats { f.MockCallStats.Failed(reason) return f } // MockStats is a testing spy for the Stats interface. type MockStats struct { mu sync.Mutex wg sync.WaitGroup stats map[string][]*MockCallStats } // NewMockStats constructs a MockStats. func NewMockStats() *MockStats { return &MockStats{ stats: make(map[string][]*MockCallStats), } } // Begin starts collecting metrics for an RPC. func (m *MockStats) Begin(f relay.CallFrame) *MockCallStats { return m.Add(string(f.Caller()), string(f.Service()), string(f.Method())).MockCallStats } // Add explicitly adds a new call along an edge of the call graph. func (m *MockStats) Add(caller, callee, procedure string) *FluentMockCallStats { m.wg.Add(1) cs := &MockCallStats{wg: &m.wg} key := m.tripleToKey(caller, callee, procedure) m.mu.Lock() m.stats[key] = append(m.stats[key], cs) m.mu.Unlock() return &FluentMockCallStats{cs} } // AssertEqual asserts that two MockStats describe the same call graph. func (m *MockStats) AssertEqual(t testing.TB, expected *MockStats) { m.WaitForEnd() m.mu.Lock() defer m.mu.Unlock() expected.mu.Lock() defer expected.mu.Unlock() if assert.Equal(t, getEdges(expected.stats), getEdges(m.stats), "Found calls along unexpected edges.") { for edge := range expected.stats { m.assertEdgeEqual(t, expected, edge) } } } // WaitForEnd waits for all calls to End. func (m *MockStats) WaitForEnd() { m.wg.Wait() } func (m *MockStats) assertEdgeEqual(t testing.TB, expected *MockStats, edge string) { expectedCalls := expected.stats[edge] actualCalls := m.stats[edge] if assert.Equal(t, len(expectedCalls), len(actualCalls), "Unexpected number of calls along %s edge.", edge) { for i := range expectedCalls { m.assertCallEqual(t, expectedCalls[i], actualCalls[i]) } } } func (m *MockStats) assertCallEqual(t testing.TB, expected *MockCallStats, actual *MockCallStats) { // Revisit these assertions if we ever need to assert zero or many calls to // End. require.Equal(t, 1, expected.ended, "Expected call must assert exactly one call to End.") require.False( t, expected.succeeded <= 0 && len(expected.failedMsgs) == 0, "Expectation must indicate whether RPC should succeed or fail.", ) failed := !assert.Equal(t, expected.succeeded, actual.succeeded, "Unexpected number of successes.") failed = !assert.Equal(t, expected.failedMsgs, actual.failedMsgs, "Unexpected reasons for RPC failure.") || failed failed = !assert.Equal(t, expected.ended, actual.ended, "Unexpected number of calls to End.") || failed if failed { // The default testify output is often insufficient. t.Logf("\nExpected relayed stats were:\n\t%+v\nActual relayed stats were:\n\t%+v\n", expected, actual) } } func (m *MockStats) tripleToKey(caller, callee, procedure string) string { return fmt.Sprintf("%s->%s::%s", caller, callee, procedure) } func getEdges(m map[string][]*MockCallStats) []string { edges := make([]string, 0, len(m)) for k := range m { edges = append(edges, k) } sort.Strings(edges) return edges } // Map returns all stats as a map of key to int. // It waits for any ongoing calls to end first to avoid races. func (m *MockStats) Map() map[string]int { m.WaitForEnd() m.mu.Lock() defer m.mu.Unlock() stats := make(map[string]int) for k, calls := range m.stats { for _, call := range calls { name := k stats[name+".calls"]++ if call.ended > 0 { stats[name+".ended"]++ } if call.succeeded > 0 { stats[name+".succeeded"]++ } if len(call.failedMsgs) > 0 { failureName := name + ".failed-" + strings.Join(call.failedMsgs, ",") stats[failureName]++ } stats[name+".sent-bytes"] = call.sent stats[name+".received-bytes"] = call.received } } return stats }