From 8bec24f2687d1d1076bf4898f3103b92bca9ce20 Mon Sep 17 00:00:00 2001 From: DarthSim Date: Tue, 9 Sep 2025 21:59:18 +0300 Subject: [PATCH] Return reset func for testutil.LazyObj; Add testutil.LazySuite --- integration/load_test.go | 11 ++--- integration/processing_handler_test.go | 43 +++++++++---------- integration/suite.go | 16 +++++-- testutil/lasy_suite.go | 59 ++++++++++++++++++++++++++ testutil/lazy_obj.go | 51 ++++++++++++++++++---- testutil/readers_equal.go | 5 ++- 6 files changed, 140 insertions(+), 45 deletions(-) create mode 100644 testutil/lasy_suite.go diff --git a/integration/load_test.go b/integration/load_test.go index a8273f3c..9dd1d8ee 100644 --- a/integration/load_test.go +++ b/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) diff --git a/integration/processing_handler_test.go b/integration/processing_handler_test.go index 0b5c940f..bb591890 100644 --- a/integration/processing_handler_test.go +++ b/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 } diff --git a/integration/suite.go b/integration/suite.go index 6b8cdac0..f4b0cd21 100644 --- a/integration/suite.go +++ b/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, + } } diff --git a/testutil/lasy_suite.go b/testutil/lasy_suite.go new file mode 100644 index 00000000..9e41441a --- /dev/null +++ b/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 +} diff --git a/testutil/lazy_obj.go b/testutil/lazy_obj.go index e7230f42..de6d0cbb 100644 --- a/testutil/lazy_obj.go +++ b/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 +} -// 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() +// 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. +// 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 } diff --git a/testutil/readers_equal.go b/testutil/readers_equal.go index 0955edd7..ce767861 100644 --- a/testutil/readers_equal.go +++ b/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