package internal import ( "fmt" "runtime" "time" ) var ( // Unfortunately, the resolution of time.Now() on Windows is coarse: Two // sequential calls to time.Now() may return the same value, and tests // which expect non-zero durations may fail. To avoid adding sleep // statements or mocking time.Now(), those tests are skipped on Windows. doDurationTests = runtime.GOOS != `windows` ) // Validator is used for testing. type Validator interface { Error(...interface{}) } func validateStringField(v Validator, fieldName, v1, v2 string) { if v1 != v2 { v.Error(fieldName, v1, v2) } } type addValidatorField struct { field interface{} original Validator } func (a addValidatorField) Error(fields ...interface{}) { fields = append([]interface{}{a.field}, fields...) a.original.Error(fields...) } // ExtendValidator is used to add more context to a validator. func ExtendValidator(v Validator, field interface{}) Validator { return addValidatorField{ field: field, original: v, } } // WantMetric is a metric expectation. If Data is nil, then any data values are // acceptable. type WantMetric struct { Name string Scope string Forced interface{} // true, false, or nil Data []float64 } // WantCustomEvent is a custom event expectation. type WantCustomEvent struct { Type string Params map[string]interface{} } // WantError is a traced error expectation. type WantError struct { TxnName string Msg string Klass string Caller string URL string UserAttributes map[string]interface{} AgentAttributes map[string]interface{} } // WantErrorEvent is an error event expectation. type WantErrorEvent struct { TxnName string Msg string Klass string Queuing bool ExternalCallCount uint64 DatastoreCallCount uint64 UserAttributes map[string]interface{} AgentAttributes map[string]interface{} } // WantTxnEvent is a transaction event expectation. type WantTxnEvent struct { Name string Zone string Queuing bool ExternalCallCount uint64 DatastoreCallCount uint64 UserAttributes map[string]interface{} AgentAttributes map[string]interface{} } // WantTxnTrace is a transaction trace expectation. type WantTxnTrace struct { MetricName string CleanURL string NumSegments int UserAttributes map[string]interface{} AgentAttributes map[string]interface{} } // WantSlowQuery is a slowQuery expectation. type WantSlowQuery struct { Count int32 MetricName string Query string TxnName string TxnURL string DatabaseName string Host string PortPathOrID string Params map[string]interface{} } // Expect exposes methods that allow for testing whether the correct data was // captured. type Expect interface { ExpectCustomEvents(t Validator, want []WantCustomEvent) ExpectErrors(t Validator, want []WantError) ExpectErrorEvents(t Validator, want []WantErrorEvent) ExpectTxnEvents(t Validator, want []WantTxnEvent) ExpectMetrics(t Validator, want []WantMetric) ExpectTxnTraces(t Validator, want []WantTxnTrace) ExpectSlowQueries(t Validator, want []WantSlowQuery) } func expectMetricField(t Validator, id metricID, v1, v2 float64, fieldName string) { if v1 != v2 { t.Error("metric fields do not match", id, v1, v2, fieldName) } } // ExpectMetrics allows testing of metrics. func ExpectMetrics(t Validator, mt *metricTable, expect []WantMetric) { if len(mt.metrics) != len(expect) { t.Error("metric counts do not match expectations", len(mt.metrics), len(expect)) } expectedIds := make(map[metricID]struct{}) for _, e := range expect { id := metricID{Name: e.Name, Scope: e.Scope} expectedIds[id] = struct{}{} m := mt.metrics[id] if nil == m { t.Error("unable to find metric", id) continue } if b, ok := e.Forced.(bool); ok { if b != (forced == m.forced) { t.Error("metric forced incorrect", b, m.forced, id) } } if nil != e.Data { expectMetricField(t, id, e.Data[0], m.data.countSatisfied, "countSatisfied") expectMetricField(t, id, e.Data[1], m.data.totalTolerated, "totalTolerated") expectMetricField(t, id, e.Data[2], m.data.exclusiveFailed, "exclusiveFailed") expectMetricField(t, id, e.Data[3], m.data.min, "min") expectMetricField(t, id, e.Data[4], m.data.max, "max") expectMetricField(t, id, e.Data[5], m.data.sumSquares, "sumSquares") } } for id := range mt.metrics { if _, ok := expectedIds[id]; !ok { t.Error("expected metrics does not contain", id.Name, id.Scope) } } } func expectAttributes(v Validator, exists map[string]interface{}, expect map[string]interface{}) { // TODO: This params comparison can be made smarter: Alert differences // based on sub/super set behavior. if len(exists) != len(expect) { v.Error("attributes length difference", exists, expect) return } for key, val := range expect { found, ok := exists[key] if !ok { v.Error("missing key", key) continue } v1 := fmt.Sprint(found) v2 := fmt.Sprint(val) if v1 != v2 { v.Error("value difference", fmt.Sprintf("key=%s", key), v1, v2) } } } func expectCustomEvent(v Validator, event *CustomEvent, expect WantCustomEvent) { if event.eventType != expect.Type { v.Error("type mismatch", event.eventType, expect.Type) } now := time.Now() diff := absTimeDiff(now, event.timestamp) if diff > time.Hour { v.Error("large timestamp difference", event.eventType, now, event.timestamp) } expectAttributes(v, event.truncatedParams, expect.Params) } // ExpectCustomEvents allows testing of custom events. func ExpectCustomEvents(v Validator, cs *customEvents, expect []WantCustomEvent) { if len(cs.events.events) != len(expect) { v.Error("number of custom events does not match", len(cs.events.events), len(expect)) return } for i, e := range expect { event, ok := cs.events.events[i].jsonWriter.(*CustomEvent) if !ok { v.Error("wrong custom event") } else { expectCustomEvent(v, event, e) } } } func expectErrorEvent(v Validator, err *ErrorEvent, expect WantErrorEvent) { validateStringField(v, "txnName", expect.TxnName, err.TxnName) validateStringField(v, "klass", expect.Klass, err.Klass) validateStringField(v, "msg", expect.Msg, err.Msg) if (0 != err.Queuing) != expect.Queuing { v.Error("queuing", err.Queuing) } if nil != expect.UserAttributes { expectAttributes(v, getUserAttributes(err.Attrs, destError), expect.UserAttributes) } if nil != expect.AgentAttributes { expectAttributes(v, getAgentAttributes(err.Attrs, destError), expect.AgentAttributes) } if expect.ExternalCallCount != err.externalCallCount { v.Error("external call count", expect.ExternalCallCount, err.externalCallCount) } if doDurationTests && (0 == expect.ExternalCallCount) != (err.externalDuration == 0) { v.Error("external duration", err.externalDuration) } if expect.DatastoreCallCount != err.datastoreCallCount { v.Error("datastore call count", expect.DatastoreCallCount, err.datastoreCallCount) } if doDurationTests && (0 == expect.DatastoreCallCount) != (err.datastoreDuration == 0) { v.Error("datastore duration", err.datastoreDuration) } } // ExpectErrorEvents allows testing of error events. func ExpectErrorEvents(v Validator, events *errorEvents, expect []WantErrorEvent) { if len(events.events.events) != len(expect) { v.Error("number of custom events does not match", len(events.events.events), len(expect)) return } for i, e := range expect { event, ok := events.events.events[i].jsonWriter.(*ErrorEvent) if !ok { v.Error("wrong error event") } else { expectErrorEvent(v, event, e) } } } func expectTxnEvent(v Validator, e *TxnEvent, expect WantTxnEvent) { validateStringField(v, "apdex zone", expect.Zone, e.Zone.label()) validateStringField(v, "name", expect.Name, e.Name) if doDurationTests && 0 == e.Duration { v.Error("zero duration", e.Duration) } if (0 != e.Queuing) != expect.Queuing { v.Error("queuing", e.Queuing) } if nil != expect.UserAttributes { expectAttributes(v, getUserAttributes(e.Attrs, destTxnEvent), expect.UserAttributes) } if nil != expect.AgentAttributes { expectAttributes(v, getAgentAttributes(e.Attrs, destTxnEvent), expect.AgentAttributes) } if expect.ExternalCallCount != e.externalCallCount { v.Error("external call count", expect.ExternalCallCount, e.externalCallCount) } if doDurationTests && (0 == expect.ExternalCallCount) != (e.externalDuration == 0) { v.Error("external duration", e.externalDuration) } if expect.DatastoreCallCount != e.datastoreCallCount { v.Error("datastore call count", expect.DatastoreCallCount, e.datastoreCallCount) } if doDurationTests && (0 == expect.DatastoreCallCount) != (e.datastoreDuration == 0) { v.Error("datastore duration", e.datastoreDuration) } } // ExpectTxnEvents allows testing of txn events. func ExpectTxnEvents(v Validator, events *txnEvents, expect []WantTxnEvent) { if len(events.events.events) != len(expect) { v.Error("number of txn events does not match", len(events.events.events), len(expect)) return } for i, e := range expect { event, ok := events.events.events[i].jsonWriter.(*TxnEvent) if !ok { v.Error("wrong txn event") } else { expectTxnEvent(v, event, e) } } } func expectError(v Validator, err *harvestError, expect WantError) { caller := topCallerNameBase(err.TxnError.Stack) validateStringField(v, "caller", expect.Caller, caller) validateStringField(v, "txnName", expect.TxnName, err.txnName) validateStringField(v, "klass", expect.Klass, err.TxnError.Klass) validateStringField(v, "msg", expect.Msg, err.TxnError.Msg) validateStringField(v, "URL", expect.URL, err.requestURI) if nil != expect.UserAttributes { expectAttributes(v, getUserAttributes(err.attrs, destError), expect.UserAttributes) } if nil != expect.AgentAttributes { expectAttributes(v, getAgentAttributes(err.attrs, destError), expect.AgentAttributes) } } // ExpectErrors allows testing of errors. func ExpectErrors(v Validator, errors *harvestErrors, expect []WantError) { if len(errors.errors) != len(expect) { v.Error("number of errors mismatch", len(errors.errors), len(expect)) return } for i, e := range expect { expectError(v, errors.errors[i], e) } } func expectTxnTrace(v Validator, trace *HarvestTrace, expect WantTxnTrace) { if doDurationTests && 0 == trace.Duration { v.Error("zero trace duration") } validateStringField(v, "metric name", expect.MetricName, trace.MetricName) validateStringField(v, "request url", expect.CleanURL, trace.CleanURL) if nil != expect.UserAttributes { expectAttributes(v, getUserAttributes(trace.Attrs, destTxnTrace), expect.UserAttributes) } if nil != expect.AgentAttributes { expectAttributes(v, getAgentAttributes(trace.Attrs, destTxnTrace), expect.AgentAttributes) } if expect.NumSegments != len(trace.Trace.nodes) { v.Error("wrong number of segments", expect.NumSegments, len(trace.Trace.nodes)) } } // ExpectTxnTraces allows testing of transaction traces. func ExpectTxnTraces(v Validator, traces *harvestTraces, want []WantTxnTrace) { if len(want) == 0 { if nil != traces.trace { v.Error("trace exists when not expected") } } else if len(want) > 1 { v.Error("too many traces expected") } else { if nil == traces.trace { v.Error("missing expected trace") } else { expectTxnTrace(v, traces.trace, want[0]) } } } func expectSlowQuery(t Validator, slowQuery *slowQuery, want WantSlowQuery) { if slowQuery.Count != want.Count { t.Error("wrong Count field", slowQuery.Count, want.Count) } validateStringField(t, "MetricName", slowQuery.DatastoreMetric, want.MetricName) validateStringField(t, "Query", slowQuery.ParameterizedQuery, want.Query) validateStringField(t, "TxnName", slowQuery.TxnName, want.TxnName) validateStringField(t, "TxnURL", slowQuery.TxnURL, want.TxnURL) validateStringField(t, "DatabaseName", slowQuery.DatabaseName, want.DatabaseName) validateStringField(t, "Host", slowQuery.Host, want.Host) validateStringField(t, "PortPathOrID", slowQuery.PortPathOrID, want.PortPathOrID) expectAttributes(t, map[string]interface{}(slowQuery.QueryParameters), want.Params) } // ExpectSlowQueries allows testing of slow queries. func ExpectSlowQueries(t Validator, slowQueries *slowQueries, want []WantSlowQuery) { if len(want) != len(slowQueries.priorityQueue) { t.Error("wrong number of slow queries", "expected", len(want), "got", len(slowQueries.priorityQueue)) return } for _, s := range want { idx, ok := slowQueries.lookup[s.Query] if !ok { t.Error("unable to find slow query", s.Query) continue } expectSlowQuery(t, slowQueries.priorityQueue[idx], s) } }