Repository: uber-go/fx
Pull request #692 (2020-01-04 00:42:47)

log: Add shared Spy implementation
This consolidates the multiple Spy fx.Printer implementations into one.
The previous implementations operated by wrapping a Buffer and accessing
the result with `String()`. The methods used by us were,

    Reset()
    String() string

The new Spy implementation exposes both these methods, as well as a
`Messages() []string` method which exposes all logged messages
separately, which allows for a more accurate way to verify the logged
entries.

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/fx/pull/692?src=pr&el=h1) Report
> Merging [#692](https://codecov.io/gh/uber-go/fx/pull/692?src=pr&el=desc) into [master](https://codecov.io/gh/uber-go/fx/commit/1488d1d0fd845270c03c080771bdf19b196827fa?src=pr&el=desc) will **increase** coverage by `0.04%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/uber-go/fx/pull/692/graphs/tree.svg?width=650&token=PYY9PSfaC6&height=150&src=pr)](https://codecov.io/gh/uber-go/fx/pull/692?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #692      +/-   ##
=========================================
+ Coverage   98.06%   98.1%   +0.04%     
=========================================
  Files          14      15       +1     
  Lines         516     528      +12     
=========================================
+ Hits          506     518      +12     
  Misses          8       8              
  Partials        2       2
```


| [Impacted Files](https://codecov.io/gh/uber-go/fx/pull/692?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/fxlog/spy.go](https://codecov.io/gh/uber-go/fx/pull/692/diff?src=pr&el=tree#diff-aW50ZXJuYWwvZnhsb2cvc3B5Lmdv) | `100% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber-go/fx/pull/692?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/uber-go/fx/pull/692?src=pr&el=footer). Last update [1488d1d...21c8209](https://codecov.io/gh/uber-go/fx/pull/692?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/internal/fxlog/spy.go b/internal/fxlog/spy.go
new file mode 100644
index 0000000..839f9ed
--- /dev/null
+++ b/internal/fxlog/spy.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2020 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 fxlog
+
+import (
+	"fmt"
+	"strings"
+)
+
+// Spy is an Fx Printer that captures logged statements. It may be used in
+// tests of Fx logs.
+type Spy struct {
+	msgs []string
+}
+
+var _ Printer = &Spy{}
+
+// Printf logs the given message, formatting it in printf-style.
+func (s *Spy) Printf(msg string, args ...interface{}) {
+	s.msgs = append(s.msgs, fmt.Sprintf(msg, args...))
+}
+
+// Messages returns a copy of all captured messages.
+func (s *Spy) Messages() []string {
+	msgs := make([]string, len(s.msgs))
+	copy(msgs, s.msgs)
+	return msgs
+}
+
+// String returns all logged messages as a single newline-delimited string.
+func (s *Spy) String() string {
+	if len(s.msgs) == 0 {
+		return ""
+	}
+
+	// trailing newline because all log entries should have a newline
+	// after them.
+	return strings.Join(s.msgs, "\n") + "\n"
+}
+
+// Reset clears all messages from the Spy.
+func (s *Spy) Reset() {
+	s.msgs = s.msgs[:0]
+}


Test patch:
diff --git a/app_test.go b/app_test.go
index 665ce07..8682785 100644
--- a/app_test.go
+++ b/app_test.go
@@ -31,21 +31,13 @@ import (
 
 	. "go.uber.org/fx"
 	"go.uber.org/fx/fxtest"
+	"go.uber.org/fx/internal/fxlog"
 	"go.uber.org/multierr"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-type printerSpy struct {
-	*bytes.Buffer
-}
-
-func (ps printerSpy) Printf(format string, args ...interface{}) {
-	fmt.Fprintf(ps.Buffer, format, args...)
-	ps.Buffer.WriteRune('\n')
-}
-
 func NewForTest(t testing.TB, opts ...Option) *App {
 	testOpts := []Option{Logger(fxtest.NewTestPrinter(t))}
 	opts = append(testOpts, opts...)
@@ -71,7 +63,7 @@ func TestNewApp(t *testing.T) {
 		// after applying other options. This prevents the app configuration
 		// (e.g., logging) from changing halfway through our provides.
 
-		spy := &printerSpy{&bytes.Buffer{}}
+		spy := new(fxlog.Spy)
 		app := fxtest.New(t, Provide(func() struct{} { return struct{}{} }), Logger(spy))
 		defer app.RequireStart().RequireStop()
 		assert.Contains(t, spy.String(), "PROVIDE\tstruct {}")
@@ -405,7 +397,7 @@ func TestOptions(t *testing.T) {
 	})
 
 	t.Run("Error", func(t *testing.T) {
-		spy := printerSpy{&bytes.Buffer{}}
+		spy := new(fxlog.Spy)
 		New(
 			Provide(&bytes.Buffer{}), // error, not a constructor
 			Logger(spy),
@@ -520,7 +512,7 @@ func TestAppStart(t *testing.T) {
 	})
 
 	t.Run("InvokeNonFunction", func(t *testing.T) {
-		spy := printerSpy{&bytes.Buffer{}}
+		spy := new(fxlog.Spy)
 
 		app := New(Logger(spy), Invoke(struct{}{}))
 		err := app.Err()
@@ -680,7 +672,7 @@ func TestDone(t *testing.T) {
 }
 
 func TestReplaceLogger(t *testing.T) {
-	spy := printerSpy{&bytes.Buffer{}}
+	spy := new(fxlog.Spy)
 	app := fxtest.New(t, Logger(spy))
 	app.RequireStart().RequireStop()
 	assert.Contains(t, spy.String(), "RUNNING")
diff --git a/internal/fxlog/fxlog_test.go b/internal/fxlog/fxlog_test.go
index aab0d0e..da3a928 100644
--- a/internal/fxlog/fxlog_test.go
+++ b/internal/fxlog/fxlog_test.go
@@ -23,7 +23,6 @@ package fxlog
 import (
 	"bytes"
 	"errors"
-	"fmt"
 	"os"
 	"testing"
 
@@ -33,18 +32,6 @@ import (
 	sample "go.uber.org/fx/internal/fxlog/sample.git"
 )
 
-type spy struct {
-	*bytes.Buffer
-}
-
-func newSpy() *spy {
-	return &spy{bytes.NewBuffer(nil)}
-}
-
-func (s *spy) Printf(format string, is ...interface{}) {
-	fmt.Fprintln(s, fmt.Sprintf(format, is...))
-}
-
 // stubs the exit call, returns a function that restores a real exit function
 // and asserts that the stub was called.
 func stubExit() func(testing.TB) {
@@ -62,7 +49,7 @@ func TestNew(t *testing.T) {
 }
 
 func TestPrint(t *testing.T) {
-	sink := newSpy()
+	sink := new(Spy)
 	logger := &Logger{sink}
 
 	t.Run("printf", func(t *testing.T) {
@@ -158,14 +145,14 @@ func TestPrint(t *testing.T) {
 }
 
 func TestPanic(t *testing.T) {
-	sink := newSpy()
+	sink := new(Spy)
 	logger := &Logger{sink}
 	assert.Panics(t, func() { logger.Panic(errors.New("foo")) })
 	assert.Equal(t, "[Fx] foo\n", sink.String())
 }
 
 func TestFatal(t *testing.T) {
-	sink := newSpy()
+	sink := new(Spy)
 	logger := &Logger{sink}
 
 	undo := stubExit()
diff --git a/internal/fxlog/spy_test.go b/internal/fxlog/spy_test.go
new file mode 100644
index 0000000..72702d4
--- /dev/null
+++ b/internal/fxlog/spy_test.go
@@ -0,0 +1,64 @@
+// Copyright (c) 2020 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 fxlog
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSpy(t *testing.T) {
+	var s Spy
+
+	t.Run("empty spy", func(t *testing.T) {
+		assert.Empty(t, s.Messages(), "messages must be empty")
+		assert.Empty(t, s.String(), "string must be empty")
+	})
+
+	s.Printf("foo bar")
+	t.Run("unformatted message", func(t *testing.T) {
+		assert.Equal(t, []string{"foo bar"}, s.Messages(), "messages must match")
+		assert.Equal(t, "foo bar\n", s.String(), "string must match")
+	})
+
+	s.Printf("something went wrong: %v", errors.New("great sadness"))
+	t.Run("formatted message", func(t *testing.T) {
+		assert.Equal(t, []string{
+			"foo bar",
+			"something went wrong: great sadness",
+		}, s.Messages())
+		assert.Equal(t, "foo bar\nsomething went wrong: great sadness\n", s.String())
+	})
+
+	s.Reset()
+	t.Run("reset", func(t *testing.T) {
+		assert.Empty(t, s.Messages(), "messages must be empty")
+		assert.Empty(t, s.String(), "string must be empty")
+	})
+
+	s.Printf("baz qux")
+	t.Run("use after reset", func(t *testing.T) {
+		assert.Equal(t, []string{"baz qux"}, s.Messages(), "messages must match")
+		assert.Equal(t, "baz qux\n", s.String(), "string must match")
+	})
+}