load_test.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package integration
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "image/png"
  7. "io"
  8. "net/http"
  9. "os"
  10. "path"
  11. "path/filepath"
  12. "strings"
  13. "testing"
  14. "github.com/corona10/goimagehash"
  15. "github.com/imgproxy/imgproxy/v3"
  16. "github.com/imgproxy/imgproxy/v3/config"
  17. "github.com/imgproxy/imgproxy/v3/imagetype"
  18. "github.com/imgproxy/imgproxy/v3/testutil"
  19. "github.com/imgproxy/imgproxy/v3/vips"
  20. "github.com/stretchr/testify/suite"
  21. )
  22. const (
  23. similarityThreshold = 5 // Distance between images to be considered similar
  24. bindPort = 9090 // Port to bind imgproxy to
  25. bindHost = "localhost"
  26. )
  27. type IntegrationSuite struct {
  28. suite.Suite
  29. ctx context.Context
  30. cancel context.CancelFunc
  31. testData *testutil.TestDataProvider
  32. testImagesPath string
  33. }
  34. // SetupSuite starts imgproxy instance server
  35. func (s *IntegrationSuite) SetupSuite() {
  36. s.testData = testutil.NewTestDataProvider(s.T())
  37. s.testImagesPath = s.testData.Path("test-images")
  38. s.ctx, s.cancel = context.WithCancel(s.T().Context())
  39. s.startImgproxy(s.ctx)
  40. }
  41. // TearDownSuite stops imgproxy instance server
  42. func (s *IntegrationSuite) TearDownSuite() {
  43. s.cancel()
  44. }
  45. // testLoadFolder fetches images iterates over images in the specified folder,
  46. // runs imgproxy on each image, and compares the result with the reference image
  47. // which is expected to be in the `integration` folder with the same name
  48. // but with `.png` extension.
  49. func (s *IntegrationSuite) testLoadFolder(folder string) {
  50. walkPath := path.Join(s.testImagesPath, folder)
  51. // Iterate over the files in the source folder
  52. err := filepath.Walk(walkPath, func(path string, info os.FileInfo, err error) error {
  53. s.Require().NoError(err)
  54. // Skip directories
  55. if info.IsDir() {
  56. return nil
  57. }
  58. // get the base name of the file (8-bpp.png)
  59. basePath := filepath.Base(path)
  60. // Replace the extension with .png
  61. referencePath := strings.TrimSuffix(basePath, filepath.Ext(basePath)) + ".png"
  62. // Construct the full path to the reference image (integration/ folder)
  63. referencePath = filepath.Join(s.testImagesPath, "integration", folder, referencePath)
  64. // Construct the source URL for imgproxy (no processing)
  65. sourceUrl := fmt.Sprintf("insecure/plain/local:///%s/%s@png", folder, basePath)
  66. imgproxyImageBytes := s.fetchImage(sourceUrl)
  67. imgproxyImage, err := png.Decode(bytes.NewReader(imgproxyImageBytes))
  68. s.Require().NoError(err, "Failed to decode PNG image from imgproxy for %s", basePath)
  69. referenceFile, err := os.Open(referencePath)
  70. s.Require().NoError(err)
  71. defer referenceFile.Close()
  72. referenceImage, err := png.Decode(referenceFile)
  73. s.Require().NoError(err, "Failed to decode PNG reference image for %s", referencePath)
  74. hash1, err := goimagehash.DifferenceHash(imgproxyImage)
  75. s.Require().NoError(err)
  76. hash2, err := goimagehash.DifferenceHash(referenceImage)
  77. s.Require().NoError(err)
  78. distance, err := hash1.Distance(hash2)
  79. s.Require().NoError(err)
  80. s.Require().LessOrEqual(distance, similarityThreshold,
  81. "Image %s differs from reference image %s by %d, which is greater than the allowed threshold of %d",
  82. basePath, referencePath, distance, similarityThreshold)
  83. return nil
  84. })
  85. s.Require().NoError(err)
  86. }
  87. // fetchImage fetches an image from the imgproxy server
  88. func (s *IntegrationSuite) fetchImage(path string) []byte {
  89. url := fmt.Sprintf("http://%s:%d/%s", bindHost, bindPort, path)
  90. resp, err := http.Get(url)
  91. s.Require().NoError(err, "Failed to fetch image from %s", url)
  92. defer resp.Body.Close()
  93. s.Require().Equal(http.StatusOK, resp.StatusCode, "Expected status code 200 OK, got %d, url: %s", resp.StatusCode, url)
  94. bytes, err := io.ReadAll(resp.Body)
  95. s.Require().NoError(err, "Failed to read response body from %s", url)
  96. return bytes
  97. }
  98. func (s *IntegrationSuite) startImgproxy(ctx context.Context) *imgproxy.ImgProxy {
  99. c, err := imgproxy.LoadConfigFromEnv(nil)
  100. s.Require().NoError(err)
  101. c.Server.Bind = ":" + fmt.Sprintf("%d", bindPort)
  102. c.Transport.Local.Root = s.testImagesPath
  103. c.Server.LogMemStats = true
  104. config.MaxAnimationFrames = 999
  105. config.DevelopmentErrorsMode = true
  106. i, err := imgproxy.New(ctx, c)
  107. s.Require().NoError(err)
  108. go func() {
  109. err = i.StartServer(ctx)
  110. if err != nil {
  111. s.T().Errorf("Imgproxy server exited with error: %v", err)
  112. }
  113. }()
  114. return i
  115. }
  116. // TestLoadSaveToPng ensures that our load pipeline works,
  117. // including standard and custom loaders. For each source image
  118. // in the folder, it does the passthrough request through imgproxy:
  119. // no processing, just convert format of the source file to png.
  120. // Then, it compares the result with the reference image.
  121. func (s *IntegrationSuite) TestLoadSaveToPng() {
  122. testCases := []struct {
  123. name string
  124. imageType imagetype.Type
  125. folderName string
  126. }{
  127. {"GIF", imagetype.GIF, "gif"},
  128. {"JPEG", imagetype.JPEG, "jpg"},
  129. {"HEIC", imagetype.HEIC, "heif"},
  130. {"JXL", imagetype.JXL, "jxl"},
  131. {"SVG", imagetype.SVG, "svg"},
  132. {"TIFF", imagetype.TIFF, "tiff"},
  133. {"WEBP", imagetype.WEBP, "webp"},
  134. {"BMP", imagetype.BMP, "bmp"},
  135. {"ICO", imagetype.ICO, "ico"},
  136. }
  137. for _, tc := range testCases {
  138. s.T().Run(tc.name, func(t *testing.T) {
  139. if vips.SupportsLoad(tc.imageType) {
  140. s.testLoadFolder(tc.folderName)
  141. } else {
  142. t.Skipf("%s format not supported by VIPS", tc.name)
  143. }
  144. })
  145. }
  146. }
  147. func TestIntegration(t *testing.T) {
  148. suite.Run(t, new(IntegrationSuite))
  149. }