Prechádzať zdrojové kódy

Return reset func for testutil.LazyObj; Add testutil.LazySuite

DarthSim 4 týždňov pred
rodič
commit
8bec24f268

+ 4 - 7
integration/load_test.go

@@ -2,11 +2,9 @@ package integration
 
 import (
 	"bytes"
-	"context"
 	"fmt"
 	"image/png"
 	"io"
-	"net"
 	"net/http"
 	"os"
 	"path"
@@ -32,8 +30,7 @@ type LoadTestSuite struct {
 	testData       *testutil.TestDataProvider
 	testImagesPath string
 
-	addr         net.Addr
-	stopImgproxy context.CancelFunc
+	server *TestServer
 }
 
 // SetupSuite starts imgproxy instance server
@@ -49,12 +46,12 @@ func (s *LoadTestSuite) SetupSuite() {
 	config.DevelopmentErrorsMode = true
 
 	// In this test we start the single imgproxy server for all test cases
-	s.addr, s.stopImgproxy = s.StartImgproxy(c)
+	s.server = s.StartImgproxy(c)
 }
 
 // TearDownSuite stops imgproxy instance server
 func (s *LoadTestSuite) TearDownSuite() {
-	s.stopImgproxy()
+	s.server.Shutdown()
 }
 
 // testLoadFolder fetches images iterates over images in the specified folder,
@@ -117,7 +114,7 @@ func (s *LoadTestSuite) testLoadFolder(folder string) {
 
 // fetchImage fetches an image from the imgproxy server
 func (s *LoadTestSuite) fetchImage(path string) []byte {
-	url := fmt.Sprintf("http://%s/%s", s.addr.String(), path)
+	url := fmt.Sprintf("http://%s/%s", s.server.Addr, path)
 
 	resp, err := http.Get(url)
 	s.Require().NoError(err, "Failed to fetch image from %s", url)

+ 19 - 24
integration/processing_handler_test.go

@@ -1,7 +1,6 @@
 package integration
 
 import (
-	"bytes"
 	"fmt"
 	"io"
 	"net/http"
@@ -36,6 +35,7 @@ type ProcessingHandlerTestSuite struct {
 	// happen afterwards. It is done via lazy obj. When all config values will be moved
 	// to imgproxy.Config struct, this can be removed.
 	config testutil.LazyObj[*imgproxy.Config]
+	server testutil.LazyObj[*TestServer]
 }
 
 func (s *ProcessingHandlerTestSuite) SetupSuite() {
@@ -44,15 +44,8 @@ func (s *ProcessingHandlerTestSuite) SetupSuite() {
 
 	// Initialize test data provider (local test files)
 	s.testData = testutil.NewTestDataProvider(s.T())
-}
-
-func (s *ProcessingHandlerTestSuite) TearDownSuite() {
-	logrus.SetOutput(os.Stdout)
-}
 
-// setupObjs initializes lazy objects
-func (s *ProcessingHandlerTestSuite) setupObjs() {
-	s.config = testutil.NewLazyObj(s.T(), func() (*imgproxy.Config, error) {
+	s.config, _ = testutil.NewLazySuiteObj(s, func() (*imgproxy.Config, error) {
 		c, err := imgproxy.LoadConfigFromEnv(nil)
 		s.Require().NoError(err)
 
@@ -61,6 +54,21 @@ func (s *ProcessingHandlerTestSuite) setupObjs() {
 
 		return c, nil
 	})
+
+	s.server, _ = testutil.NewLazySuiteObj(
+		s,
+		func() (*TestServer, error) {
+			return s.StartImgproxy(s.config()), nil
+		},
+		func(s *TestServer) error {
+			s.Shutdown()
+			return nil
+		},
+	)
+}
+
+func (s *ProcessingHandlerTestSuite) TearDownSuite() {
+	logrus.SetOutput(os.Stdout)
 }
 
 func (s *ProcessingHandlerTestSuite) SetupTest() {
@@ -69,23 +77,17 @@ func (s *ProcessingHandlerTestSuite) SetupTest() {
 	// NOTE: This must be moved to security config
 	config.AllowLoopbackSourceAddresses = true
 	// NOTE: end note
-
-	s.setupObjs()
 }
 
 func (s *ProcessingHandlerTestSuite) SetupSubTest() {
 	// We use t.Run() a lot, so we need to reset lazy objects at the beginning of each subtest
-	s.setupObjs()
+	s.ResetLazyObjects()
 }
 
 // GET performs a GET request to the imageproxy real server
 // NOTE: Do not forget to move this to Suite in case of need in other future test suites
 func (s *ProcessingHandlerTestSuite) GET(path string, header ...http.Header) *http.Response {
-	// In this test we start the imgproxy server instance per request
-	addr, stopServer := s.StartImgproxy(s.config())
-	defer stopServer()
-
-	url := fmt.Sprintf("http://%s%s", addr.String(), path)
+	url := fmt.Sprintf("http://%s%s", s.server().Addr, path)
 
 	// Perform GET request to an url
 	req, _ := http.NewRequest("GET", url, nil)
@@ -99,13 +101,6 @@ func (s *ProcessingHandlerTestSuite) GET(path string, header ...http.Header) *ht
 	resp, err := http.DefaultClient.Do(req)
 	s.Require().NoError(err)
 
-	// Read the entire body into memory and replace the original body with memory reader
-	// to avoid the defer
-	bodyBytes, err := io.ReadAll(resp.Body)
-	s.Require().NoError(err)
-	resp.Body.Close()
-	resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
-
 	return resp
 }
 

+ 12 - 4
integration/suite.go

@@ -5,16 +5,21 @@ import (
 	"net"
 
 	"github.com/imgproxy/imgproxy/v3"
-	"github.com/stretchr/testify/suite"
+	"github.com/imgproxy/imgproxy/v3/testutil"
 )
 
+type TestServer struct {
+	Addr     net.Addr
+	Shutdown context.CancelFunc
+}
+
 type Suite struct {
-	suite.Suite
+	testutil.LazySuite
 }
 
 // StartImgproxy starts imgproxy instance for the tests
 // Returns instance, instance address and stop function
-func (s *Suite) StartImgproxy(c *imgproxy.Config) (net.Addr, context.CancelFunc) {
+func (s *Suite) StartImgproxy(c *imgproxy.Config) *TestServer {
 	ctx, cancel := context.WithCancel(s.T().Context())
 
 	c.Server.Bind = ":0"
@@ -32,5 +37,8 @@ func (s *Suite) StartImgproxy(c *imgproxy.Config) (net.Addr, context.CancelFunc)
 		}
 	}()
 
-	return <-addrCh, cancel
+	return &TestServer{
+		Addr:     <-addrCh,
+		Shutdown: cancel,
+	}
 }

+ 59 - 0
testutil/lasy_suite.go

@@ -0,0 +1,59 @@
+package testutil
+
+import (
+	"context"
+
+	"github.com/stretchr/testify/suite"
+)
+
+// LazySuite is a test suite that automatically resets [LazyObj] instances.
+// It uses [LazySuite.AfterTest] to perform the reset after each test,
+// so if you also use this function in your test suite, don't forget to call
+// [LazySuite.AfterTest] or [LazySuite.ResetLazyObjects] explicitly.
+type LazySuite struct {
+	suite.Suite
+
+	resets []context.CancelFunc
+}
+
+// Lazy returns the LazySuite instance itself.
+// Needed to implement [LazySuiteFrom].
+func (s *LazySuite) Lazy() *LazySuite {
+	return s
+}
+
+// AfterTest is called by testify after each test.
+// If you also use this function in your test suite, don't forget to call
+// [LazySuite.AfterTest] or [LazySuite.ResetLazyObjects] explicitly.
+func (s *LazySuite) AfterTest(_, _ string) {
+	// Reset lazy objects after each test
+	s.ResetLazyObjects()
+}
+
+// ResetLazyObjects resets all lazy objects created with [NewLazySuiteObj]
+func (s *LazySuite) ResetLazyObjects() {
+	for _, reset := range s.resets {
+		reset()
+	}
+}
+
+type LazySuiteFrom interface {
+	Lazy() *LazySuite
+}
+
+// NewLazySuiteObj creates a new [LazyObj] instance and registers its cleanup function
+// to a provided [LazySuite].
+func NewLazySuiteObj[T any](
+	s LazySuiteFrom,
+	newFn LazyObjNew[T],
+	dropFn ...LazyObjDrop[T],
+) (LazyObj[T], context.CancelFunc) {
+	// Get the [LazySuite] instance
+	lazy := s.Lazy()
+	// Create the [LazyObj] instance
+	obj, cancel := NewLazyObj(lazy, newFn, dropFn...)
+	// Add cleanup function to the resets list
+	lazy.resets = append(lazy.resets, cancel)
+
+	return obj, cancel
+}

+ 43 - 8
testutil/lazy_obj.go

@@ -1,6 +1,7 @@
 package testutil
 
 import (
+	"context"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -9,24 +10,58 @@ import (
 // LazyObj is a function that returns an object of type T.
 type LazyObj[T any] func() T
 
-// LazyObjInit is a function that initializes and returns an object of type T and an error if any.
-type LazyObjInit[T any] func() (T, error)
+// LazyObjT is an interface that provides access to the [testing.T] object.
+type LazyObjT interface {
+	T() *testing.T
+}
+
+// LazyObjNew is a function that creates and returns an object of type T and an error if any.
+type LazyObjNew[T any] func() (T, error)
+
+// LazyObjDrop is a callback that is called when [LazyObj] is reset.
+// It receives a pointer to the object being dropped.
+// If the object was not yet initialized, the callback is not called.
+type LazyObjDrop[T any] func(T) error
 
-// NewLazyObj creates a new LazyObj that initializes the object on the first call.
-func NewLazyObj[T any](t *testing.T, init LazyObjInit[T]) LazyObj[T] {
-	t.Helper()
+// NewLazyObj creates a new [LazyObj] that initializes the object on the first call.
+// It returns a function that can be called to get the object and a cancel function
+// that can be called to reset the object.
+func NewLazyObj[T any](
+	s LazyObjT,
+	newFn LazyObjNew[T],
+	dropFn ...LazyObjDrop[T],
+) (LazyObj[T], context.CancelFunc) {
+	s.T().Helper()
 
 	var obj *T
 
-	return func() T {
+	init := func() T {
 		if obj != nil {
 			return *obj
 		}
 
-		o, err := init()
-		require.NoError(t, err)
+		o, err := newFn()
+		require.NoError(s.T(), err, "Failed to initialize lazy object")
 
 		obj = &o
 		return o
 	}
+
+	cancel := func() {
+		if obj == nil {
+			return
+		}
+
+		for _, fn := range dropFn {
+			if fn == nil {
+				continue
+			}
+
+			require.NoError(s.T(), fn(*obj), "Failed to reset lazy object")
+		}
+
+		obj = nil
+	}
+
+	return init, cancel
 }

+ 3 - 2
testutil/readers_equal.go

@@ -4,6 +4,7 @@ import (
 	"io"
 	"testing"
 
+	"github.com/imgproxy/imgproxy/v3/ioutil"
 	"github.com/stretchr/testify/require"
 )
 
@@ -20,8 +21,8 @@ func ReadersEqual(t *testing.T, expected, actual io.Reader) bool {
 	buf2 := make([]byte, bufSize)
 
 	for {
-		n1, err1 := expected.Read(buf1)
-		n2, err2 := actual.Read(buf2)
+		n1, err1 := ioutil.TryReadFull(expected, buf1)
+		n2, err2 := ioutil.TryReadFull(actual, buf2)
 
 		if n1 != n2 {
 			return false