Skip to content

Commit bf7ffa3

Browse files
authored
log/logtest: Add AssertEqual and remove AssertRecordEqual (#6662)
Fixes #6487 Fixes #6488 Towards #6341 Prior-art: #6464 Replace `AssertRecordEqual` with `AssertEqual` which accepts options and can work not only with `Record` but most importantly, whole `Recording`. The changes between the previous approach are inspired by the design of https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest. Thanks to it we continue to use the "assert" pattern, but are not impacted by downsides of `testify`. The main issue is that with `testify` we cannot force `[]log.KeyValue` to sort slices before comparing (unordered collection equality). This can make the tests too much "white-boxed" and can easy break during refactoring. Moreover, empty and nil slices are seen not equal which in this case is not relevant. Here is an example of how the current tests of log bridges can be improved: - open-telemetry/opentelemetry-go-contrib#6953
1 parent 5cd1611 commit bf7ffa3

File tree

6 files changed

+229
-113
lines changed

6 files changed

+229
-113
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1818
- Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#6751)
1919
- Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#6752)
2020
- Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#6688)
21+
- Add `AssertEqual` function in `go.opentelemetry.io/otel/log/logtest`. (#6662)
2122

2223
### Removed
2324

2425
- Drop support for [Go 1.22]. (#6381, #6418)
2526
- Remove `Resource` field from `EnabledParameters` in `go.opentelemetry.io/otel/sdk/log`. (#6494)
26-
- Remove `RecordFactory` type from `go.opentelemetry.io/otel/log/logtest`. (#6492)
27+
- Remove `RecordFactory` type from `go.opentelemetry.io/otel/log/logtest`. (#6492)
2728
- Remove `ScopeRecords`, `EmittedRecord`, and `RecordFactory` types from `go.opentelemetry.io/otel/log/logtest`. (#6507)
29+
- Remove `AssertRecordEqual` function in `go.opentelemetry.io/otel/log/logtest`, use `AssertEqual` instead. (#6662)
2830

2931
### Changed
3032

log/logtest/assert.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package logtest // import "go.opentelemetry.io/otel/log/logtest"
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/google/go-cmp/cmp/cmpopts"
12+
13+
"go.opentelemetry.io/otel/log"
14+
)
15+
16+
// AssertEqual asserts that the two concrete data-types from the logtest package are equal.
17+
func AssertEqual[T Recording | Record](t *testing.T, want, got T, opts ...AssertOption) bool {
18+
t.Helper()
19+
return assertEqual(t, want, got, opts...)
20+
}
21+
22+
// testingT reports failure messages.
23+
// *testing.T implements this interface.
24+
type testingT interface {
25+
Errorf(format string, args ...any)
26+
}
27+
28+
func assertEqual[T Recording | Record](t testingT, want, got T, _ ...AssertOption) bool {
29+
if h, ok := t.(interface{ Helper() }); ok {
30+
h.Helper()
31+
}
32+
33+
cmpOpts := []cmp.Option{
34+
cmp.Comparer(func(x, y context.Context) bool { return x == y }), // Compare context.
35+
cmpopts.SortSlices(
36+
func(a, b log.KeyValue) bool { return a.Key < b.Key },
37+
), // Unordered compare of the key values.
38+
cmpopts.EquateEmpty(), // Empty and nil collections are equal.
39+
}
40+
41+
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
42+
t.Errorf("mismatch (-want +got):\n%s", diff)
43+
return false
44+
}
45+
return true
46+
}
47+
48+
type assertConfig struct{}
49+
50+
// AssertOption allows for fine grain control over how AssertEqual operates.
51+
type AssertOption interface {
52+
apply(cfg assertConfig) assertConfig
53+
}

log/logtest/assert_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package logtest
5+
6+
import (
7+
"context"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/assert"
12+
13+
"go.opentelemetry.io/otel/log"
14+
)
15+
16+
var y2k = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
17+
18+
type mockTestingT struct {
19+
errors []string
20+
}
21+
22+
func (m *mockTestingT) Errorf(format string, args ...any) {
23+
m.errors = append(m.errors, format)
24+
}
25+
26+
func TestAssertEqual(t *testing.T) {
27+
a := Recording{
28+
Scope{Name: t.Name()}: []Record{
29+
{Body: log.StringValue("msg"), Attributes: []log.KeyValue{log.String("foo", "bar"), log.Int("n", 1)}},
30+
},
31+
}
32+
b := Recording{
33+
Scope{Name: t.Name()}: []Record{
34+
{Body: log.StringValue("msg"), Attributes: []log.KeyValue{log.Int("n", 1), log.String("foo", "bar")}},
35+
},
36+
}
37+
38+
got := AssertEqual(t, a, b)
39+
assert.True(t, got, "expected recordings to be equal")
40+
}
41+
42+
func TestAssertEqualRecording(t *testing.T) {
43+
tests := []struct {
44+
name string
45+
a Recording
46+
b Recording
47+
opts []AssertOption
48+
want bool
49+
}{
50+
{
51+
name: "equal recordings",
52+
a: Recording{
53+
Scope{Name: t.Name()}: []Record{
54+
{
55+
Timestamp: y2k,
56+
Context: context.Background(),
57+
Attributes: []log.KeyValue{log.Int("n", 1), log.String("foo", "bar")},
58+
},
59+
},
60+
},
61+
b: Recording{
62+
Scope{Name: t.Name()}: []Record{
63+
{
64+
Timestamp: y2k,
65+
Context: context.Background(),
66+
Attributes: []log.KeyValue{log.String("foo", "bar"), log.Int("n", 1)},
67+
},
68+
},
69+
},
70+
want: true,
71+
},
72+
{
73+
name: "different recordings",
74+
a: Recording{
75+
Scope{Name: t.Name()}: []Record{
76+
{Attributes: []log.KeyValue{log.String("foo", "bar")}},
77+
},
78+
},
79+
b: Recording{
80+
Scope{Name: t.Name()}: []Record{
81+
{Attributes: []log.KeyValue{log.Int("n", 1)}},
82+
},
83+
},
84+
want: false,
85+
},
86+
{
87+
name: "equal empty scopes",
88+
a: Recording{
89+
Scope{Name: t.Name()}: nil,
90+
},
91+
b: Recording{
92+
Scope{Name: t.Name()}: []Record{},
93+
},
94+
want: true,
95+
},
96+
{
97+
name: "equal empty attributes",
98+
a: Recording{
99+
Scope{Name: t.Name()}: []Record{
100+
{Body: log.StringValue("msg"), Attributes: []log.KeyValue{}},
101+
},
102+
},
103+
b: Recording{
104+
Scope{Name: t.Name()}: []Record{
105+
{Body: log.StringValue("msg"), Attributes: nil},
106+
},
107+
},
108+
want: true,
109+
},
110+
}
111+
112+
for _, tc := range tests {
113+
t.Run(tc.name, func(t *testing.T) {
114+
mockT := &mockTestingT{}
115+
result := assertEqual(mockT, tc.a, tc.b, tc.opts...)
116+
if result != tc.want {
117+
t.Errorf("AssertEqual() = %v, want %v", result, tc.want)
118+
}
119+
if !tc.want && len(mockT.errors) == 0 {
120+
t.Errorf("expected Errorf call but got none")
121+
}
122+
})
123+
}
124+
}
125+
126+
func TestAssertEqualRecord(t *testing.T) {
127+
tests := []struct {
128+
name string
129+
a Record
130+
b Record
131+
opts []AssertOption
132+
want bool
133+
}{
134+
{
135+
name: "equal records",
136+
a: Record{
137+
Timestamp: y2k,
138+
Context: context.Background(),
139+
Attributes: []log.KeyValue{log.Int("n", 1), log.String("foo", "bar")},
140+
},
141+
b: Record{
142+
Timestamp: y2k,
143+
Context: context.Background(),
144+
Attributes: []log.KeyValue{log.String("foo", "bar"), log.Int("n", 1)},
145+
},
146+
want: true,
147+
},
148+
{
149+
name: "different records",
150+
a: Record{
151+
Attributes: []log.KeyValue{log.String("foo", "bar")},
152+
},
153+
b: Record{
154+
Attributes: []log.KeyValue{log.Int("n", 1)},
155+
},
156+
want: false,
157+
},
158+
}
159+
160+
for _, tc := range tests {
161+
t.Run(tc.name, func(t *testing.T) {
162+
mockT := &mockTestingT{}
163+
result := assertEqual(mockT, tc.a, tc.b, tc.opts...)
164+
if result != tc.want {
165+
t.Errorf("AssertEqual() = %v, want %v", result, tc.want)
166+
}
167+
if !tc.want && len(mockT.errors) == 0 {
168+
t.Errorf("expected Errorf call but got none")
169+
}
170+
})
171+
}
172+
}

log/logtest/assertions.go

Lines changed: 0 additions & 78 deletions
This file was deleted.

log/logtest/assertions_test.go

Lines changed: 0 additions & 34 deletions
This file was deleted.

log/logtest/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module go.opentelemetry.io/otel/log/logtest
33
go 1.23.0
44

55
require (
6+
github.com/google/go-cmp v0.7.0
67
github.com/stretchr/testify v1.10.0
78
go.opentelemetry.io/otel v1.35.0
89
go.opentelemetry.io/otel/log v0.11.0

0 commit comments

Comments
 (0)