Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add otellog test recorder #48

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion otellog/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/d-velop/dvelop-sdk-go/otellog

go 1.17
go 1.18
91 changes: 91 additions & 0 deletions otellog/otellogtest/recorder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package otellogtest

import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/d-velop/dvelop-sdk-go/otellog"
)

type testingT interface {
Errorf(format string, args ...any)
Helper()
}

type LogRecorder struct {
Events []otellog.Event
t testingT
}

// NewLogRecorder creates a new LogRecorder that can be used to assert log messages.
// The given testingT is used to fail the test if an assertion fails.
// The default log output formatter is replaced with a formatter that records all log messages.
// The default time function is replaced with a function that always returns the same time (2022-01-01 01:02:03.000000004 UTC).
func NewLogRecorder(t testingT) *LogRecorder {
otellog.Default().Reset()
rec := &LogRecorder{[]otellog.Event{}, t}

otellog.SetOutputFormatter(func(event *otellog.Event) ([]byte, error) {
rec.Events = append(rec.Events, *event)
return []byte{}, nil
})

otellog.SetTime(func() time.Time {
return time.Date(2022, time.January, 01, 1, 2, 3, 4, time.UTC)
})
return rec
}

// ShouldHaveLogged asserts that the given log message was logged at some point.
// The log message can be an otellog.Severity, string or any other type that can be converted to a string.
// If multiple arguments are given, they are treated as a logical AND.
func (l *LogRecorder) ShouldHaveLogged(conditions ...any) {
l.t.Helper()

for _, event := range l.Events {
if matches(event, conditions...) {
return
}
}

l.t.Errorf("no log found matching %v", conditions)
}

func matches(event otellog.Event, conditions ...any) bool {
for _, condition := range conditions {
switch condition := condition.(type) {
case otellog.Severity:
if event.Severity != condition {
return false
}
case int:
if int(event.Severity) != condition {
return false
}

case func(event otellog.Event) bool:
if !condition(event) {
return false
}

default:
bodyAsString := fmt.Sprint(event.Body)
conditionAsString := fmt.Sprint(condition)
if !strings.Contains(bodyAsString, conditionAsString) {
return false
}
}
}
return true
}

func ContainsAttribute(key string, value any) func(event otellog.Event) bool {
return func(event otellog.Event) bool {
bytes, _ := event.Attributes.MarshalJSON()
attributes := map[string]string{}
_ = json.Unmarshal(bytes, &attributes)
return attributes[key] == value
}
}
195 changes: 195 additions & 0 deletions otellog/otellogtest/recorder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package otellogtest_test

import (
"context"
"fmt"
"testing"

"github.com/d-velop/dvelop-sdk-go/otellog"
"github.com/d-velop/dvelop-sdk-go/otellog/otellogtest"
)

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldMatch(t *testing.T) {
recorder := otellogtest.NewLogRecorder(t)
otellog.Info(context.Background(), "foo")

recorder.ShouldHaveLogged("foo")
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldMatchPartially(t *testing.T) {
recorder := otellogtest.NewLogRecorder(t)
otellog.Info(context.Background(), "foo bar")

recorder.ShouldHaveLogged("foo")
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldMatchWithSeverity(t *testing.T) {
recorder := otellogtest.NewLogRecorder(t)
otellog.Info(context.Background(), "foo")

recorder.ShouldHaveLogged(otellog.SeverityInfo, "foo")
}

func TestLogRecorder_givenFunctionMatcher_whenAsserting_thenShouldMatch(t *testing.T) {
recorder := otellogtest.NewLogRecorder(t)
otellog.Info(context.Background(), "test")

recorder.ShouldHaveLogged(func(event otellog.Event) bool {
return true
})
}

func TestLogRecorder_givenFunctionMatcherNotMatching_whenAsserting_thenShouldNotMatch(t *testing.T) {
ft := &fakeTestingT{}
recorder := otellogtest.NewLogRecorder(ft)
otellog.Info(context.Background(), "test")

recorder.ShouldHaveLogged(func(event otellog.Event) bool {
return false
})

if !ft.failed {
t.Error("expected test to fail")
}
}

func TestLogRecorder_givenAttributeMatcher_whenAsserting_thenShouldMatch(t *testing.T) {
recorder := otellogtest.NewLogRecorder(t)
otellog.WithAdditionalAttributes(map[string]any{"foo": "bar"}).Info(context.Background(), "test")

recorder.ShouldHaveLogged("test", otellogtest.ContainsAttribute("foo", "bar"))
}

func TestLogRecorder_givenAttributeMatcherNotMatching_whenAsserting_thenShouldNotMatch(t *testing.T) {
ft := &fakeTestingT{}
recorder := otellogtest.NewLogRecorder(ft)
otellog.WithAdditionalAttributes(map[string]any{"foo": "bar"}).Info(context.Background(), "test")

recorder.ShouldHaveLogged("test", otellogtest.ContainsAttribute("hello", "world"))

if !ft.failed {
t.Error("expected test to fail")
}
}

func TestLogRecorder_givenOnlySeverity_whenAsserting_thenShouldMatch(t *testing.T) {
recorder := otellogtest.NewLogRecorder(t)
otellog.Info(context.Background(), "foo")
var severity otellog.Severity
{
severity = otellog.SeverityInfo
}

recorder.ShouldHaveLogged(severity)
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldNotMatch(t *testing.T) {
ft := &fakeTestingT{}
recorder := otellogtest.NewLogRecorder(ft)
otellog.Info(context.Background(), "foo")

recorder.ShouldHaveLogged("bar")
if !ft.failed {
t.Error("expected test to fail")
}
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldNotMatchWithSeverity(t *testing.T) {
ft := &fakeTestingT{}
recorder := otellogtest.NewLogRecorder(ft)
otellog.Info(context.Background(), "foo")
var severity otellog.Severity
{
severity = otellog.SeverityError
}

recorder.ShouldHaveLogged(severity, "foo")
if !ft.failed {
t.Error("expected test to fail")
}
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldNotMatchWithSeverityOnly(t *testing.T) {
ft := &fakeTestingT{}
recorder := otellogtest.NewLogRecorder(ft)
otellog.Info(context.Background(), "foo")

recorder.ShouldHaveLogged(otellog.SeverityError)
if !ft.failed {
t.Error("expected test to fail")
}
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldMatchWithMultipleConditions(t *testing.T) {
recorder := otellogtest.NewLogRecorder(t)
otellog.Info(context.Background(), "foo bar")

recorder.ShouldHaveLogged("foo", "bar")
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldNotMatchWithMultipleConditions(t *testing.T) {
ft := &fakeTestingT{}
recorder := otellogtest.NewLogRecorder(ft)
otellog.Info(context.Background(), "foo bar")

recorder.ShouldHaveLogged("foo", "baz")
if !ft.failed {
t.Error("expected test to fail")
}
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenShouldMatchWithStringerThing(t *testing.T) {
recorder := otellogtest.NewLogRecorder(t)
otellog.Info(context.Background(), thing{"foo"})

recorder.ShouldHaveLogged(thing{"foo"})
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenFailsWithCorrectMessage(t *testing.T) {
ft := &fakeTestingT{}
recorder := otellogtest.NewLogRecorder(ft)
otellog.Info(context.Background(), "foo")

recorder.ShouldHaveLogged("bar")
if !ft.failed {
t.Error("expected test to fail")
}
if ft.m != "no log found matching [bar]" {
t.Errorf("expected error message to be 'no log found matching [bar]', but was '%v'", ft.m)
}
}

func TestLogRecorder_givenSomeLog_whenAsserting_thenFailsWithCorrectMessageAndSeverity(t *testing.T) {
ft := &fakeTestingT{}
recorder := otellogtest.NewLogRecorder(ft)
otellog.Info(context.Background(), "foo")

recorder.ShouldHaveLogged(otellog.SeverityError, "bar")
if !ft.failed {
t.Error("expected test to fail")
}
if ft.m != "no log found matching [17 bar]" {
t.Errorf("expected error message to be 'no log found matching [17 bar]', but was '%v'", ft.m)
}
}

type thing struct {
content string
}

func (t thing) String() string {
return t.content
}

type fakeTestingT struct {
failed bool
m string
}

func (f *fakeTestingT) Errorf(format string, args ...any) {
f.failed = true
f.m = fmt.Sprintf(format, args...)
}

func (f *fakeTestingT) Helper() {
}