diff --git a/sdk/metric/metricdata/metricdatatest/assertion.go b/sdk/metric/metricdata/metricdatatest/assertion.go new file mode 100644 index 00000000000..9d32886904d --- /dev/null +++ b/sdk/metric/metricdata/metricdatatest/assertion.go @@ -0,0 +1,98 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.18 +// +build go1.18 + +// Package metricdatatest provides testing functionality for use with the +// metricdata package. +package metricdatatest // import "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + +import ( + "fmt" + "testing" + + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +// Datatypes are the concrete data-types the metricdata package provides. +type Datatypes interface { + metricdata.DataPoint | metricdata.Float64 | metricdata.Gauge | metricdata.Histogram | metricdata.HistogramDataPoint | metricdata.Int64 | metricdata.Metrics | metricdata.ResourceMetrics | metricdata.ScopeMetrics | metricdata.Sum + + // Interface types are not allowed in union types, therefore the + // Aggregation and Value type from metricdata are not included here. +} + +// AssertEqual asserts that the two concrete data-types from the metricdata +// package are equal. +func AssertEqual[T Datatypes](t *testing.T, expected, actual T) bool { + t.Helper() + + // Generic types cannot be type asserted. Use an interface instead. + aIface := interface{}(actual) + + var r []string + switch e := interface{}(expected).(type) { + case metricdata.DataPoint: + r = equalDataPoints(e, aIface.(metricdata.DataPoint)) + case metricdata.Float64: + r = equalFloat64(e, aIface.(metricdata.Float64)) + case metricdata.Gauge: + r = equalGauges(e, aIface.(metricdata.Gauge)) + case metricdata.Histogram: + r = equalHistograms(e, aIface.(metricdata.Histogram)) + case metricdata.HistogramDataPoint: + r = equalHistogramDataPoints(e, aIface.(metricdata.HistogramDataPoint)) + case metricdata.Int64: + r = equalInt64(e, aIface.(metricdata.Int64)) + case metricdata.Metrics: + r = equalMetrics(e, aIface.(metricdata.Metrics)) + case metricdata.ResourceMetrics: + r = equalResourceMetrics(e, aIface.(metricdata.ResourceMetrics)) + case metricdata.ScopeMetrics: + r = equalScopeMetrics(e, aIface.(metricdata.ScopeMetrics)) + case metricdata.Sum: + r = equalSums(e, aIface.(metricdata.Sum)) + default: + // We control all types passed to this, panic to signal developers + // early they changed things in an incompatible way. + panic(fmt.Sprintf("unknown types: %T", expected)) + } + + if len(r) > 0 { + t.Error(r) + return false + } + return true +} + +// AssertAggregationsEqual asserts that two Aggregations are equal. +func AssertAggregationsEqual(t *testing.T, expected, actual metricdata.Aggregation) bool { + t.Helper() + if r := equalAggregations(expected, actual); len(r) > 0 { + t.Error(r) + return false + } + return true +} + +// AssertValuesEqual asserts that two Values are equal. +func AssertValuesEqual(t *testing.T, expected, actual metricdata.Value) bool { + t.Helper() + if r := equalValues(expected, actual); len(r) > 0 { + t.Error(r) + return false + } + return true +} diff --git a/sdk/metric/metricdata/metricdatatest/assertion_fail_test.go b/sdk/metric/metricdata/metricdatatest/assertion_fail_test.go new file mode 100644 index 00000000000..228bfbddd8c --- /dev/null +++ b/sdk/metric/metricdata/metricdatatest/assertion_fail_test.go @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.18 && tests_fail +// +build go1.18,tests_fail + +package metricdatatest // import "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + +import ( + "testing" +) + +// These tests are used to develop the failure messages of this package's +// assertions. They can be run with the following. +// +// go test -tags tests_fail ./... + +func testFailDatatype[T Datatypes](a, b T) func(*testing.T) { + return func(t *testing.T) { + AssertEqual(t, a, b) + } +} + +func TestFailAssertEqual(t *testing.T) { + t.Run("ResourceMetrics", testFailDatatype(resourceMetricsA, resourceMetricsB)) + t.Run("ScopeMetrics", testFailDatatype(scopeMetricsA, scopeMetricsB)) + t.Run("Metrics", testFailDatatype(metricsA, metricsB)) + t.Run("Histogram", testFailDatatype(histogramA, histogramB)) + t.Run("Sum", testFailDatatype(sumA, sumB)) + t.Run("Gauge", testFailDatatype(gaugeA, gaugeB)) + t.Run("HistogramDataPoint", testFailDatatype(histogramDataPointA, histogramDataPointB)) + t.Run("DataPoint", testFailDatatype(dataPointsA, dataPointsB)) + t.Run("Int64", testFailDatatype(int64A, int64B)) + t.Run("Float64", testFailDatatype(float64A, float64B)) +} + +func TestFailAssertAggregationsEqual(t *testing.T) { + AssertAggregationsEqual(t, sumA, nil) + AssertAggregationsEqual(t, sumA, gaugeA) + AssertAggregationsEqual(t, unknownAggregation{}, unknownAggregation{}) + AssertAggregationsEqual(t, sumA, sumB) + AssertAggregationsEqual(t, gaugeA, gaugeB) + AssertAggregationsEqual(t, histogramA, histogramB) +} + +func TestFailAssertValuesEqual(t *testing.T) { + AssertValuesEqual(t, int64A, nil) + AssertValuesEqual(t, int64A, float64A) + AssertValuesEqual(t, unknownValue{}, unknownValue{}) + AssertValuesEqual(t, int64A, int64B) + AssertValuesEqual(t, float64A, float64B) +} diff --git a/sdk/metric/metricdata/metricdatatest/assertion_test.go b/sdk/metric/metricdata/metricdatatest/assertion_test.go new file mode 100644 index 00000000000..79d639ba2c3 --- /dev/null +++ b/sdk/metric/metricdata/metricdatatest/assertion_test.go @@ -0,0 +1,215 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.18 +// +build go1.18 + +package metricdatatest // import "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric/unit" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/resource" +) + +var ( + attrA = attribute.NewSet(attribute.Bool("A", true)) + attrB = attribute.NewSet(attribute.Bool("B", true)) + + float64A = metricdata.Float64(-1.0) + float64B = metricdata.Float64(2.0) + + int64A = metricdata.Int64(-1) + int64B = metricdata.Int64(2) + + startA = time.Now() + startB = startA.Add(time.Millisecond) + endA = startA.Add(time.Second) + endB = startB.Add(time.Second) + + dataPointsA = metricdata.DataPoint{ + Attributes: attrA, + StartTime: startA, + Time: endA, + Value: int64A, + } + dataPointsB = metricdata.DataPoint{ + Attributes: attrB, + StartTime: startB, + Time: endB, + Value: float64B, + } + + max, min = 99.0, 3. + histogramDataPointA = metricdata.HistogramDataPoint{ + Attributes: attrA, + StartTime: startA, + Time: endA, + Count: 2, + Bounds: []float64{0, 10}, + BucketCounts: []uint64{1, 1}, + Sum: 2, + } + histogramDataPointB = metricdata.HistogramDataPoint{ + Attributes: attrB, + StartTime: startB, + Time: endB, + Count: 3, + Bounds: []float64{0, 10, 100}, + BucketCounts: []uint64{1, 1, 1}, + Max: &max, + Min: &min, + Sum: 3, + } + + gaugeA = metricdata.Gauge{DataPoints: []metricdata.DataPoint{dataPointsA}} + gaugeB = metricdata.Gauge{DataPoints: []metricdata.DataPoint{dataPointsB}} + + sumA = metricdata.Sum{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint{dataPointsA}, + } + sumB = metricdata.Sum{ + Temporality: metricdata.DeltaTemporality, + IsMonotonic: false, + DataPoints: []metricdata.DataPoint{dataPointsB}, + } + + histogramA = metricdata.Histogram{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint{histogramDataPointA}, + } + histogramB = metricdata.Histogram{ + Temporality: metricdata.DeltaTemporality, + DataPoints: []metricdata.HistogramDataPoint{histogramDataPointB}, + } + + metricsA = metricdata.Metrics{ + Name: "A", + Description: "A desc", + Unit: unit.Dimensionless, + Data: sumA, + } + metricsB = metricdata.Metrics{ + Name: "B", + Description: "B desc", + Unit: unit.Bytes, + Data: gaugeB, + } + + scopeMetricsA = metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{Name: "A"}, + Metrics: []metricdata.Metrics{metricsA}, + } + scopeMetricsB = metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{Name: "B"}, + Metrics: []metricdata.Metrics{metricsB}, + } + + resourceMetricsA = metricdata.ResourceMetrics{ + Resource: resource.NewSchemaless(attribute.String("resource", "A")), + ScopeMetrics: []metricdata.ScopeMetrics{scopeMetricsA}, + } + resourceMetricsB = metricdata.ResourceMetrics{ + Resource: resource.NewSchemaless(attribute.String("resource", "B")), + ScopeMetrics: []metricdata.ScopeMetrics{scopeMetricsB}, + } +) + +type equalFunc[T Datatypes] func(T, T) []string + +func testDatatype[T Datatypes](a, b T, f equalFunc[T]) func(*testing.T) { + return func(t *testing.T) { + AssertEqual(t, a, a) + AssertEqual(t, b, b) + + r := f(a, b) + assert.Greaterf(t, len(r), 0, "%v == %v", a, b) + } +} + +func TestAssertEqual(t *testing.T) { + t.Run("ResourceMetrics", testDatatype(resourceMetricsA, resourceMetricsB, equalResourceMetrics)) + t.Run("ScopeMetrics", testDatatype(scopeMetricsA, scopeMetricsB, equalScopeMetrics)) + t.Run("Metrics", testDatatype(metricsA, metricsB, equalMetrics)) + t.Run("Histogram", testDatatype(histogramA, histogramB, equalHistograms)) + t.Run("Sum", testDatatype(sumA, sumB, equalSums)) + t.Run("Gauge", testDatatype(gaugeA, gaugeB, equalGauges)) + t.Run("HistogramDataPoint", testDatatype(histogramDataPointA, histogramDataPointB, equalHistogramDataPoints)) + t.Run("DataPoint", testDatatype(dataPointsA, dataPointsB, equalDataPoints)) + t.Run("Int64", testDatatype(int64A, int64B, equalInt64)) + t.Run("Float64", testDatatype(float64A, float64B, equalFloat64)) +} + +type unknownAggregation struct { + metricdata.Aggregation +} + +func TestAssertAggregationsEqual(t *testing.T) { + AssertAggregationsEqual(t, nil, nil) + AssertAggregationsEqual(t, sumA, sumA) + AssertAggregationsEqual(t, gaugeA, gaugeA) + AssertAggregationsEqual(t, histogramA, histogramA) + + r := equalAggregations(sumA, nil) + assert.Len(t, r, 1, "should return nil comparison mismatch only") + + r = equalAggregations(sumA, gaugeA) + assert.Len(t, r, 1, "should return with type mismatch only") + + r = equalAggregations(unknownAggregation{}, unknownAggregation{}) + assert.Len(t, r, 1, "should return with unknown aggregation only") + + r = equalAggregations(sumA, sumB) + assert.Greaterf(t, len(r), 0, "%v == %v", sumA, sumB) + + r = equalAggregations(gaugeA, gaugeB) + assert.Greaterf(t, len(r), 0, "%v == %v", gaugeA, gaugeB) + + r = equalAggregations(histogramA, histogramB) + assert.Greaterf(t, len(r), 0, "%v == %v", histogramA, histogramB) +} + +type unknownValue struct { + metricdata.Value +} + +func TestAssertValuesEqual(t *testing.T) { + AssertValuesEqual(t, nil, nil) + AssertValuesEqual(t, int64A, int64A) + AssertValuesEqual(t, float64A, float64A) + + r := equalValues(int64A, nil) + assert.Len(t, r, 1, "should return nil comparison mismatch only") + + r = equalValues(int64A, float64A) + assert.Len(t, r, 1, "should return with type mismatch only") + + r = equalValues(unknownValue{}, unknownValue{}) + assert.Len(t, r, 1, "should return with unknown value only") + + r = equalValues(int64A, int64B) + assert.Greaterf(t, len(r), 0, "%v == %v", int64A, int64B) + + r = equalValues(float64A, float64B) + assert.Greaterf(t, len(r), 0, "%v == %v", float64A, float64B) +} diff --git a/sdk/metric/metricdata/metricdatatest/comparisons.go b/sdk/metric/metricdata/metricdatatest/comparisons.go new file mode 100644 index 00000000000..78e36169461 --- /dev/null +++ b/sdk/metric/metricdata/metricdatatest/comparisons.go @@ -0,0 +1,400 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.18 +// +build go1.18 + +package metricdatatest // import "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + +import ( + "bytes" + "fmt" + "reflect" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +// equalResourceMetrics returns reasons ResourceMetrics are not equal. If they +// are equal, the returned reasons will be empty. +// +// The ScopeMetrics each ResourceMetrics contains are compared based on +// containing the same ScopeMetrics, not the order they are stored in. +func equalResourceMetrics(a, b metricdata.ResourceMetrics) (reasons []string) { + if !a.Resource.Equal(b.Resource) { + reasons = append(reasons, notEqualStr("Resources", a.Resource, b.Resource)) + } + + r := compareDiff(diffSlices( + a.ScopeMetrics, + b.ScopeMetrics, + func(a, b metricdata.ScopeMetrics) bool { + r := equalScopeMetrics(a, b) + return len(r) == 0 + }, + )) + if r != "" { + reasons = append(reasons, fmt.Sprintf("ResourceMetrics ScopeMetrics not equal:\n%s", r)) + } + return reasons +} + +// equalScopeMetrics returns reasons ScopeMetrics are not equal. If they are +// equal, the returned reasons will be empty. +// +// The Metrics each ScopeMetrics contains are compared based on containing the +// same Metrics, not the order they are stored in. +func equalScopeMetrics(a, b metricdata.ScopeMetrics) (reasons []string) { + if a.Scope != b.Scope { + reasons = append(reasons, notEqualStr("Scope", a.Scope, b.Scope)) + } + + r := compareDiff(diffSlices( + a.Metrics, + b.Metrics, + func(a, b metricdata.Metrics) bool { + r := equalMetrics(a, b) + return len(r) == 0 + }, + )) + if r != "" { + reasons = append(reasons, fmt.Sprintf("ScopeMetrics Metrics not equal:\n%s", r)) + } + return reasons +} + +// equalMetrics returns reasons Metrics are not equal. If they are equal, the +// returned reasons will be empty. +func equalMetrics(a, b metricdata.Metrics) (reasons []string) { + if a.Name != b.Name { + reasons = append(reasons, notEqualStr("Name", a.Name, b.Name)) + } + if a.Description != b.Description { + reasons = append(reasons, notEqualStr("Description", a.Description, b.Description)) + } + if a.Unit != b.Unit { + reasons = append(reasons, notEqualStr("Unit", a.Unit, b.Unit)) + } + + r := equalAggregations(a.Data, b.Data) + if len(r) > 0 { + reasons = append(reasons, "Metrics Data not equal:") + reasons = append(reasons, r...) + } + return reasons +} + +// equalAggregations returns reasons a and b are not equal. If they are equal, +// the returned reasons will be empty. +func equalAggregations(a, b metricdata.Aggregation) (reasons []string) { + if a == nil || b == nil { + if a != b { + return []string{notEqualStr("Aggregation", a, b)} + } + return reasons + } + + if reflect.TypeOf(a) != reflect.TypeOf(b) { + return []string{fmt.Sprintf("Aggregation types not equal:\nexpected: %T\nactual: %T", a, b)} + } + + switch v := a.(type) { + case metricdata.Gauge: + r := equalGauges(v, b.(metricdata.Gauge)) + if len(r) > 0 { + reasons = append(reasons, "Gauge not equal:") + reasons = append(reasons, r...) + } + case metricdata.Sum: + r := equalSums(v, b.(metricdata.Sum)) + if len(r) > 0 { + reasons = append(reasons, "Sum not equal:") + reasons = append(reasons, r...) + } + case metricdata.Histogram: + r := equalHistograms(v, b.(metricdata.Histogram)) + if len(r) > 0 { + reasons = append(reasons, "Histogram not equal:") + reasons = append(reasons, r...) + } + default: + reasons = append(reasons, fmt.Sprintf("Aggregation of unknown types %T", a)) + } + return reasons +} + +// equalGauges returns reasons Gauges are not equal. If they are equal, the +// returned reasons will be empty. +// +// The DataPoints each Gauge contains are compared based on containing the +// same DataPoints, not the order they are stored in. +func equalGauges(a, b metricdata.Gauge) (reasons []string) { + r := compareDiff(diffSlices( + a.DataPoints, + b.DataPoints, + func(a, b metricdata.DataPoint) bool { + r := equalDataPoints(a, b) + return len(r) == 0 + }, + )) + if r != "" { + reasons = append(reasons, fmt.Sprintf("Gauge DataPoints not equal:\n%s", r)) + } + return reasons +} + +// equalSums returns reasons Sums are not equal. If they are equal, the +// returned reasons will be empty. +// +// The DataPoints each Sum contains are compared based on containing the same +// DataPoints, not the order they are stored in. +func equalSums(a, b metricdata.Sum) (reasons []string) { + if a.Temporality != b.Temporality { + reasons = append(reasons, notEqualStr("Temporality", a.Temporality, b.Temporality)) + } + if a.IsMonotonic != b.IsMonotonic { + reasons = append(reasons, notEqualStr("IsMonotonic", a.IsMonotonic, b.IsMonotonic)) + } + + r := compareDiff(diffSlices( + a.DataPoints, + b.DataPoints, + func(a, b metricdata.DataPoint) bool { + r := equalDataPoints(a, b) + return len(r) == 0 + }, + )) + if r != "" { + reasons = append(reasons, fmt.Sprintf("Sum DataPoints not equal:\n%s", r)) + } + return reasons +} + +// equalHistograms returns reasons Histograms are not equal. If they are +// equal, the returned reasons will be empty. +// +// The DataPoints each Histogram contains are compared based on containing the +// same HistogramDataPoint, not the order they are stored in. +func equalHistograms(a, b metricdata.Histogram) (reasons []string) { + if a.Temporality != b.Temporality { + reasons = append(reasons, notEqualStr("Temporality", a.Temporality, b.Temporality)) + } + + r := compareDiff(diffSlices( + a.DataPoints, + b.DataPoints, + func(a, b metricdata.HistogramDataPoint) bool { + r := equalHistogramDataPoints(a, b) + return len(r) == 0 + }, + )) + if r != "" { + reasons = append(reasons, fmt.Sprintf("Histogram DataPoints not equal:\n%s", r)) + } + return reasons +} + +// equalDataPoints returns reasons DataPoints are not equal. If they are +// equal, the returned reasons will be empty. +func equalDataPoints(a, b metricdata.DataPoint) (reasons []string) { + if !a.Attributes.Equals(&b.Attributes) { + reasons = append(reasons, notEqualStr( + "Attributes", + a.Attributes.Encoded(attribute.DefaultEncoder()), + b.Attributes.Encoded(attribute.DefaultEncoder()), + )) + } + if !a.StartTime.Equal(b.StartTime) { + reasons = append(reasons, notEqualStr("StartTime", a.StartTime.UnixNano(), b.StartTime.UnixNano())) + } + if !a.Time.Equal(b.Time) { + reasons = append(reasons, notEqualStr("Time", a.Time.UnixNano(), b.Time.UnixNano())) + } + + r := equalValues(a.Value, b.Value) + if len(r) > 0 { + reasons = append(reasons, "DataPoint Value not equal:") + reasons = append(reasons, r...) + } + return reasons +} + +// equalHistogramDataPoints returns reasons HistogramDataPoints are not equal. +// If they are equal, the returned reasons will be empty. +func equalHistogramDataPoints(a, b metricdata.HistogramDataPoint) (reasons []string) { + if !a.Attributes.Equals(&b.Attributes) { + reasons = append(reasons, notEqualStr( + "Attributes", + a.Attributes.Encoded(attribute.DefaultEncoder()), + b.Attributes.Encoded(attribute.DefaultEncoder()), + )) + } + if !a.StartTime.Equal(b.StartTime) { + reasons = append(reasons, notEqualStr("StartTime", a.StartTime.UnixNano(), b.StartTime.UnixNano())) + } + if !a.Time.Equal(b.Time) { + reasons = append(reasons, notEqualStr("Time", a.Time.UnixNano(), b.Time.UnixNano())) + } + if a.Count != b.Count { + reasons = append(reasons, notEqualStr("Count", a.Count, b.Count)) + } + if !equalSlices(a.Bounds, b.Bounds) { + reasons = append(reasons, notEqualStr("Bounds", a.Bounds, b.Bounds)) + } + if !equalSlices(a.BucketCounts, b.BucketCounts) { + reasons = append(reasons, notEqualStr("BucketCounts", a.BucketCounts, b.BucketCounts)) + } + if !equalPtrValues(a.Min, b.Min) { + reasons = append(reasons, notEqualStr("Min", a.Min, b.Min)) + } + if !equalPtrValues(a.Max, b.Max) { + reasons = append(reasons, notEqualStr("Max", a.Max, b.Max)) + } + if a.Sum != b.Sum { + reasons = append(reasons, notEqualStr("Sum", a.Sum, b.Sum)) + } + return reasons +} + +// equalValues returns reasons Values are not equal. If they are equal, the +// returned reasons will be empty. +func equalValues(a, b metricdata.Value) (reasons []string) { + if a == nil || b == nil { + if a != b { + return []string{notEqualStr("Values", a, b)} + } + return reasons + } + + if reflect.TypeOf(a) != reflect.TypeOf(b) { + return []string{fmt.Sprintf("Value types not equal:\nexpected: %T\nactual: %T", a, b)} + } + + switch v := a.(type) { + case metricdata.Int64: + r := equalInt64(v, b.(metricdata.Int64)) + if len(r) > 0 { + reasons = append(reasons, "Int64 not equal:") + reasons = append(reasons, r...) + } + case metricdata.Float64: + r := equalFloat64(v, b.(metricdata.Float64)) + if len(r) > 0 { + reasons = append(reasons, "Float64 not equal:") + reasons = append(reasons, r...) + } + default: + reasons = append(reasons, fmt.Sprintf("Value of unknown types %T", a)) + } + + return reasons +} + +// equalFloat64 returns reasons Float64s are not equal. If they are equal, the +// returned reasons will be empty. +func equalFloat64(a, b metricdata.Float64) (reasons []string) { + if a != b { + reasons = append(reasons, notEqualStr("Float64 value", a, b)) + } + return reasons +} + +// equalInt64 returns reasons Int64s are not equal. If they are equal, the +// returned reasons will be empty. +func equalInt64(a, b metricdata.Int64) (reasons []string) { + if a != b { + reasons = append(reasons, notEqualStr("Int64 value", a, b)) + } + return reasons +} + +func notEqualStr(prefix string, expected, actual interface{}) string { + return fmt.Sprintf("%s not equal:\nexpected: %v\nactual: %v", prefix, expected, actual) +} + +func equalSlices[T comparable](a, b []T) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +func equalPtrValues[T comparable](a, b *T) bool { + if a == nil || b == nil { + return a == b + } + + return *a == *b +} + +func diffSlices[T any](a, b []T, equal func(T, T) bool) (extraA, extraB []T) { + visited := make([]bool, len(b)) + for i := 0; i < len(a); i++ { + found := false + for j := 0; j < len(b); j++ { + if visited[j] { + continue + } + if equal(a[i], b[j]) { + visited[j] = true + found = true + break + } + } + if !found { + extraA = append(extraA, a[i]) + } + } + + for j := 0; j < len(b); j++ { + if visited[j] { + continue + } + extraB = append(extraB, b[j]) + } + + return extraA, extraB +} + +func compareDiff[T any](extraExpected, extraActual []T) string { + if len(extraExpected) == 0 && len(extraActual) == 0 { + return "" + } + + formater := func(v T) string { + return fmt.Sprintf("%#v", v) + } + + var msg bytes.Buffer + if len(extraExpected) > 0 { + _, _ = msg.WriteString("missing expected values:\n") + for _, v := range extraExpected { + _, _ = msg.WriteString(formater(v) + "\n") + } + } + + if len(extraActual) > 0 { + _, _ = msg.WriteString("unexpected additional values:\n") + for _, v := range extraActual { + _, _ = msg.WriteString(formater(v) + "\n") + } + } + + return msg.String() +}