Repository: gavv/httpexpect (go)

Issue:
Implement PanicReporter
See #187 for details. PanicReporter will be similar to FatalReporter, but instead of calling t.Fatalf, it just calls panic.

This reporter may be useful in multithreaded tests because t.Fatal may be called only from test's goroutine. This should be mentioned in the comment.

Pull request:
PanicReporter implementation
Hello, this is my implementation to fix #248
Changes:
- add PanicReporter implementation
- add test coverage for PanicReporter
- edit docs to mention PanicReporter

Please let me know if this is not expected behavior or if anything needs to be added and I will make the appropriate changes.

Patch:
diff --git a/expect.go b/expect.go
index 44d73b9..3c0bafe 100644
--- a/expect.go
+++ b/expect.go
@@ -162,6 +162,8 @@ type Config struct {
 	//    (non-fatal / fatal failures using testify package)
 	//  - testing.T / FatalReporter
 	//    (non-fatal / fatal failures using standard testing package)
+	//  - PanicReporter
+	//    (failures that panic to be used in multithreaded tests)
 	//  - custom implementation
 	Reporter Reporter
 
@@ -285,6 +287,27 @@ type RequestFactory interface {
 	NewRequest(method, url string, body io.Reader) (*http.Request, error)
 }
 
+// RequestFactoryFunc is an adapter that allows a function
+// to be used as the RequestFactory
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		RequestFactory: httpextect.RequestFactoryFunc(
+//			func(method string, url string, body io.Reader) (*http.Request, error) {
+//				// factory code here
+//			}),
+//	})
+type RequestFactoryFunc func(
+	method string, url string, body io.Reader,
+) (*http.Request, error)
+
+func (f RequestFactoryFunc) NewRequest(
+	method string, url string, body io.Reader,
+) (*http.Request, error) {
+	return f(method, url, body)
+}
+
 // Client is used to send http.Request and receive http.Response.
 // http.Client implements this interface.
 //
@@ -303,6 +326,22 @@ type Client interface {
 	Do(*http.Request) (*http.Response, error)
 }
 
+// ClientFunc is an adapter that allows a function to be used as the Client
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		Client: httpextect.ClientFunc(
+//			func(req *http.Request) (*http.Response, error) {
+//				// client code here
+//			}),
+//	})
+type ClientFunc func(req *http.Request) (*http.Response, error)
+
+func (f ClientFunc) Do(req *http.Request) (*http.Response, error) {
+	return f(req)
+}
+
 // WebsocketDialer is used to establish websocket.Conn and receive http.Response
 // of handshake result.
 // websocket.Dialer implements this interface.
@@ -313,8 +352,7 @@ type Client interface {
 // Example:
 //
 //	e := httpexpect.WithConfig(httpexpect.Config{
-//	  BaseURL:         "http://example.com",
-//	  WebsocketDialer: httpexpect.NewWebsocketDialer(myHandler),
+//	    WebsocketDialer: httpexpect.NewWebsocketDialer(myHandler),
 //	})
 type WebsocketDialer interface {
 	// Dial establishes new Websocket connection and returns response
@@ -322,14 +360,51 @@ type WebsocketDialer interface {
 	Dial(url string, reqH http.Header) (*websocket.Conn, *http.Response, error)
 }
 
+// WebsocketDialerFunc is an adapter that allows a function
+// to be used as the WebsocketDialer
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		WebsocketDialer: httpextect.WebsocketDialerFunc(
+//			func(url string, reqH http.Header) (*websocket.Conn, *http.Response, error) {
+//				// dialer code here
+//			}),
+//	})
+type WebsocketDialerFunc func(
+	url string, reqH http.Header,
+) (*websocket.Conn, *http.Response, error)
+
+func (f WebsocketDialerFunc) Dial(
+	url string, reqH http.Header,
+) (*websocket.Conn, *http.Response, error) {
+	return f(url, reqH)
+}
+
 // Reporter is used to report failures.
-// *testing.T, FatalReporter, AssertReporter, RequireReporter implement it.
+// *testing.T, FatalReporter, AssertReporter, RequireReporter, PanicReporter implement it.
 type Reporter interface {
 	// Errorf reports failure.
 	// Allowed to return normally or terminate test using t.FailNow().
 	Errorf(message string, args ...interface{})
 }
 
+// ReporterFunc is an adapter that allows a function to be used as the Reporter
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		Reporter: httpextect.ReporterFunc(
+//			func(message string, args ...interface{}) {
+//				// reporter code here
+//			}),
+//	})
+type ReporterFunc func(message string, args ...interface{})
+
+func (f ReporterFunc) Errorf(message string, args ...interface{}) {
+	f(message, args)
+}
+
 // Logger is used as output backend for Printer.
 // *testing.T implements this interface.
 type Logger interface {
@@ -337,6 +412,25 @@ type Logger interface {
 	Logf(fmt string, args ...interface{})
 }
 
+// LoggerFunc is an adapter that allows a function to be used as the Logger
+//
+// Example:
+//
+//	e := httpexpect.WithConfig(httpexpect.Config{
+//		Printers: []httpexpect.Printer{
+//			httpexpect.NewCompactPrinter(
+//				httpextect.LoggerFunc(
+//					func(fmt string, args ...interface{}) {
+//						// logger code here
+//					})),
+//		},
+//	})
+type LoggerFunc func(fmt string, args ...interface{})
+
+func (f LoggerFunc) Logf(fmt string, args ...interface{}) {
+	f(fmt, args)
+}
+
 // TestingTB is a subset of testing.TB interface used by httpexpect.
 // You can use *testing.T or pass custom implementation.
 type TestingTB interface {
diff --git a/formatter.go b/formatter.go
index 4b5c7ce..2cf3986 100644
--- a/formatter.go
+++ b/formatter.go
@@ -760,6 +760,42 @@ var defaultTemplateFuncs = template.FuncMap{
 		}
 		return color.New(colorAttr).Sprint(s)
 	},
+	"colordiff": func(enable bool, s string) string {
+		if !enable {
+			return s
+		}
+
+		prefixColor := []struct {
+			prefix string
+			color  color.Attribute
+		}{
+			{"---", color.FgWhite},
+			{"+++", color.FgWhite},
+			{"-", color.FgRed},
+			{"+", color.FgGreen},
+		}
+
+		lineColor := func(s string) color.Attribute {
+			for _, pc := range prefixColor {
+				if strings.HasPrefix(s, pc.prefix) {
+					return pc.color
+				}
+			}
+
+			return color.Reset
+		}
+
+		var sb strings.Builder
+		for _, line := range strings.Split(s, "\n") {
+			if sb.Len() != 0 {
+				sb.WriteString("\n")
+			}
+
+			sb.WriteString(color.New(lineColor(line)).Sprint(line))
+		}
+
+		return sb.String()
+	},
 }
 
 var defaultSuccessTemplate = `[OK] {{ join .AssertPath .LineWidth }}`
@@ -813,6 +849,6 @@ allowed delta:
 {{- if .HaveDiff }}
 
 diff:
-{{ .Diff | indent }}
+{{ .Diff | colordiff .EnableColors | indent }}
 {{- end -}}
 `
diff --git a/reporter.go b/reporter.go
index 954739b..6dfdd1d 100644
--- a/reporter.go
+++ b/reporter.go
@@ -1,6 +1,7 @@
 package httpexpect
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -54,3 +55,20 @@ func NewFatalReporter(t testing.TB) *FatalReporter {
 func (r *FatalReporter) Errorf(message string, args ...interface{}) {
 	r.backend.Fatalf(message, args...)
 }
+
+// PanicReporter is a struct that implements the Reporter interface
+// and panics when a test fails.
+// Useful for multithreaded tests when you want to report fatal
+// failures from goroutines other than the main goroutine, because
+// the main goroutine is forbidden to call t.Fatal.
+type PanicReporter struct{}
+
+// NewPanicReporter returns a new PanicReporter object.
+func NewPanicReporter() *PanicReporter {
+	return &PanicReporter{}
+}
+
+// Errorf implements Reporter.Errorf
+func (r *PanicReporter) Errorf(message string, args ...interface{}) {
+	panic(fmt.Sprintf(message, args...))
+}


Test patch:
diff --git a/expect_test.go b/expect_test.go
index 859440e..b5186ba 100644
--- a/expect_test.go
+++ b/expect_test.go
@@ -1,8 +1,12 @@
 package httpexpect
 
 import (
+	"errors"
+	"io"
+	"net/http"
 	"testing"
 
+	"github.com/gorilla/websocket"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -529,3 +533,109 @@ func TestExpect_Config(t *testing.T) {
 		})
 	})
 }
+
+func TestExpect_Adapters(t *testing.T) {
+	t.Run("RequestFactoryFunc", func(t *testing.T) {
+		called := false
+		factory := RequestFactoryFunc(func(
+			_ string, _ string, _ io.Reader,
+		) (*http.Request, error) {
+			called = true
+			return nil, nil
+		})
+
+		e := WithConfig(Config{
+			RequestFactory: factory,
+			Reporter:       newMockReporter(t),
+		})
+
+		e.Request("GET", "/")
+
+		assert.True(t, called)
+	})
+
+	t.Run("ClientFunc", func(t *testing.T) {
+		called := false
+		client := ClientFunc(func(_ *http.Request) (*http.Response, error) {
+			called = true
+			return &http.Response{
+				Status:     "Test Status",
+				StatusCode: 504,
+			}, nil
+		})
+
+		e := WithConfig(Config{
+			Client:   client,
+			Reporter: newMockReporter(t),
+		})
+
+		req := e.GET("/")
+		resp := req.Expect()
+
+		assert.True(t, called)
+		assert.Equal(t, resp.httpResp.StatusCode, 504)
+		assert.Equal(t, resp.httpResp.Status, "Test Status")
+	})
+
+	t.Run("WebsocketDialerFunc", func(t *testing.T) {
+		called := false
+		dialer := WebsocketDialerFunc(func(
+			_ string, _ http.Header,
+		) (*websocket.Conn, *http.Response, error) {
+			called = true
+			return &websocket.Conn{}, &http.Response{}, nil
+		})
+
+		e := WithConfig(Config{
+			WebsocketDialer: dialer,
+			Reporter:        newMockReporter(t),
+		})
+
+		e.GET("/path").WithWebsocketUpgrade().Expect().Websocket()
+
+		assert.True(t, called)
+	})
+
+	t.Run("ReporterFunc", func(t *testing.T) {
+		called := false
+		message := ""
+		client := ClientFunc(func(r *http.Request) (*http.Response, error) {
+			return nil, errors.New("")
+		})
+		reporter := ReporterFunc(func(_ string, _ ...interface{}) {
+			called = true
+			message = "test reporter called"
+		})
+
+		e := WithConfig(Config{
+			Reporter: reporter,
+			Client:   client,
+		})
+
+		e.GET("/").Expect()
+
+		assert.True(t, called)
+		assert.Contains(t, message, "test reporter called")
+	})
+
+	t.Run("LoggerFunc", func(t *testing.T) {
+		called := false
+		message := ""
+		logger := LoggerFunc(func(_ string, _ ...interface{}) {
+			called = true
+			message = "test logger called"
+		})
+
+		e := WithConfig(Config{
+			Reporter: newMockReporter(t),
+			Printers: []Printer{
+				NewCompactPrinter(logger),
+			},
+		})
+
+		e.GET("").Expect()
+
+		assert.True(t, called)
+		assert.Contains(t, message, "test logger called")
+	})
+}
diff --git a/reporter_test.go b/reporter_test.go
index 8f87476..3e0dd6c 100644
--- a/reporter_test.go
+++ b/reporter_test.go
@@ -32,14 +32,6 @@ func (m *mockRequireT) FailNow() {
 	m.failNowInvoked = true
 }
 
-func TestReporter_FatalReporter(t *testing.T) {
-	mockBackend := &mockT{}
-	reporter := NewFatalReporter(mockBackend)
-
-	reporter.Errorf("test")
-	assert.True(t, mockBackend.fatalfInvoked)
-}
-
 func TestReporter_AssertReporter(t *testing.T) {
 	mockBackend := &mockAssertT{}
 	reporter := NewAssertReporter(mockBackend)
@@ -55,3 +47,19 @@ func TestReporter_RequireReporter(t *testing.T) {
 	reporter.Errorf("test")
 	assert.True(t, mockBackend.failNowInvoked)
 }
+
+func TestReporter_FatalReporter(t *testing.T) {
+	mockBackend := &mockT{}
+	reporter := NewFatalReporter(mockBackend)
+
+	reporter.Errorf("test")
+	assert.True(t, mockBackend.fatalfInvoked)
+}
+
+func TestReporter_PanicReporter(t *testing.T) {
+	reporter := NewPanicReporter()
+
+	assert.Panics(t, func() {
+		reporter.Errorf("test")
+	})
+}