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

zaptest/NewLogger: fail test on internal errors #566

Merged
merged 2 commits into from
Apr 9, 2018
Merged
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
30 changes: 28 additions & 2 deletions zaptest/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,39 @@ func NewLogger(t TestingT, opts ...LoggerOption) *zap.Logger {
o.applyLoggerOption(&cfg)
}

writer := newTestingWriter(t)
return zap.New(
zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
testingWriter{t},
writer,
cfg.Level,
),

// Send zap errors to the same writer and mark the test as failed if
// that happens.
zap.ErrorOutput(writer.WithMarkFailed(true)),
)
}

// testingWriter is a WriteSyncer that writes to the given testing.TB.
type testingWriter struct{ t TestingT }
type testingWriter struct {
t TestingT

// If true, the test will be marked as failed if this testingWriter is
// ever used.
markFailed bool
}

func newTestingWriter(t TestingT) testingWriter {
return testingWriter{t: t}
}

// WithMarkFailed returns a copy of this testingWriter with markFailed set to
// the provided value.
func (w testingWriter) WithMarkFailed(v bool) testingWriter {
w.markFailed = v
return w
}

func (w testingWriter) Write(p []byte) (n int, err error) {
n = len(p)
Expand All @@ -85,6 +107,10 @@ func (w testingWriter) Write(p []byte) (n int, err error) {

// Note: t.Log is safe for concurrent use.
w.t.Logf("%s", p)
if w.markFailed {
w.t.Fail()
}

return n, nil
}

Expand Down
69 changes: 65 additions & 4 deletions zaptest/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ import (
"testing"

"go.uber.org/zap"
"go.uber.org/zap/internal/ztest"
"go.uber.org/zap/zapcore"

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

func TestTestLogger(t *testing.T) {
ts := newTestLogSpy(t)
defer ts.AssertPassed()

log := NewLogger(ts)

log.Info("received work order")
Expand All @@ -57,6 +61,8 @@ func TestTestLogger(t *testing.T) {

func TestTestLoggerSupportsLevels(t *testing.T) {
ts := newTestLogSpy(t)
defer ts.AssertPassed()

log := NewLogger(ts, Level(zap.WarnLevel))

log.Info("received work order")
Expand All @@ -77,25 +83,69 @@ func TestTestLoggerSupportsLevels(t *testing.T) {

func TestTestingWriter(t *testing.T) {
ts := newTestLogSpy(t)
w := testingWriter{ts}
w := newTestingWriter(ts)

n, err := io.WriteString(w, "hello\n\n")
assert.NoError(t, err, "WriteString must not fail")
assert.Equal(t, 7, n)
}

func TestTestLoggerErrorOutput(t *testing.T) {
// This test verifies that the test logger logs internal messages to the
// testing.T and marks the test as failed.

ts := newTestLogSpy(t)
defer ts.AssertFailed()

log := NewLogger(ts)

// Replace with a core that fails.
log = log.WithOptions(zap.WrapCore(func(zapcore.Core) zapcore.Core {
return zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
zapcore.Lock(zapcore.AddSync(ztest.FailWriter{})),
zapcore.DebugLevel,
)
}))

log.Info("foo") // this fails

if assert.Len(t, ts.Messages, 1, "expected a log message") {
assert.Regexp(t, `write error: failed`, ts.Messages[0])
}
}

// testLogSpy is a testing.TB that captures logged messages.
type testLogSpy struct {
testing.TB

mu sync.Mutex
mu sync.RWMutex
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional/can follow up:
this spy is only used internally, and I don't think any of the tests are concurrent, so we might be able to just remove the mutex.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spy is thread safe only because testing.TB is supposed to be thread-
safe, but yeah, I'll drop it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failed bool
Messages []string
}

func newTestLogSpy(t testing.TB) *testLogSpy {
return &testLogSpy{TB: t}
}

func (t *testLogSpy) Fail() {
t.mu.Lock()
t.failed = true
t.mu.Unlock()
}

func (t *testLogSpy) Failed() bool {
t.mu.RLock()
failed := t.failed
t.mu.RUnlock()
return failed
}

func (t *testLogSpy) FailNow() {
t.Fail()
t.TB.FailNow()
}

func (t *testLogSpy) Logf(format string, args ...interface{}) {
// Log messages are in the format,
//
Expand All @@ -106,7 +156,6 @@ func (t *testLogSpy) Logf(format string, args ...interface{}) {
m := fmt.Sprintf(format, 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()
Expand All @@ -115,5 +164,17 @@ func (t *testLogSpy) Logf(format string, args ...interface{}) {
}

func (t *testLogSpy) AssertMessages(msgs ...string) {
assert.Equal(t, msgs, t.Messages)
assert.Equal(t.TB, msgs, t.Messages, "logged messages did not match")
}

func (t *testLogSpy) AssertPassed() {
t.assertFailed(false, "expected test to pass")
}

func (t *testLogSpy) AssertFailed() {
t.assertFailed(true, "expected test to fail")
}

func (t *testLogSpy) assertFailed(v bool, msg string) {
assert.Equal(t.TB, v, t.failed, msg)
}
9 changes: 9 additions & 0 deletions zaptest/testingt.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ type TestingT interface {
// Logs the given message and marks the test as failed.
Errorf(string, ...interface{})

// Marks the test as failed.
Fail()

// Returns true if the test has been marked as failed.
Failed() bool

// Returns the name of the test.
Name() string

// Marks the test as failed and stops execution of that test.
FailNow()
}
Expand Down