Skip to content

Commit

Permalink
add tests for xlog [wip]
Browse files Browse the repository at this point in the history
  • Loading branch information
dmke committed Oct 31, 2024
1 parent d680742 commit 7ef0add
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 11 deletions.
1 change: 1 addition & 0 deletions service/middleware/logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func TestLogging(t *testing.T) {
log, err := xlog.New(
xlog.AsText(),
xlog.WriteTo(&buf),
xlog.WithSource(),
xlog.MockClock(time.Unix(1650000000, 0).UTC()),
)
require.NoError(t, err)
Expand Down
1 change: 1 addition & 0 deletions service/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func TestHandleStatus_withFailIO(t *testing.T) {
log, err := xlog.New(
xlog.AsText(),
xlog.WriteTo(&buf),
xlog.WithSource(),
xlog.MockClock(time.Unix(1650000000, 0).UTC()),
)
require.NoError(t, err)
Expand Down
4 changes: 2 additions & 2 deletions xlog/attrs.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ func (err ErrorValue) Value() slog.Value {

// LogValue implements [slog.LogValuer].
func (err ErrorValue) LogValue() slog.Value {
return slog.StringValue(err.Error())
return err.Value()
}

// Error constructs a first-class error log attribute.
//
// Not to be confused with (xlog.Logger).Error() or (log/slog).Error(),
// which produce an error-level log message.
func Error(err error) slog.Attr {
return slog.Any(ErrorKey, ErrorValue{err})
return Any(ErrorKey, ErrorValue{err})
}
11 changes: 11 additions & 0 deletions xlog/discard.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package xlog

import (
"context"
"log/slog"
"os"
)
Expand All @@ -16,9 +17,19 @@ func NewDiscard() Logger {
return &discard{}
}

var (
_ Logger = (*discard)(nil)
_ slog.Handler = (*discard)(nil)
)

func (*discard) Debug(string, ...slog.Attr) {}
func (*discard) Info(string, ...slog.Attr) {}
func (*discard) Warn(string, ...slog.Attr) {}
func (*discard) Error(string, ...slog.Attr) {}
func (*discard) Fatal(string, ...slog.Attr) { os.Exit(1) }
func (d *discard) With(...slog.Attr) Logger { return d }

func (*discard) Enabled(context.Context, slog.Level) bool { return false }
func (*discard) Handle(context.Context, slog.Record) error { return nil }
func (d *discard) WithAttrs(attrs []slog.Attr) slog.Handler { return d }
func (d *discard) WithGroup(name string) slog.Handler { return d }
16 changes: 14 additions & 2 deletions xlog/options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package xlog

import (
"errors"
"io"
"log/slog"
"os"
Expand Down Expand Up @@ -47,9 +48,15 @@ func LeveledString(s string) Option {
}
}

var ErrNilWriter = errors.New("invalid writer: nil")

// WriteTo sets the output.
func WriteTo(w io.Writer) Option {
return func(o *options) error {
if w == nil {
return ErrNilWriter
}

o.output = w
return nil
}
Expand Down Expand Up @@ -103,7 +110,9 @@ func AsText() Option {
opts := []slogor.OptionFn{
slogor.SetLevel(o.level.Level()),
slogor.SetTimeFormat("[15:04:05.000]"),
slogor.ShowSource(),
}
if o.source {
opts = append(opts, slogor.ShowSource())
}
if f, isFile := o.output.(*os.File); !isFile || !isatty.IsTerminal(f.Fd()) {
opts = append(opts, slogor.DisableColor())
Expand All @@ -118,7 +127,10 @@ func AsText() Option {
// constructor.
func Discard() Option {
return func(o *options) error {
o.discard = true
o.buildHandler = func(o *options) slog.Handler {
// the discard logger doesn't have any options
return &discard{}
}
return nil
}
}
139 changes: 139 additions & 0 deletions xlog/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package xlog

import (
"bytes"
"context"
"fmt"
"log/slog"
"runtime"
"testing"
"time"

"github.com/stretchr/testify/suite"
)

func TestOptionSuite(t *testing.T) {
suite.Run(t, new(OptionSuite))
}

type OptionSuite struct {
suite.Suite
options
}

func (o *OptionSuite) SetupTest() {
o.level = slog.LevelDebug
}

func (o *OptionSuite) TestLeveled() {
err := Leveled(slog.LevelError)(&o.options)
o.Require().NoError(err)
o.Assert().Equal(slog.LevelError, o.level)

err = Leveled(slog.Level(999))(&o.options)
o.Require().NoError(err)
o.Assert().Equal(slog.Level(999), o.level)
}

func (o *OptionSuite) TestLeveledString_valid() {
for i, tt := range []struct {
input string
expected slog.Level
}{
{"dbg", slog.LevelDebug},
{"debug", slog.LevelDebug},

{"", slog.LevelInfo},
{"info", slog.LevelInfo},
{"INFO", slog.LevelInfo},

{"WARN", slog.LevelWarn},
{"WARNING", slog.LevelWarn},

{"ERR", slog.LevelError},
{"ERROR", slog.LevelError},
{"FaTaL", slog.LevelError},
} {
o.level = slog.Level(100 + i)
err := LeveledString(tt.input)(&o.options)
o.Require().NoError(err)
o.Assert().Equal(tt.expected, o.level)
}
}

func (o *OptionSuite) TestLeveledString_invalid() {
o.level = slog.Level(100)
err := LeveledString("ifno")(&o.options)
o.Require().Equal(slog.Level(100), o.level)
o.Assert().EqualError(err, `unknown log level: "ifno"`)
}

func (o *OptionSuite) TestWriteTo() {
var buf bytes.Buffer
err := WriteTo(&buf)(&o.options)
o.Require().NoError(err)
o.Assert().Equal(&buf, o.output)

err = WriteTo(nil)(&o.options)
o.Assert().EqualError(err, "invalid writer: nil")
}

func (o *OptionSuite) TestMockClock() {
t := time.Now()
err := MockClock(t)(&o.options)
o.Require().NoError(err)
o.Require().NotNil(o.clock)
o.Assert().EqualValues(t, o.clock.Now())
}

func (o *OptionSuite) TestWithSource() {
err := WithSource()(&o.options)
o.Require().NoError(err)
o.Assert().True(o.source)
}

func (o *OptionSuite) TestColor() {
err := Color()(&o.options)
o.Require().NoError(err)
o.Assert().True(o.color)
}

func (o *OptionSuite) testBuildHandler(expectedLog string, opts ...Option) {
o.T().Helper()

var buf bytes.Buffer
o.output = &buf

for _, opt := range opts {
o.Require().NoError(opt(&o.options))
}
o.Require().NotNil(o.buildHandler)
h := o.buildHandler(&o.options)
o.Require().NotNil(h)

o.Assert().False(h.Enabled(context.Background(), -999))

pc, _, _, ok := runtime.Caller(1)
o.Require().True(ok)
h.Handle(context.Background(), slog.NewRecord(

Check failure on line 118 in xlog/options_test.go

View workflow job for this annotation

GitHub Actions / Run linter

Error return value of `h.Handle` is not checked (errcheck)
time.Time{}, slog.LevelInfo, "test", pc,
))
o.Assert().EqualValues(expectedLog, buf.String())
}

func (o *OptionSuite) TestAsJSON() {
o.testBuildHandler(`{"level":"INFO","msg":"test"}`+"\n", AsJSON())
}

func (o *OptionSuite) TestAsText() {
o.testBuildHandler("INFO test\n", AsText())

_, _, line, ok := runtime.Caller(0)
o.Require().True(ok)
msg := fmt.Sprintf("INFO options_test.go:%d test\n", line+3)
o.testBuildHandler(msg, WithSource(), AsText())
}

func (o *OptionSuite) TestDiscard() {
o.testBuildHandler("", Discard())
}
15 changes: 8 additions & 7 deletions xlog/xlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ func New(opt ...Option) (Logger, error) {
}
}

// the discard logger doesn't require any further setup
if opts.discard {
return &discard{}, nil
}

// setup mock time
h := opts.buildHandler(&opts)

// skip a lot of overhead for the discard logger
if d, ok := h.(*discard); ok {
return d, nil
}

if opts.clock != nil {
h = &mockTimeHandler{
clock: opts.clock,
Expand Down Expand Up @@ -111,13 +112,13 @@ func (log *logger) With(a ...slog.Attr) Logger {
// "error" and "fatal". Other input will result in err not being nil.
func ParseLevel(s string) (l slog.Level, err error) {
switch strings.ToLower(s) {
case "debug":
case "dbg", "debug":
l = slog.LevelDebug
case "info", "": // make the zero value useful
l = slog.LevelInfo
case "warn", "warning":
l = slog.LevelWarn
case "error", "fatal":
case "err", "error", "fatal":
l = slog.LevelError
default:
err = fmt.Errorf("unknown log level: %q", s)
Expand Down

0 comments on commit 7ef0add

Please sign in to comment.