diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ccf0dd62..cf4c3abed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,13 +18,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#6751) - Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#6752) - Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#6688) +- Add `AssertEqual` function in `go.opentelemetry.io/otel/log/logtest`. (#6662) ### Removed - Drop support for [Go 1.22]. (#6381, #6418) - Remove `Resource` field from `EnabledParameters` in `go.opentelemetry.io/otel/sdk/log`. (#6494) -- Remove `RecordFactory` type from `go.opentelemetry.io/otel/log/logtest`. (#6492) +- Remove `RecordFactory` type from `go.opentelemetry.io/otel/log/logtest`. (#6492) - Remove `ScopeRecords`, `EmittedRecord`, and `RecordFactory` types from `go.opentelemetry.io/otel/log/logtest`. (#6507) +- Remove `AssertRecordEqual` function in `go.opentelemetry.io/otel/log/logtest`, use `AssertEqual` instead. (#6662) ### Changed diff --git a/log/logtest/assert.go b/log/logtest/assert.go new file mode 100644 index 000000000..859c170d9 --- /dev/null +++ b/log/logtest/assert.go @@ -0,0 +1,53 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logtest // import "go.opentelemetry.io/otel/log/logtest" + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "go.opentelemetry.io/otel/log" +) + +// AssertEqual asserts that the two concrete data-types from the logtest package are equal. +func AssertEqual[T Recording | Record](t *testing.T, want, got T, opts ...AssertOption) bool { + t.Helper() + return assertEqual(t, want, got, opts...) +} + +// testingT reports failure messages. +// *testing.T implements this interface. +type testingT interface { + Errorf(format string, args ...any) +} + +func assertEqual[T Recording | Record](t testingT, want, got T, _ ...AssertOption) bool { + if h, ok := t.(interface{ Helper() }); ok { + h.Helper() + } + + cmpOpts := []cmp.Option{ + cmp.Comparer(func(x, y context.Context) bool { return x == y }), // Compare context. + cmpopts.SortSlices( + func(a, b log.KeyValue) bool { return a.Key < b.Key }, + ), // Unordered compare of the key values. + cmpopts.EquateEmpty(), // Empty and nil collections are equal. + } + + if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + return false + } + return true +} + +type assertConfig struct{} + +// AssertOption allows for fine grain control over how AssertEqual operates. +type AssertOption interface { + apply(cfg assertConfig) assertConfig +} diff --git a/log/logtest/assert_test.go b/log/logtest/assert_test.go new file mode 100644 index 000000000..bf3e4b499 --- /dev/null +++ b/log/logtest/assert_test.go @@ -0,0 +1,172 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logtest + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +var y2k = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) + +type mockTestingT struct { + errors []string +} + +func (m *mockTestingT) Errorf(format string, args ...any) { + m.errors = append(m.errors, format) +} + +func TestAssertEqual(t *testing.T) { + a := Recording{ + Scope{Name: t.Name()}: []Record{ + {Body: log.StringValue("msg"), Attributes: []log.KeyValue{log.String("foo", "bar"), log.Int("n", 1)}}, + }, + } + b := Recording{ + Scope{Name: t.Name()}: []Record{ + {Body: log.StringValue("msg"), Attributes: []log.KeyValue{log.Int("n", 1), log.String("foo", "bar")}}, + }, + } + + got := AssertEqual(t, a, b) + assert.True(t, got, "expected recordings to be equal") +} + +func TestAssertEqualRecording(t *testing.T) { + tests := []struct { + name string + a Recording + b Recording + opts []AssertOption + want bool + }{ + { + name: "equal recordings", + a: Recording{ + Scope{Name: t.Name()}: []Record{ + { + Timestamp: y2k, + Context: context.Background(), + Attributes: []log.KeyValue{log.Int("n", 1), log.String("foo", "bar")}, + }, + }, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{ + { + Timestamp: y2k, + Context: context.Background(), + Attributes: []log.KeyValue{log.String("foo", "bar"), log.Int("n", 1)}, + }, + }, + }, + want: true, + }, + { + name: "different recordings", + a: Recording{ + Scope{Name: t.Name()}: []Record{ + {Attributes: []log.KeyValue{log.String("foo", "bar")}}, + }, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{ + {Attributes: []log.KeyValue{log.Int("n", 1)}}, + }, + }, + want: false, + }, + { + name: "equal empty scopes", + a: Recording{ + Scope{Name: t.Name()}: nil, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{}, + }, + want: true, + }, + { + name: "equal empty attributes", + a: Recording{ + Scope{Name: t.Name()}: []Record{ + {Body: log.StringValue("msg"), Attributes: []log.KeyValue{}}, + }, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{ + {Body: log.StringValue("msg"), Attributes: nil}, + }, + }, + want: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockT := &mockTestingT{} + result := assertEqual(mockT, tc.a, tc.b, tc.opts...) + if result != tc.want { + t.Errorf("AssertEqual() = %v, want %v", result, tc.want) + } + if !tc.want && len(mockT.errors) == 0 { + t.Errorf("expected Errorf call but got none") + } + }) + } +} + +func TestAssertEqualRecord(t *testing.T) { + tests := []struct { + name string + a Record + b Record + opts []AssertOption + want bool + }{ + { + name: "equal records", + a: Record{ + Timestamp: y2k, + Context: context.Background(), + Attributes: []log.KeyValue{log.Int("n", 1), log.String("foo", "bar")}, + }, + b: Record{ + Timestamp: y2k, + Context: context.Background(), + Attributes: []log.KeyValue{log.String("foo", "bar"), log.Int("n", 1)}, + }, + want: true, + }, + { + name: "different records", + a: Record{ + Attributes: []log.KeyValue{log.String("foo", "bar")}, + }, + b: Record{ + Attributes: []log.KeyValue{log.Int("n", 1)}, + }, + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockT := &mockTestingT{} + result := assertEqual(mockT, tc.a, tc.b, tc.opts...) + if result != tc.want { + t.Errorf("AssertEqual() = %v, want %v", result, tc.want) + } + if !tc.want && len(mockT.errors) == 0 { + t.Errorf("expected Errorf call but got none") + } + }) + } +} diff --git a/log/logtest/assertions.go b/log/logtest/assertions.go deleted file mode 100644 index 9b6145fe1..000000000 --- a/log/logtest/assertions.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package logtest // import "go.opentelemetry.io/otel/log/logtest" - -import ( - "slices" - "testing" - - "go.opentelemetry.io/otel/log" -) - -// AssertRecordEqual compares two log records, and fails the test if they are -// not equal. -func AssertRecordEqual(t testing.TB, want, got log.Record) bool { - t.Helper() - - if want.EventName() != got.EventName() { - t.Errorf("EventName value is not equal:\nwant: %v\ngot: %v", want.EventName(), got.EventName()) - return false - } - if !want.Timestamp().Equal(got.Timestamp()) { - t.Errorf("Timestamp value is not equal:\nwant: %v\ngot: %v", want.Timestamp(), got.Timestamp()) - return false - } - if !want.ObservedTimestamp().Equal(got.ObservedTimestamp()) { - t.Errorf( - "ObservedTimestamp value is not equal:\nwant: %v\ngot: %v", - want.ObservedTimestamp(), - got.ObservedTimestamp(), - ) - return false - } - if want.Severity() != got.Severity() { - t.Errorf("Severity value is not equal:\nwant: %v\ngot: %v", want.Severity(), got.Severity()) - return false - } - if want.SeverityText() != got.SeverityText() { - t.Errorf("SeverityText value is not equal:\nwant: %v\ngot: %v", want.SeverityText(), got.SeverityText()) - return false - } - if !assertBody(t, want.Body(), got) { - return false - } - - var attrs []log.KeyValue - want.WalkAttributes(func(kv log.KeyValue) bool { - attrs = append(attrs, kv) - return true - }) - return assertAttributes(t, attrs, got) -} - -func assertBody(t testing.TB, want log.Value, r log.Record) bool { - t.Helper() - got := r.Body() - if !got.Equal(want) { - t.Errorf("Body value is not equal:\nwant: %v\ngot: %v", want, got) - return false - } - - return true -} - -func assertAttributes(t testing.TB, want []log.KeyValue, r log.Record) bool { - t.Helper() - var got []log.KeyValue - r.WalkAttributes(func(kv log.KeyValue) bool { - got = append(got, kv) - return true - }) - if !slices.EqualFunc(want, got, log.KeyValue.Equal) { - t.Errorf("Attributes are not equal:\nwant: %v\ngot: %v", want, got) - return false - } - - return true -} diff --git a/log/logtest/assertions_test.go b/log/logtest/assertions_test.go deleted file mode 100644 index 04d62d15d..000000000 --- a/log/logtest/assertions_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package logtest - -import ( - "testing" - "time" - - "go.opentelemetry.io/otel/log" -) - -func TestAssertRecord(t *testing.T) { - r1 := log.Record{} - r2 := log.Record{} - AssertRecordEqual(t, r1, r2) - - now := time.Now() - r1.SetEventName("my_event") - r2.SetEventName("my_event") - r1.SetTimestamp(now) - r2.SetTimestamp(now) - r1.SetObservedTimestamp(now) - r2.SetObservedTimestamp(now) - r1.SetSeverity(log.SeverityTrace1) - r2.SetSeverity(log.SeverityTrace1) - r1.SetSeverityText("trace") - r2.SetSeverityText("trace") - r1.SetBody(log.StringValue("log body")) - r2.SetBody(log.StringValue("log body")) - r1.AddAttributes(log.Bool("attr", true)) - r2.AddAttributes(log.Bool("attr", true)) - AssertRecordEqual(t, r1, r2) -} diff --git a/log/logtest/go.mod b/log/logtest/go.mod index c7752c057..52ab175ae 100644 --- a/log/logtest/go.mod +++ b/log/logtest/go.mod @@ -3,6 +3,7 @@ module go.opentelemetry.io/otel/log/logtest go 1.23.0 require ( + github.com/google/go-cmp v0.7.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/log v0.11.0