From a02a8ba4f9fc25ec2964767df015bf0f4366ddcb Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Thu, 26 Oct 2017 13:05:54 -0700 Subject: [PATCH] zaptest: Add testing.TB compatible logger This adds `zaptest.NewTestLogger(testing.TB) *zap.Logger` and a level-override variant of it. This will enable us to use non-nop loggers from tests without spamming the output. These loggers will not print any output unless the test failed or `go test -v` was used. --- zaptest/test_logger.go | 87 ++++++++++++++++++++++++++++++++ zaptest/test_logger_test.go | 99 +++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 zaptest/test_logger.go create mode 100644 zaptest/test_logger_test.go diff --git a/zaptest/test_logger.go b/zaptest/test_logger.go new file mode 100644 index 000000000..4c2ddab51 --- /dev/null +++ b/zaptest/test_logger.go @@ -0,0 +1,87 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zaptest + +import ( + "testing" + + "go.uber.org/zap/zapcore" +) + +// NewTestLogger builds a new Core that logs all messages to the given +// testing.TB. +// +// Use this with a *testing.T or *testing.B to get logs which get printed only +// if a test fails or if you ran go test -v. +// +// logger := zap.New(NewTestLogger(t)) +func NewTestLogger(t testing.TB) zapcore.Core { + return NewTestLoggerAt(t, zapcore.DebugLevel) +} + +// NewTestLoggerAt builds a new Core that logs messages to the given +// testing.TB if the given LevelEnabler allows it. +// +// Use this with a *testing.T or *testing.B to get logs which get printed only +// if a test fails or if you ran go test -v. +// +// logger := zap.New(NewTestLoggerAt(t, zap.InfoLevel)) +func NewTestLoggerAt(t testing.TB, enab zapcore.LevelEnabler) zapcore.Core { + return zapcore.NewCore( + zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + // EncoderConfig copied from zap.NewDevelopmentEncoderConfig. + // Can't use it directly because that would cause a cyclic import. + TimeKey: "T", + LevelKey: "L", + NameKey: "N", + CallerKey: "C", + MessageKey: "M", + StacktraceKey: "S", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }), + testingWriter{t}, + enab, + ) +} + +// testingWriter is a WriteSyncer that writes to the given testing.TB. +type testingWriter struct{ t testing.TB } + +func (w testingWriter) Write(p []byte) (n int, err error) { + s := string(p) + + // Strip trailing newline because t.Log always adds one. + if s[len(s)-1] == '\n' { + s = s[:len(s)-1] + } + + // Note: t.Log is safe for concurrent use. + w.t.Log(s) + return len(p), nil +} + +func (w testingWriter) Sync() error { + return nil +} diff --git a/zaptest/test_logger_test.go b/zaptest/test_logger_test.go new file mode 100644 index 000000000..26dfba96c --- /dev/null +++ b/zaptest/test_logger_test.go @@ -0,0 +1,99 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zaptest + +import ( + "errors" + "fmt" + "strings" + "sync" + "testing" + + "go.uber.org/zap" + + "github.com/stretchr/testify/assert" +) + +func TestTestLoggerIncludesDebug(t *testing.T) { + ts := newTestLogSpy(t) + log := zap.New(NewTestLogger(ts)) + log.Debug("calculating") + log.Info("finished calculating", zap.Int("answer", 42)) + + ts.AssertMessages( + "DEBUG calculating", + `INFO finished calculating {"answer": 42}`, + ) +} + +func TestTestLoggerAt(t *testing.T) { + ts := newTestLogSpy(t) + log := zap.New(NewTestLoggerAt(ts, zap.WarnLevel)) + + log.Info("received work order") + log.Debug("starting work") + log.Warn("work may fail") + log.Error("work failed", zap.Error(errors.New("great sadness"))) + + assert.Panics(t, func() { + log.Panic("failed to do work") + }, "log.Panic should panic") + + ts.AssertMessages( + "WARN work may fail", + `ERROR work failed {"error": "great sadness"}`, + "PANIC failed to do work", + ) +} + +// testLogSpy is a testing.TB that captures logged messages. +type testLogSpy struct { + testing.TB + + mu sync.Mutex + Messages []string +} + +func newTestLogSpy(t testing.TB) *testLogSpy { + return &testLogSpy{TB: t} +} + +func (t *testLogSpy) Log(args ...interface{}) { + // Log messages are in the format, + // + // 2017-10-27T13:03:01.000-0700 DEBUG your message here {data here} + // + // We strip the first part of these messages because we can't really test + // for the timestamp from these tests. + m := fmt.Sprint(args...) + m = m[strings.IndexByte(m, '\t')+1:] + + // t.Log should be thread-safe. + t.mu.Lock() + t.Messages = append(t.Messages, m) + t.mu.Unlock() + + t.TB.Log(args...) +} + +func (t *testLogSpy) AssertMessages(msgs ...string) { + assert.Equal(t, msgs, t.Messages) +}