From 956da05eeccb87a94f2c72f2629c8d22dbd90cea Mon Sep 17 00:00:00 2001 From: Gregor Noczinski Date: Thu, 25 Jul 2024 19:32:39 +0200 Subject: [PATCH] Added SDK testlog (#29) --- .github/workflows/ci.yml | 2 +- LICENSE | 2 +- README.md | 6 +- doc.go | 54 ++--- fields/filtered_test.go | 3 +- native/color/prepare_appengine.go | 1 + native/color/prepare_bsd.go | 1 + native/color/prepare_mock.go | 1 + native/color/prepare_no_terminal.go | 1 + native/color/prepare_notappengine.go | 1 + native/color/prepare_solaris.go | 1 + native/color/prepare_unix.go | 1 + native/color/prepare_windows.go | 1 + native/color/supported_mocked_test.go | 1 + native/doc.go | 36 ++-- native/facade/value/doc.go | 13 +- native/facade/value/formatter.go | 1 + native/formatter/functions/color_test.go | 4 +- native/formatter/json.go | 1 + native/level/colorizer.go | 4 +- sdk/bridge/doc.go | 8 +- sdk/bridge/hook/doc.go | 6 +- sdk/bridge/wrapper.go | 6 +- sdk/testlog/README.md | 31 +++ sdk/testlog/doc.go | 26 +++ sdk/testlog/event.go | 67 ++++++ sdk/testlog/event_test.go | 118 +++++++++++ sdk/testlog/hook.go | 29 +++ sdk/testlog/hook_test.go | 16 ++ sdk/testlog/level/formatter.go | 36 ++++ sdk/testlog/level/formatter_test.go | 45 ++++ sdk/testlog/logger.go | 26 +++ sdk/testlog/logger_core.go | 207 ++++++++++++++++++ sdk/testlog/logger_core_compat.go | 7 + sdk/testlog/logger_core_test.go | 202 ++++++++++++++++++ sdk/testlog/logger_core_with_depth.go | 18 ++ sdk/testlog/logger_renamed.go | 18 ++ sdk/testlog/logger_test.go | 30 +++ sdk/testlog/provider.go | 254 +++++++++++++++++++++++ sdk/testlog/provider_test.go | 171 +++++++++++++++ testing/recording/logger_core.go | 2 +- 41 files changed, 1389 insertions(+), 69 deletions(-) create mode 100644 sdk/testlog/README.md create mode 100644 sdk/testlog/doc.go create mode 100644 sdk/testlog/event.go create mode 100644 sdk/testlog/event_test.go create mode 100644 sdk/testlog/hook.go create mode 100644 sdk/testlog/hook_test.go create mode 100644 sdk/testlog/level/formatter.go create mode 100644 sdk/testlog/level/formatter_test.go create mode 100644 sdk/testlog/logger.go create mode 100644 sdk/testlog/logger_core.go create mode 100644 sdk/testlog/logger_core_compat.go create mode 100644 sdk/testlog/logger_core_test.go create mode 100644 sdk/testlog/logger_core_with_depth.go create mode 100644 sdk/testlog/logger_renamed.go create mode 100644 sdk/testlog/logger_test.go create mode 100644 sdk/testlog/provider.go create mode 100644 sdk/testlog/provider_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f9ec84..aafb2a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: name: Test strategy: matrix: - go-version: [ 1.17.0, 1.20.0, 1.21.0 ] + go-version: [ 1.17.0, 1.21.0, 1.22.0 ] os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: diff --git a/LICENSE b/LICENSE index 45e41de..db4e98a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 echocat +Copyright (c) 2020-2024 echocat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d5f4065..d87c576 100644 --- a/README.md +++ b/README.md @@ -147,9 +147,11 @@ Done. Enjoy! ## Implementations -### [slf4g-native](native) +1. [native](native): Reference implementation of [slf4g](https://github.com/echocat/slf4g). -This is the reference implementation of [slf4g](https://github.com/echocat/slf4g). +2. [testlog](sdk/testlog): Ensure that everything which is logged within test by [slf4g](https://github.com/echocat/slf4g) appears correctly within tests. + +2. [recording](testing/recording): Will record everything which is logged by [slf4g](https://github.com/echocat/slf4g) and can then be asserted inside test cases. ## Contributing diff --git a/doc.go b/doc.go index f72c5ea..0aa7852 100644 --- a/doc.go +++ b/doc.go @@ -1,8 +1,8 @@ -// The Simple Logging Facade for Go provides an easy access who everyone who -// wants to log something and do not want to care how it is logged and gives -// others the possibility to implement their own loggers in easy way. +// Package log is the Simple Logging Facade for Go provides an easy access who +// everyone who wants to log something and do not want to care how it is logged +// and gives others the possibility to implement their own loggers in easy way. // -// Usage +// # Usage // // There are 2 common ways to use this framework. // @@ -10,45 +10,45 @@ // and is quite clean for one package. If the package contains too many logic // it might worth it to use the 2nd approach (see below). // -// package foo +// package foo // -// import "github.com/echocat/slf4g" +// import "github.com/echocat/slf4g" // -// var logger = log.GetLoggerForCurrentPackage() +// var logger = log.GetLoggerForCurrentPackage() // -// func sayHello() { -// // will log with logger="github.com/user/foo" -// logger.Info("Hello, world!") -// } +// func sayHello() { +// // will log with logger="github.com/user/foo" +// logger.Info("Hello, world!") +// } // // 2. By getting a logger for the object the logger is for. This is the most // clean approach and will give you later the maximum flexibility and control. // -// package foo +// package foo // -// import "github.com/echocat/slf4g" +// import "github.com/echocat/slf4g" // -// var logger = log.GetLogger(myType{}) +// var logger = log.GetLogger(myType{}) // -// type myType struct { -// ... -// } +// type myType struct { +// ... +// } // -// func (mt myType) sayHello() { -// // will log with logger="github.com/user/foo.myType" -// logger.Info("Hello, world!") -// } +// func (mt myType) sayHello() { +// // will log with logger="github.com/user/foo.myType" +// logger.Info("Hello, world!") +// } // // 3. By using the global packages methods which is quite equal to how the SDK // base logger works. This is only recommend for small application and not for // libraries you like to export. // -// package foo +// package foo // -// import "github.com/echocat/slf4g" +// import "github.com/echocat/slf4g" // -// func sayHello() { -// // will log with logger=ROOT -// log.Info("Hello, world!") -// } +// func sayHello() { +// // will log with logger=ROOT +// log.Info("Hello, world!") +// } package log diff --git a/fields/filtered_test.go b/fields/filtered_test.go index 54c789f..7b3fd1c 100644 --- a/fields/filtered_test.go +++ b/fields/filtered_test.go @@ -2,9 +2,10 @@ package fields import ( "fmt" - "github.com/echocat/slf4g/level" "testing" + "github.com/echocat/slf4g/level" + "github.com/echocat/slf4g/internal/test/assert" ) diff --git a/native/color/prepare_appengine.go b/native/color/prepare_appengine.go index eee2a3b..bf88140 100644 --- a/native/color/prepare_appengine.go +++ b/native/color/prepare_appengine.go @@ -1,3 +1,4 @@ +//go:build !mock && appengine // +build !mock,appengine package color diff --git a/native/color/prepare_bsd.go b/native/color/prepare_bsd.go index e8009f1..790201c 100644 --- a/native/color/prepare_bsd.go +++ b/native/color/prepare_bsd.go @@ -1,3 +1,4 @@ +//go:build (!mock && darwin) || dragonfly || freebsd || netbsd || openbsd // +build !mock,darwin dragonfly freebsd netbsd openbsd package color diff --git a/native/color/prepare_mock.go b/native/color/prepare_mock.go index 213103c..3b223ee 100644 --- a/native/color/prepare_mock.go +++ b/native/color/prepare_mock.go @@ -1,3 +1,4 @@ +//go:build mock // +build mock package color diff --git a/native/color/prepare_no_terminal.go b/native/color/prepare_no_terminal.go index 8b5813e..66f301b 100644 --- a/native/color/prepare_no_terminal.go +++ b/native/color/prepare_no_terminal.go @@ -1,3 +1,4 @@ +//go:build (!mock && js) || nacl || plan9 // +build !mock,js nacl plan9 package color diff --git a/native/color/prepare_notappengine.go b/native/color/prepare_notappengine.go index ccee678..985f550 100644 --- a/native/color/prepare_notappengine.go +++ b/native/color/prepare_notappengine.go @@ -1,3 +1,4 @@ +//go:build !mock && !appengine && !js && !windows && !nacl && !plan9 // +build !mock,!appengine,!js,!windows,!nacl,!plan9 package color diff --git a/native/color/prepare_solaris.go b/native/color/prepare_solaris.go index 1a95627..3814de2 100644 --- a/native/color/prepare_solaris.go +++ b/native/color/prepare_solaris.go @@ -1,3 +1,4 @@ +//go:build !mock // +build !mock package color diff --git a/native/color/prepare_unix.go b/native/color/prepare_unix.go index c4770e1..41553bc 100644 --- a/native/color/prepare_unix.go +++ b/native/color/prepare_unix.go @@ -1,3 +1,4 @@ +//go:build (!mock && linux) || aix // +build !mock,linux aix package color diff --git a/native/color/prepare_windows.go b/native/color/prepare_windows.go index 9b46b05..9608522 100644 --- a/native/color/prepare_windows.go +++ b/native/color/prepare_windows.go @@ -1,3 +1,4 @@ +//go:build !mock // +build !mock package color diff --git a/native/color/supported_mocked_test.go b/native/color/supported_mocked_test.go index 1915ca1..0bb829e 100644 --- a/native/color/supported_mocked_test.go +++ b/native/color/supported_mocked_test.go @@ -1,3 +1,4 @@ +//go:build mock // +build mock package color diff --git a/native/doc.go b/native/doc.go index 32ede6e..bceddda 100644 --- a/native/doc.go +++ b/native/doc.go @@ -1,33 +1,35 @@ -// This is the reference implementation of a logger of the slf4g framework +// Package native holds the reference implementation of a logger of the slf4g framework // (https://github.com/echocat/slf4g). // -// Usage +// # Usage // // For the most common cases it is fully enough to anonymously import this // package in your main.go; nothing more is needed. // // github.com/foo/bar/main/main.go: -// package main // -// import ( -// "github.com/foo/bar" -// _ "github.com/echocat/slf4g/native" -// ) +// package main // -// func main() { -// bar.SayHello() -// } +// import ( +// "github.com/foo/bar" +// _ "github.com/echocat/slf4g/native" +// ) +// +// func main() { +// bar.SayHello() +// } // // github.com/foo/bar/bar.go: -// package bar // -// import ( -// "github.com/echocat/slf4g" -// ) +// package bar +// +// import ( +// "github.com/echocat/slf4g" +// ) // -// func SayHello() { -// log.Info("Hello, world!") -// } +// func SayHello() { +// log.Info("Hello, world!") +// } // // See more useful stuff in the examples sections. package native diff --git a/native/facade/value/doc.go b/native/facade/value/doc.go index 9055ac1..57c9b8d 100644 --- a/native/facade/value/doc.go +++ b/native/facade/value/doc.go @@ -4,12 +4,15 @@ // encoding.TextUnmarshaler, too. // // Example: -// pv := value.NewProvider(native.DefaultProvider) // -// flag.Var(pv.Consumer.Formatter, "log.format", "Configures the log format.") -// flag.Var(pv.Level, "log.level", "Configures the log level.") +// pv := value.NewProvider(native.DefaultProvider) +// +// flag.Var(pv.Consumer.Formatter, "log.format", "Configures the log format.") +// flag.Var(pv.Level, "log.level", "Configures the log level.") +// +// flag.Parse() // -// flag.Parse() // Now you can call: -// $ -log.format=json -log.level=debug ... +// +// $ -log.format=json -log.level=debug ... package value diff --git a/native/facade/value/formatter.go b/native/facade/value/formatter.go index d0c465e..1f239c4 100644 --- a/native/facade/value/formatter.go +++ b/native/facade/value/formatter.go @@ -2,6 +2,7 @@ package value import ( "fmt" + "github.com/echocat/slf4g/native/color" "github.com/echocat/slf4g/native/formatter" diff --git a/native/formatter/functions/color_test.go b/native/formatter/functions/color_test.go index 89e3b80..77e6df3 100644 --- a/native/formatter/functions/color_test.go +++ b/native/formatter/functions/color_test.go @@ -46,12 +46,12 @@ func Test_Colorize(t *testing.T) { givenColorCode: "15;1", givenText: "hello, world", shouldColorize: true, - expected: `hello, world`, + expected: `hello, world`, }, { givenColorCode: "1", givenText: "hello, world", shouldColorize: true, - expected: `hello, world`, + expected: `hello, world`, }, { givenColorCode: "15;1", givenText: "hello, world", diff --git a/native/formatter/json.go b/native/formatter/json.go index 2deee25..a008d96 100644 --- a/native/formatter/json.go +++ b/native/formatter/json.go @@ -2,6 +2,7 @@ package formatter import ( "fmt" + "github.com/echocat/slf4g/level" "github.com/echocat/slf4g/native/execution" diff --git a/native/level/colorizer.go b/native/level/colorizer.go index a83bf60..9d610a0 100644 --- a/native/level/colorizer.go +++ b/native/level/colorizer.go @@ -9,8 +9,8 @@ import ( var DefaultColorizer Colorizer = ColorizerMap{ level.Trace: ``, level.Debug: ``, - level.Info: ``, - level.Warn: ``, + level.Info: ``, + level.Warn: ``, level.Error: ``, level.Fatal: ``, } diff --git a/sdk/bridge/doc.go b/sdk/bridge/doc.go index 5005b6e..708990a 100644 --- a/sdk/bridge/doc.go +++ b/sdk/bridge/doc.go @@ -1,15 +1,15 @@ // Package sdk/bridge provides methods to either hook into the SDK logger itself // or create compatible instances. // -// Hooks +// # Hooks // // The simples way is to simply anonymously import the hook package to configure // the whole application to use the slf4g framework on any usage of the SDK // based loggers. // -// import ( -// _ "github.com/echocat/slf4g/sdk/bridge/hook" -// ) +// import ( +// _ "github.com/echocat/slf4g/sdk/bridge/hook" +// ) // // For manual hooks please see the examples. package sdk diff --git a/sdk/bridge/hook/doc.go b/sdk/bridge/hook/doc.go index 1b826ea..21a9a18 100644 --- a/sdk/bridge/hook/doc.go +++ b/sdk/bridge/hook/doc.go @@ -1,7 +1,7 @@ // Importing this package anonymously will configure the whole application to // use the slf4g framework on any usage of the SDK based loggers. // -// import ( -// _ "github.com/echocat/slf4g/sdk/bridge/hook" -// ) +// import ( +// _ "github.com/echocat/slf4g/sdk/bridge/hook" +// ) package hook diff --git a/sdk/bridge/wrapper.go b/sdk/bridge/wrapper.go index 73a52d7..baf1186 100644 --- a/sdk/bridge/wrapper.go +++ b/sdk/bridge/wrapper.go @@ -12,7 +12,7 @@ import ( // its root Logger and logs everything printed using Print(), Printf() and // Println() on level.Info. // -// Limitations +// # Limitations // // 1# Stuff logged using Fatal*() and Panic*() are on logged on the same Level // as everything else. @@ -28,7 +28,7 @@ func Configure(customizer ...func(*log.LoggingWriter)) { // it's the given Logger and logs everything printed using Print(), Printf() and // Println() on given level.Level. // -// Limitations +// # Limitations // // 1# Stuff logged using Fatal*() and Panic*() are on logged on the same Level // as everything else. @@ -54,7 +54,7 @@ func ConfigureWith(target log.CoreLogger, logAs level.Level, customizer ...func( // logs everything printed using Print(), Printf() and Println() on given // level.Level. // -// Limitations +// # Limitations // // 1# Stuff logged using Fatal*() and Panic*() are on logged on the same Level // as everything else. diff --git a/sdk/testlog/README.md b/sdk/testlog/README.md new file mode 100644 index 0000000..8d6bebd --- /dev/null +++ b/sdk/testlog/README.md @@ -0,0 +1,31 @@ +[![PkgGoDev](https://pkg.go.dev/badge/github.com/echocat/slf4g/sdk/testlog)](https://pkg.go.dev/github.com/echocat/slf4g/sdk/testlog) + +# slf4g testing Logger implementation + +Provides a Logger which will be connected to [`testing.T.Log()`](https://pkg.go.dev/testing#T.Log) of the go SDK. + +If you're looking for an instance to record all logged events see [`github.com/echocat/slf4g/testing/recording`](../../testing/recording) package. + +## Usage + +The easiest way to enable the slf4g framework in your tests is, simply: + +```golang +package foo + +import ( + "testing" + "github.com/echocat/slf4g" + "github.com/echocat/slf4g/sdk/testlog" +) + +func TestMyGreatStuff(t *testing.T) { + testlog.Hook(t) + + log.Info("Yeah! This is a log!") +} +``` + +... that's it! + +See [`Hook(..)`](hook.go) for more details. diff --git a/sdk/testlog/doc.go b/sdk/testlog/doc.go new file mode 100644 index 0000000..2a6be5f --- /dev/null +++ b/sdk/testlog/doc.go @@ -0,0 +1,26 @@ +// Package testlog provides a Logger which will be connected to testing.T.Log() +// of the go SDK. +// +// If you're looking for an instance to record all logged events see +// github.com/echocat/slf4g/testing/recording package. +// +// # Usage +// +// The easiest way to enable the slf4g framework in your tests is, simply: +// +// import ( +// "testing" +// "github.com/echocat/slf4g" +// "github.com/echocat/slf4g/sdk/testlog" +// ) +// +// func TestMyGreatStuff(t *testing.T) { +// testlog.Hook(t) +// +// log.Info("Yeah! This is a log!") +// } +// +// ... that's it! +// +// See Hook(..) for more details. +package testlog diff --git a/sdk/testlog/event.go b/sdk/testlog/event.go new file mode 100644 index 0000000..24adbe5 --- /dev/null +++ b/sdk/testlog/event.go @@ -0,0 +1,67 @@ +package testlog + +import ( + log "github.com/echocat/slf4g" + "github.com/echocat/slf4g/fields" + "github.com/echocat/slf4g/level" +) + +type event struct { + provider *Provider + fields fields.Fields + level level.Level +} + +func (instance *event) ForEach(consumer func(key string, value interface{}) error) error { + return instance.fields.ForEach(consumer) +} + +func (instance *event) Get(key string) (interface{}, bool) { + return instance.fields.Get(key) +} + +func (instance *event) Len() int { + return instance.fields.Len() +} + +func (instance *event) GetLevel() level.Level { + return instance.level +} + +func (instance *event) With(key string, value interface{}) log.Event { + return instance.with(func(s fields.Fields) fields.Fields { + return s.With(key, value) + }) +} + +func (instance *event) Withf(key string, format string, args ...interface{}) log.Event { + return instance.with(func(s fields.Fields) fields.Fields { + return s.Withf(key, format, args...) + }) +} + +func (instance *event) WithError(err error) log.Event { + return instance.with(func(s fields.Fields) fields.Fields { + return s.With(instance.provider.GetFieldKeysSpec().GetError(), err) + }) +} + +func (instance *event) WithAll(of map[string]interface{}) log.Event { + return instance.with(func(s fields.Fields) fields.Fields { + return s.WithAll(of) + }) +} + +func (instance *event) Without(keys ...string) log.Event { + return instance.with(func(s fields.Fields) fields.Fields { + return s.Without(keys...) + }) +} + +func (instance *event) with(mod func(fields.Fields) fields.Fields) log.Event { + return &event{ + provider: instance.provider, + fields: mod(instance.fields), + level: instance.level, + } +} diff --git a/sdk/testlog/event_test.go b/sdk/testlog/event_test.go new file mode 100644 index 0000000..a90e699 --- /dev/null +++ b/sdk/testlog/event_test.go @@ -0,0 +1,118 @@ +package testlog + +import ( + "errors" + "testing" + + "github.com/echocat/slf4g/level" + + "github.com/echocat/slf4g/fields" + "github.com/echocat/slf4g/internal/test/assert" +) + +func Test_event_ForEach(t *testing.T) { + instance := &event{fields: fields. + With("a", 1). + With("b", 2). + With("c", 3)} + + actual := map[string]interface{}{} + actualErr := instance.ForEach(func(key string, value interface{}) error { + actual[key] = value + return nil + }) + + assert.ToBeNil(t, actualErr) + assert.ToBeEqual(t, map[string]interface{}{"c": 3, "b": 2, "a": 1}, actual) +} + +func Test_event_Get(t *testing.T) { + instance := &event{fields: fields. + With("a", 1). + With("b", 2)} + + actual1, actualExists1 := instance.Get("a") + assert.ToBeEqual(t, true, actualExists1) + assert.ToBeEqual(t, 1, actual1) + actual2, actualExists2 := instance.Get("b") + assert.ToBeEqual(t, true, actualExists2) + assert.ToBeEqual(t, 2, actual2) + actual3, actualExists3 := instance.Get("c") + assert.ToBeEqual(t, false, actualExists3) + assert.ToBeEqual(t, nil, actual3) +} + +func Test_event_Len(t *testing.T) { + instance := &event{fields: fields. + With("a", 1). + With("b", 2)} + + actual := instance.Len() + + assert.ToBeEqual(t, 2, actual) +} + +func Test_event_GetLevel(t *testing.T) { + instance := &event{level: level.Error} + + actual := instance.GetLevel() + + assert.ToBeEqual(t, level.Error, actual) +} + +func Test_event_With(t *testing.T) { + expected := fields.With("a", 1).With("b", 2) + instance := &event{fields: fields.With("a", 1)} + + actual := instance.With("b", 2) + + assert.ToBeEqualUsing(t, expected, actual.(*event).fields, fields.AreEqual) +} + +func Test_event_Withf(t *testing.T) { + expected := fields.With("a", 1).Withf("b", "%d", 2) + instance := &event{fields: fields.With("a", 1)} + + actual := instance.Withf("b", "%d", 2) + + assert.ToBeEqualUsing(t, expected, actual.(*event).fields, fields.AreEqual) +} + +func Test_event_WithError(t *testing.T) { + givenError := errors.New("expected") + expected := fields.With("a", 1).With("error", givenError) + instance := &event{ + fields: fields.With("a", 1), + provider: &Provider{}, + } + + actual := instance.WithError(givenError) + + assert.ToBeEqualUsing(t, expected, actual.(*event).fields, fields.AreEqual) +} + +func Test_event_WithAll(t *testing.T) { + givenMap := map[string]interface{}{ + "b": 2, + "c": 3, + } + expected := fields.With("a", 1).WithAll(givenMap) + instance := &event{fields: fields.With("a", 1)} + + actual := instance.WithAll(givenMap) + + assert.ToBeEqualUsing(t, expected, actual.(*event).fields, fields.AreEqual) +} + +func Test_event_Without(t *testing.T) { + expected := fields.With("b", 2).With("d", 4) + instance := &event{fields: fields. + With("a", 1). + With("b", 2). + With("c", 3). + With("d", 4)} + + actual := instance.Without("a", "c") + + assert.ToBeEqualUsing(t, expected, actual.(*event).fields, fields.AreEqual) +} diff --git a/sdk/testlog/hook.go b/sdk/testlog/hook.go new file mode 100644 index 0000000..f68a61e --- /dev/null +++ b/sdk/testlog/hook.go @@ -0,0 +1,29 @@ +package testlog + +import ( + "testing" + + log "github.com/echocat/slf4g" +) + +// Hook creates and registers for the given *testing.T, *testing.B or *testing.F +// a new instance of a log.Logger / log.Provider. +// +// The related Provider will be automatically cleanup at the end of the related +// test run (see testing.TB#Cleanup). +// +// customizer can be used to change the behavior of the managed Provider. +// +// The method returns the related Provider instance but while the test run it +// is also available via log.GetProvider(). +func Hook(tb testing.TB, customizer ...func(*Provider)) *Provider { + provider := NewProvider(tb, customizer...) + + previous := log.SetProvider(provider) + + tb.Cleanup(func() { + log.SetProvider(previous) + }) + + return provider +} diff --git a/sdk/testlog/hook_test.go b/sdk/testlog/hook_test.go new file mode 100644 index 0000000..cf639de --- /dev/null +++ b/sdk/testlog/hook_test.go @@ -0,0 +1,16 @@ +package testlog + +import ( + "testing" + + log "github.com/echocat/slf4g" +) + +func TestHook(t *testing.T) { + provider := Hook(t) + + log.Info("log.Info(..)") + + provider.GetRootLogger().Info("provider.GetRootLogger().Info(..)") + provider.GetLogger("foo").Info("provider.GetLogger(foo).Info(..)") +} diff --git a/sdk/testlog/level/formatter.go b/sdk/testlog/level/formatter.go new file mode 100644 index 0000000..892d33d --- /dev/null +++ b/sdk/testlog/level/formatter.go @@ -0,0 +1,36 @@ +package level + +import ( + "fmt" + + "github.com/echocat/slf4g/level" +) + +type Formatter interface { + Format(level.Level) string +} + +type FormatterFunc func(level.Level) string + +func (instance FormatterFunc) Format(v level.Level) string { + return instance(v) +} + +var DefaultFormatter Formatter = FormatterFunc(func(l level.Level) string { + switch l { + case level.Trace: + return "TRACE" + case level.Debug: + return "DEBUG" + case level.Info: + return " INFO" + case level.Warn: + return " WARN" + case level.Error: + return "ERROR" + case level.Fatal: + return "FATAL" + default: + return fmt.Sprintf("%5d", l) + } +}) diff --git a/sdk/testlog/level/formatter_test.go b/sdk/testlog/level/formatter_test.go new file mode 100644 index 0000000..a9181fd --- /dev/null +++ b/sdk/testlog/level/formatter_test.go @@ -0,0 +1,45 @@ +package level + +import ( + "fmt" + "strings" + "testing" + + "github.com/echocat/slf4g/internal/test/assert" + "github.com/echocat/slf4g/level" +) + +func Test_DefaultFormatter(t *testing.T) { + cases := []struct { + given level.Level + expected string + }{ + {level.Trace, "TRACE"}, + {level.Debug, "DEBUG"}, + {level.Info, " INFO"}, + {level.Warn, " WARN"}, + {level.Error, "ERROR"}, + {level.Fatal, "FATAL"}, + {level.Level(666), " 666"}, + } + + for _, c := range cases { + t.Run(strings.TrimSpace(c.expected), func(t *testing.T) { + actual := DefaultFormatter.Format(c.given) + + assert.ToBeEqual(t, c.expected, actual) + }) + } +} + +func Test_FormatterFunc_Format(t *testing.T) { + given := level.Level(666) + + instance := FormatterFunc(func(l level.Level) string { + return fmt.Sprintf("x%dx", l) + }) + + actual := instance.Format(given) + + assert.ToBeEqual(t, "x666x", actual) +} diff --git a/sdk/testlog/logger.go b/sdk/testlog/logger.go new file mode 100644 index 0000000..4368ea3 --- /dev/null +++ b/sdk/testlog/logger.go @@ -0,0 +1,26 @@ +package testlog + +import ( + "testing" + + log "github.com/echocat/slf4g" +) + +// NewLogger creates a new instance of log.Logger ready to use. If you want +// to use a direct instance of a logger, this is the easiest way to get it. +// +// This is a shortcut for NewProvider(..).GetRootLogger(). +func NewLogger(tb testing.TB, customizer ...func(*Provider)) log.Logger { + provider := NewProvider(tb, customizer...) + return provider.GetRootLogger() +} + +// NewNamedLogger creates a new instance of log.Logger ready to use. If you want +// to use a direct instance of a logger with a specific name, this is the +// easiest way to get it. +// +// This is a shortcut for NewProvider(..).GetLogger(...). +func NewNamedLogger(tb testing.TB, name string, customizer ...func(*Provider)) log.Logger { + provider := NewProvider(tb, customizer...) + return provider.GetLogger(name) +} diff --git a/sdk/testlog/logger_core.go b/sdk/testlog/logger_core.go new file mode 100644 index 0000000..7dda7a4 --- /dev/null +++ b/sdk/testlog/logger_core.go @@ -0,0 +1,207 @@ +package testlog + +import ( + "bytes" + "encoding/json" + "strconv" + "strings" + "time" + "unicode" + + "github.com/echocat/slf4g/fields" + + log "github.com/echocat/slf4g" + "github.com/echocat/slf4g/level" +) + +// RootLoggerName specifies the name of the root version of coreLogger +// instances which are managed by Provider. +const RootLoggerName = "ROOT" + +type coreLogger struct { + *Provider +} + +// Log implements log.CoreLogger#Log(event). +func (instance *coreLogger) Log(event log.Event, skipFrames uint16) { + instance.log(instance.GetName(), event, skipFrames+1) +} + +func (instance *coreLogger) logDepth(msg string, skipFrames uint16) { + i := instance.interceptLogDepth + if i == nil { + i = instance.logLogDepth + } + i(msg, skipFrames+1) +} + +func (instance *coreLogger) fail() { + i := instance.interceptFail + if i == nil { + i = instance.tb.Fail + } + i() +} + +func (instance *coreLogger) failNow() { + i := instance.interceptFailNow + if i == nil { + i = instance.tb.FailNow + } + i() +} + +func (instance *coreLogger) log(loggerName string, event log.Event, skipFrames uint16) { + l := event.GetLevel() + if !instance.IsLevelEnabled(l) { + return + } + + if v := log.GetLoggerOf(event, instance); v == nil { + event = event.With(instance.GetFieldKeysSpec().GetLogger(), loggerName) + } + + instance.logDepth(instance.format(event), skipFrames+1) + + failNowAtLevel := instance.getFailNowAtLevel() + if failNowAtLevel < NeverFailLevel && l >= failNowAtLevel { + instance.failNow() + return + } + + failAtLevel := instance.getFailAtLevel() + if failAtLevel < NeverFailLevel && l >= failAtLevel { + instance.fail() + return + } +} + +// IsLevelEnabled implements log.CoreLogger#IsLevelEnabled() +func (instance *coreLogger) IsLevelEnabled(v level.Level) bool { + return instance.GetLevel().CompareTo(v) <= 0 +} + +// GetName implements log.CoreLogger#GetName() +func (instance *coreLogger) GetName() string { + return RootLoggerName +} + +// GetProvider implements log.CoreLogger#GetProvider() +func (instance *coreLogger) GetProvider() log.Provider { + return instance.Provider +} + +func (instance *coreLogger) NewEvent(l level.Level, values map[string]interface{}) log.Event { + return instance.NewEventWithFields(l, fields.WithAll(values)) +} + +func (instance *coreLogger) NewEventWithFields(l level.Level, f fields.ForEachEnabled) log.Event { + asFields, err := fields.AsFields(f) + if err != nil { + panic(err) + } + return &event{ + provider: instance.Provider, + fields: asFields, + level: l, + } +} + +func (instance *coreLogger) Accepts(e log.Event) bool { + return e != nil +} + +func (instance *coreLogger) format(event log.Event) string { + buf := new(bytes.Buffer) + + _, _ = buf.WriteString(instance.formatTime(event)) + _, _ = buf.WriteString(instance.formatLevel(event.GetLevel())) + _, _ = buf.WriteString(instance.formatMessage(event)) + messageKey := instance.GetFieldKeysSpec().GetMessage() + loggerKey := instance.GetFieldKeysSpec().GetLogger() + timestampKey := instance.GetFieldKeysSpec().GetTimestamp() + if err := fields.SortedForEach(event, nil, func(k string, vp interface{}) error { + if vl, ok := vp.(fields.Filtered); ok { + fv, shouldBeRespected := vl.Filter(event) + if !shouldBeRespected { + return nil + } + vp = fv + } else if vl, ok := vp.(fields.Lazy); ok { + vp = vl.Get() + } + if vp == fields.Exclude { + return nil + } + + if k == loggerKey && vp == RootLoggerName { + return nil + } + if k == messageKey || k == timestampKey { + return nil + } + v, err := instance.formatValue(vp) + if err != nil { + return err + } + + _ = buf.WriteByte(' ') + _, _ = buf.WriteString(k) + _ = buf.WriteByte('=') + _, _ = buf.Write(v) + return nil + }); err != nil { + instance.tb.Fatalf("ERR!! Cannot format event %v: %v", event, err) + return "" + } + + return buf.String() +} + +func (instance *coreLogger) formatLevel(l level.Level) string { + return "[" + instance.getLevelFormatter().Format(l) + "]" +} + +func (instance *coreLogger) formatTime(event log.Event) string { + tf := instance.getTimeFormat() + if tf == NoopTimeFormat { + return "" + } + + if tf == SinceTestStartedMcsTimeFormat { + diff := runtimeNano() - instance.startedNs + return strconv.FormatInt(diff/1000, 10) + " " + } + + if v := log.GetTimestampOf(event, instance); v != nil { + return v.Format(tf) + " " + } + return time.Now().Format(tf) + " " +} + +func (instance *coreLogger) formatMessage(event log.Event) string { + var message string + if v := log.GetMessageOf(event, instance); v != nil { + message = *v + + message = strings.TrimLeftFunc(message, func(r rune) bool { + return r == '\r' || r == '\n' + }) + message = strings.TrimRightFunc(message, unicode.IsSpace) + message = strings.TrimFunc(message, func(r rune) bool { + return r == '\r' || !unicode.IsGraphic(r) + }) + message = strings.ReplaceAll(message, "\n", "\u23CE") + if message != "" { + message = " " + message + } + } + return message +} + +func (instance *coreLogger) formatValue(v interface{}) ([]byte, error) { + if ve, ok := v.(error); ok { + v = ve.Error() + } + return json.Marshal(v) +} diff --git a/sdk/testlog/logger_core_compat.go b/sdk/testlog/logger_core_compat.go new file mode 100644 index 0000000..d7704bb --- /dev/null +++ b/sdk/testlog/logger_core_compat.go @@ -0,0 +1,7 @@ +//go:build slf4gcompat + +package testlog + +func (instance *coreLogger) logLogDepth(str string, _ uint16) { + instance.tb.Log(str) +} diff --git a/sdk/testlog/logger_core_test.go b/sdk/testlog/logger_core_test.go new file mode 100644 index 0000000..9a2363a --- /dev/null +++ b/sdk/testlog/logger_core_test.go @@ -0,0 +1,202 @@ +package testlog + +import ( + "errors" + "testing" + "time" + + "github.com/echocat/slf4g/fields" + "github.com/echocat/slf4g/internal/test/assert" + "github.com/echocat/slf4g/level" +) + +const ( + dateTimeFormat = "2006-01-02 15:04:05" +) + +func Test_coreLogger_Log_regular(t *testing.T) { + provider := NewProvider(t) + provider.initIfRequired() + instance := provider.coreLogger + + var actualMsg string + var actualskipFrames uint16 + instance.interceptLogDepth = func(msg string, skipFrames uint16) { + actualMsg = msg + actualskipFrames = skipFrames + } + + provider.GetRootLogger(). + WithError(errors.New("testError")). + With("stringField", "bar"). + With("intField", 123). + With("lazyField", fields.LazyFunc(func() interface{} { return "lazy" })). + With("nilField", nil). + With("excludedField", fields.Exclude). + With("ignoredByLevelField", fields.IgnoreLevels(level.Debug, level.Info+1, "ignored")). + With("respectedByLevelField", fields.IgnoreLevels(level.Warn, level.Error, "respected")). + Info("foo") + + assert.ToBeMatching(t, `^\d+ \[ INFO] foo error="testError" intField=123 lazyField="lazy" nilField=null respectedByLevelField="respected" stringField="bar"$`, actualMsg) + assert.ToBeEqual(t, uint16(6), actualskipFrames) +} + +func Test_coreLogger_NewEvent(t *testing.T) { + provider := NewProvider(t) + provider.initIfRequired() + instance := provider.coreLogger + + actual := instance.NewEvent(level.Level(666), map[string]interface{}{ + "foo": 123, + "bar": "str", + }) + + assert.ToBeEqual(t, &event{ + provider, + fields.WithAll(map[string]interface{}{ + "foo": 123, + "bar": "str", + }), + 666, + }, actual) +} + +func Test_coreLogger_Accepts(t *testing.T) { + provider := NewProvider(t, Level(level.Level(700))) + provider.initIfRequired() + instance := provider.coreLogger + + givenAcceptable := instance.NewEvent(level.Level(666), map[string]interface{}{}) + + assert.ToBeEqual(t, true, instance.Accepts(givenAcceptable)) +} + +func Test_coreLogger_Log_tooLowLevel(t *testing.T) { + provider := NewProvider(t) + provider.initIfRequired() + instance := provider.coreLogger + + var actualMsg string + var actualskipFrames uint16 + instance.interceptLogDepth = func(msg string, skipFrames uint16) { + actualMsg = msg + actualskipFrames = skipFrames + } + + provider.GetRootLogger().Trace("foo") + + assert.ToBeEqual(t, "", actualMsg) + assert.ToBeEqual(t, uint16(0), actualskipFrames) +} + +func Test_coreLogger_Log_fail(t *testing.T) { + provider := NewProvider(t) + provider.initIfRequired() + instance := provider.coreLogger + + var actualMsg string + var actualskipFrames uint16 + instance.interceptLogDepth = func(msg string, skipFrames uint16) { + actualMsg = msg + actualskipFrames = skipFrames + } + var actualFail bool + instance.interceptFail = func() { + actualFail = true + } + var actualFailNow bool + instance.interceptFailNow = func() { + actualFailNow = true + } + + provider.GetRootLogger().Error("foo") + + assert.ToBeMatching(t, `^\d+ \[ERROR] foo$`, actualMsg) + assert.ToBeEqual(t, uint16(6), actualskipFrames) + assert.ToBeEqual(t, true, actualFail) + assert.ToBeEqual(t, false, actualFailNow) +} + +func Test_coreLogger_Log_failNow(t *testing.T) { + provider := NewProvider(t) + provider.initIfRequired() + instance := provider.coreLogger + + var actualMsg string + var actualskipFrames uint16 + instance.interceptLogDepth = func(msg string, skipFrames uint16) { + actualMsg = msg + actualskipFrames = skipFrames + } + var actualFail bool + instance.interceptFailNow = func() { + actualFail = true + } + var actualFailNow bool + instance.interceptFailNow = func() { + actualFailNow = true + } + + provider.GetRootLogger().Fatal("foo") + + assert.ToBeMatching(t, `^\d+ \[FATAL] foo$`, actualMsg) + assert.ToBeEqual(t, uint16(6), actualskipFrames) + assert.ToBeEqual(t, false, actualFail) + assert.ToBeEqual(t, true, actualFailNow) +} + +func Test_coreLogger_formatTime_sinceTestStartedMcs(t *testing.T) { + provider := NewProvider(t, TimeFormat(SinceTestStartedMcsTimeFormat)) + provider.initIfRequired() + instance := provider.coreLogger + + givenTs, _ := time.Parse(dateTimeFormat, "2024-07-25 18:56:13") + givenEvent := instance.NewEvent(level.Info, map[string]interface{}{ + "timestamp": givenTs, + }) + + assert.ToBeMatching(t, `^\d+ $`, instance.formatTime(givenEvent)) +} + +func Test_coreLogger_formatTime_noop(t *testing.T) { + provider := NewProvider(t, TimeFormat(NoopTimeFormat)) + provider.initIfRequired() + instance := provider.coreLogger + + givenTs, _ := time.Parse(dateTimeFormat, "2024-07-25 18:56:13") + givenEvent := instance.NewEvent(level.Info, map[string]interface{}{ + "timestamp": givenTs, + }) + + assert.ToBeEqual(t, "", instance.formatTime(givenEvent)) +} + +func Test_coreLogger_formatTime_ts(t *testing.T) { + provider := NewProvider(t, TimeFormat(dateTimeFormat)) + provider.initIfRequired() + instance := provider.coreLogger + + givenTs, _ := time.Parse(dateTimeFormat, "2024-07-25 18:56:13") + givenEvent := instance.NewEvent(level.Info, map[string]interface{}{ + "timestamp": givenTs, + }) + + assert.ToBeEqual(t, "2024-07-25 18:56:13 ", instance.formatTime(givenEvent)) +} + +func Test_coreLogger_formatTime_ts_defaultNow(t *testing.T) { + provider := NewProvider(t, TimeFormat(time.RFC3339)) + provider.initIfRequired() + instance := provider.coreLogger + + givenEvent := instance.NewEvent(level.Info, map[string]interface{}{}) + + now := time.Now() + actualTs, actualErr := time.Parse(time.RFC3339+" ", instance.formatTime(givenEvent)) + assert.ToBeNoError(t, actualErr) + + diff := now.Sub(actualTs) + if diff > time.Second*10 || diff < -time.Second*10 { + t.Fatalf("the difference between %v(now) and %v(ts) should not be greather than 10s; bug was: %v", now, actualTs, diff) + } +} diff --git a/sdk/testlog/logger_core_with_depth.go b/sdk/testlog/logger_core_with_depth.go new file mode 100644 index 0000000..5428e47 --- /dev/null +++ b/sdk/testlog/logger_core_with_depth.go @@ -0,0 +1,18 @@ +//go:build !slf4gcompat + +package testlog + +import ( + "testing" + "unsafe" +) + +type testCommonT unsafe.Pointer + +//go:linkname testingCommonLogDepth testing.(*common).logDepth +//goland:noinspection GoUnusedParameter +func testingCommonLogDepth(c testCommonT, s string, depth int) + +func (instance *coreLogger) logLogDepth(str string, skipFrames uint16) { + testingCommonLogDepth(testCommonT(instance.tb.(*testing.T)), str+"\n", int(skipFrames+2)) +} diff --git a/sdk/testlog/logger_renamed.go b/sdk/testlog/logger_renamed.go new file mode 100644 index 0000000..60cc520 --- /dev/null +++ b/sdk/testlog/logger_renamed.go @@ -0,0 +1,18 @@ +package testlog + +import log "github.com/echocat/slf4g" + +type coreLoggerRenamed struct { + *coreLogger + name string +} + +// Log implements log.CoreLogger#Log(event). +func (instance *coreLoggerRenamed) Log(event log.Event, skipFrames uint16) { + instance.log(instance.name, event, skipFrames+1) +} + +// GetName implements log.CoreLogger#GetName(). +func (instance *coreLoggerRenamed) GetName() string { + return instance.name +} diff --git a/sdk/testlog/logger_test.go b/sdk/testlog/logger_test.go new file mode 100644 index 0000000..b2a9a10 --- /dev/null +++ b/sdk/testlog/logger_test.go @@ -0,0 +1,30 @@ +package testlog + +import ( + "testing" + + "github.com/echocat/slf4g/level" + + log "github.com/echocat/slf4g" + "github.com/echocat/slf4g/internal/test/assert" +) + +func TestNewLogger(t *testing.T) { + instance := NewLogger(t, Level(666)) + + actualCoreLogger := log.UnwrapCoreLogger(instance) + assert.ToBeOfType(t, &coreLogger{}, actualCoreLogger) + + assert.ToBeEqual(t, RootLoggerName, actualCoreLogger.GetName()) + assert.ToBeEqual(t, level.Level(666), instance.GetProvider().(*Provider).GetLevel()) +} + +func TestNewNamedLogger(t *testing.T) { + instance := NewNamedLogger(t, "foo", Level(666)) + + actualCoreLogger := log.UnwrapCoreLogger(instance) + assert.ToBeOfType(t, &coreLoggerRenamed{}, actualCoreLogger) + + assert.ToBeEqual(t, "foo", actualCoreLogger.GetName()) + assert.ToBeEqual(t, level.Level(666), instance.GetProvider().(*Provider).GetLevel()) +} diff --git a/sdk/testlog/provider.go b/sdk/testlog/provider.go new file mode 100644 index 0000000..9990d67 --- /dev/null +++ b/sdk/testlog/provider.go @@ -0,0 +1,254 @@ +package testlog + +import ( + "sync" + "testing" + _ "unsafe" + + log "github.com/echocat/slf4g" + "github.com/echocat/slf4g/fields" + "github.com/echocat/slf4g/level" + tlevel "github.com/echocat/slf4g/sdk/testlog/level" +) + +// NewProvider creates a new instance of Provider which is ready to use. +// +// tb should hold an instance of either *testing.T, *testing.B or *testing.F. +// +// customizer can be used to change the behavior of the Provider: +// - Level +// - FailAtLevel +// - FailNowAtLevel +// - TimeFormat +// - LevelFormatter +// - Name +// - AllLevels +// - FieldKeysSpec +func NewProvider(tb testing.TB, customizer ...func(*Provider)) *Provider { + result := &Provider{tb: tb, startedNs: runtimeNano()} + + for _, c := range customizer { + c(result) + } + + return result +} + +const ( + // DefaultLevel specifies the default level.Level of an instance of Provider + // which be used if no other level was defined. + DefaultLevel = level.Debug + + // NeverFailLevel is used for Provider.FailAtLevel and indicates that regardless + // at which level each log.Event is logged, this event will never lead to a fail of + // the tests. + NeverFailLevel = level.Level(65535) + + // NoopTimeFormat tells the Provider to not print any timestamp in the log messages. + NoopTimeFormat = "" + + // SinceTestStartedMcsTimeFormat tells the Provider to print only the microseconds since + // the test started (is based on Hook() and/or NewProvider()). + SinceTestStartedMcsTimeFormat = "" +) + +var ( + // DefaultFailAtLevel is used if FailAtLevel was not used. + DefaultFailAtLevel = level.Error + + // DefaultFailNowAtLevel is used if FailNowAtLevel was not used. + DefaultFailNowAtLevel = level.Fatal + + // DefaultTimeFormat is used if TimeFormat was not used. + DefaultTimeFormat = SinceTestStartedMcsTimeFormat +) + +// Provider is an implementation of log.Provider which ensures that everything is +// logged using testing.TB#Log(). Use NewProvider(..) to get a new instance. +type Provider struct { + tb testing.TB + startedNs int64 + + name string + level level.Level + allLevels level.Levels + fieldKeysSpec fields.KeysSpec + failAtLevel level.Level + failNowAtLevel level.Level + timeFormat string + levelFormatter tlevel.Formatter + + coreLogger *coreLogger + logger log.Logger + initLogger sync.Once + + // For testing only + interceptLogDepth func(string, uint16) + interceptFail func() + interceptFailNow func() +} + +//go:linkname runtimeNano runtime.nanotime +func runtimeNano() int64 + +func (instance *Provider) initIfRequired() { + instance.initLogger.Do(func() { + instance.coreLogger = &coreLogger{instance} + instance.logger = log.NewLogger(instance.coreLogger) + }) +} + +// GetRootLogger implements log.Provider#GetRootLogger() +func (instance *Provider) GetRootLogger() log.Logger { + instance.initIfRequired() + return instance.logger +} + +// GetLogger implements log.Provider#GetLogger() +func (instance *Provider) GetLogger(name string) log.Logger { + if name == RootLoggerName { + return instance.GetRootLogger() + } + + instance.initIfRequired() + return log.NewLogger(&coreLoggerRenamed{instance.coreLogger, name}) +} + +// GetName implements log.Provider#GetName() +func (instance *Provider) GetName() string { + if v := instance.name; v != "" { + return v + } + return instance.tb.Name() +} + +// GetAllLevels implements log.Provider#GetAllLevels() +func (instance *Provider) GetAllLevels() level.Levels { + if v := instance.allLevels; v != nil { + return v + } + return level.GetProvider().GetLevels() +} + +// GetFieldKeysSpec implements log.Provider#GetFieldKeysSpec() +func (instance *Provider) GetFieldKeysSpec() fields.KeysSpec { + if v := instance.fieldKeysSpec; v != nil { + return v + } + return &fields.KeysSpecImpl{} +} + +// GetLevel returns the current level.Level where this log.Provider is set to. +func (instance *Provider) GetLevel() level.Level { + if v := instance.level; v != 0 { + return v + } + return DefaultLevel +} + +// SetLevel changes the current level.Level of this log.Provider. If set to +// 0 it will force this Provider to use DefaultLevel. +func (instance *Provider) SetLevel(v level.Level) { + instance.level = v +} + +func (instance *Provider) getFailAtLevel() level.Level { + if v := instance.failAtLevel; v != 0 { + return v + } + return DefaultFailAtLevel +} + +func (instance *Provider) getFailNowAtLevel() level.Level { + if v := instance.failNowAtLevel; v != 0 { + return v + } + return DefaultFailNowAtLevel +} + +func (instance *Provider) getTimeFormat() string { + if v := instance.timeFormat; v != "" { + return v + } + return DefaultTimeFormat +} + +func (instance *Provider) getLevelFormatter() tlevel.Formatter { + if v := instance.levelFormatter; v != nil { + return v + } + return tlevel.DefaultFormatter +} + +// Level specifies the level of the Provider which will be also inherited +// by all of its loggers. By default, the Provider will use DefaultLevel. +func Level(v level.Level) func(*Provider) { + return func(provider *Provider) { + provider.level = v + } +} + +// FailAtLevel defines a level.Level at which log.Event will lead to a test failure +// after each code of the test has passed (in contrast to FailNowAtLevel which will +// fail immediately) if they're logged with this a log.Logger handled by the +// Provider. If set to NeverFailLevel nothing happens. By default, the Provider +// will use DefaultFailAtLevel. +func FailAtLevel(v level.Level) func(*Provider) { + return func(provider *Provider) { + provider.failAtLevel = v + } +} + +// FailNowAtLevel defines a level.Level at which log.Event will lead to a test +// fails immediately (in contrast to FailAtLevel which allows to test to finish) +// if they're logged with this a log.Logger handled by the Provider. If set to +// NeverFailLevel nothing happens. By default, the Provider will use +// DefaultFailNowAtLevel. +func FailNowAtLevel(v level.Level) func(*Provider) { + return func(provider *Provider) { + provider.failNowAtLevel = v + } +} + +// TimeFormat defines how each entry will be formatted on print. If NoopTimeFormat +// is used, nothing will be printed. If SinceTestStartedMcsTimeFormat will be used +// no time is printed, but the microseconds since the test started. By default, the +// Provider will use DefaultTimeFormat. See time.Layout for more details. +func TimeFormat(v string) func(*Provider) { + return func(provider *Provider) { + provider.timeFormat = v + } +} + +// LevelFormatter formats the levels on printing. By default, the Provider will use +// level.DefaultFormatter. +func LevelFormatter(v tlevel.Formatter) func(*Provider) { + return func(provider *Provider) { + provider.levelFormatter = v + } +} + +// Name specifies the name of the Provider. By default, the Provider will use +// testing.TB#Name(). +func Name(v string) func(*Provider) { + return func(provider *Provider) { + provider.name = v + } +} + +// AllLevels specifies the levels which are supported by the Provider and all +// of its loggers. By default, the Provider will use level.GetProvider()#GetLevels(). +func AllLevels(v level.Levels) func(*Provider) { + return func(provider *Provider) { + provider.allLevels = v + } +} + +// FieldKeysSpec specifies the spec of the fields are supported by the Provider +// and all of its loggers. By default, the Provider will use the default instance +// of fields.KeysSpecImpl. +func FieldKeysSpec(v fields.KeysSpec) func(*Provider) { + return func(provider *Provider) { + provider.fieldKeysSpec = v + } +} diff --git a/sdk/testlog/provider_test.go b/sdk/testlog/provider_test.go new file mode 100644 index 0000000..02642c4 --- /dev/null +++ b/sdk/testlog/provider_test.go @@ -0,0 +1,171 @@ +package testlog + +import ( + "testing" + + log "github.com/echocat/slf4g" + + tlevel "github.com/echocat/slf4g/sdk/testlog/level" + + "github.com/echocat/slf4g/fields" + + "github.com/echocat/slf4g/level" + + "github.com/echocat/slf4g/internal/test/assert" +) + +func TestProvider_GetName_default(t *testing.T) { + instance := NewProvider(t) + assert.ToBeEqual(t, t.Name(), instance.GetName()) +} + +func TestProvider_GetName_specific(t *testing.T) { + instance := NewProvider(t, Name("foo")) + assert.ToBeEqual(t, "foo", instance.GetName()) +} + +func TestProvider_GetAllLevels_default(t *testing.T) { + instance := NewProvider(t) + assert.ToBeEqual(t, level.GetProvider().GetLevels(), instance.GetAllLevels()) +} + +func TestProvider_GetAllLevels_specific(t *testing.T) { + given := level.Levels{level.Info, level.Level(666)} + instance := NewProvider(t, AllLevels(given)) + assert.ToBeEqual(t, given, instance.GetAllLevels()) +} + +func TestProvider_GetFieldKeysSpec_default(t *testing.T) { + instance := NewProvider(t) + assert.ToBeEqual(t, &fields.KeysSpecImpl{}, instance.GetFieldKeysSpec()) +} + +func TestProvider_GetFieldKeysSpec_specific(t *testing.T) { + given := &mockFieldKeysSpec{} + instance := NewProvider(t, FieldKeysSpec(given)) + assert.ToBeEqual(t, given, instance.GetFieldKeysSpec()) +} + +func TestProvider_GetLevel_default(t *testing.T) { + instance := NewProvider(t) + assert.ToBeEqual(t, DefaultLevel, instance.GetLevel()) +} + +func TestProvider_GetLevel_specific(t *testing.T) { + given := level.Level(666) + instance := NewProvider(t, Level(given)) + assert.ToBeEqual(t, given, instance.GetLevel()) +} + +func TestProvider_SetLevel(t *testing.T) { + given := level.Level(666) + + instance := NewProvider(t) + assert.ToBeEqual(t, DefaultLevel, instance.GetLevel()) + + instance.SetLevel(given) + assert.ToBeEqual(t, given, instance.GetLevel()) +} + +func TestProvider_getFailAtLevel_default(t *testing.T) { + instance := NewProvider(t) + assert.ToBeEqual(t, DefaultFailAtLevel, instance.getFailAtLevel()) +} + +func TestProvider_getFailAtLevel_specific(t *testing.T) { + given := level.Level(666) + instance := NewProvider(t, FailAtLevel(given)) + assert.ToBeEqual(t, given, instance.getFailAtLevel()) +} + +func TestProvider_getFailNowAtLevel_default(t *testing.T) { + instance := NewProvider(t) + assert.ToBeEqual(t, DefaultFailNowAtLevel, instance.getFailNowAtLevel()) +} + +func TestProvider_getFailNowAtLevel_specific(t *testing.T) { + given := level.Level(666) + instance := NewProvider(t, FailNowAtLevel(given)) + assert.ToBeEqual(t, given, instance.getFailNowAtLevel()) +} + +func TestProvider_getTimeFormat_default(t *testing.T) { + instance := NewProvider(t) + assert.ToBeEqual(t, DefaultTimeFormat, instance.getTimeFormat()) +} + +func TestProvider_getTimeFormat_specific(t *testing.T) { + given := "15:04:05" + instance := NewProvider(t, TimeFormat(given)) + assert.ToBeEqual(t, given, instance.getTimeFormat()) +} + +func TestProvider_getLevelFormatter_default(t *testing.T) { + instance := NewProvider(t) + assert.ToBeEqual(t, tlevel.DefaultFormatter, instance.getLevelFormatter()) +} + +func TestProvider_getLevelFormatter_specific(t *testing.T) { + given := tlevel.FormatterFunc(nil) + instance := NewProvider(t, LevelFormatter(given)) + assert.ToBeEqual(t, given, instance.getLevelFormatter()) +} + +func TestProvider_GetRootLogger(t *testing.T) { + instance := NewProvider(t) + + actual := instance.GetRootLogger() + assert.ToBeNotNil(t, actual) + + actualCoreLogger := log.UnwrapCoreLogger(actual) + assert.ToBeOfType(t, &coreLogger{}, actualCoreLogger) + + assert.ToBeEqual(t, RootLoggerName, actualCoreLogger.GetName()) +} + +func TestProvider_GetLogger(t *testing.T) { + instance := NewProvider(t) + + actualRootLogger := instance.GetRootLogger() + assert.ToBeNotNil(t, actualRootLogger) + + actualRootCoreLogger := log.UnwrapCoreLogger(actualRootLogger) + assert.ToBeOfType(t, &coreLogger{}, actualRootCoreLogger) + + actual := instance.GetLogger("foo") + assert.ToBeNotNil(t, actualRootLogger) + + actualCoreLogger := log.UnwrapCoreLogger(actual) + assert.ToBeOfType(t, &coreLoggerRenamed{}, actualCoreLogger) + + assert.ToBeEqual(t, "foo", actualCoreLogger.GetName()) + assert.ToBeSame(t, actualRootCoreLogger, actualCoreLogger.(*coreLoggerRenamed).coreLogger) +} + +func TestProvider_GetLogger_rootLogger(t *testing.T) { + instance := NewProvider(t) + + actualRootLogger := instance.GetRootLogger() + assert.ToBeNotNil(t, actualRootLogger) + + actual := instance.GetLogger(RootLoggerName) + assert.ToBeSame(t, actualRootLogger, actual) +} + +type mockFieldKeysSpec struct{} + +func (instance *mockFieldKeysSpec) GetTimestamp() string { + panic("not implemented in tests") +} + +func (instance *mockFieldKeysSpec) GetMessage() string { + panic("not implemented in tests") +} + +func (instance *mockFieldKeysSpec) GetError() string { + panic("not implemented in tests") +} + +func (instance *mockFieldKeysSpec) GetLogger() string { + panic("not implemented in tests") +} diff --git a/testing/recording/logger_core.go b/testing/recording/logger_core.go index 92ccd67..b209d81 100644 --- a/testing/recording/logger_core.go +++ b/testing/recording/logger_core.go @@ -18,7 +18,7 @@ const RootLoggerName = "ROOT" // CoreLogger implements log.CoreLogger and records simply every logged event. // Each of these events can be received using GetAll() or Get(index). // -// Mutation +// # Mutation // // This log.CoreLogger will mutate the recorded instances of log.Event if // one of the following fields is absent: logger, timestamp. This behavior can