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

This commit is contained in:
DarthSim
2025-09-09 21:59:18 +03:00
parent 01327c1cce
commit 8bec24f268
6 changed files with 140 additions and 45 deletions

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
testutil/lasy_suite.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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