Skip to content

Commit 2501de7

Browse files
committed
resources/images: Refactor golden image tests to locate them closer to the implementation
1 parent 06cc867 commit 2501de7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+306
-240
lines changed

‎resources/images/images_golden_integration_test.go

+49-240
Original file line numberDiff line numberDiff line change
@@ -14,63 +14,22 @@
1414
package images_test
1515

1616
import (
17-
"image"
18-
"image/gif"
1917
_ "image/jpeg"
20-
"io/fs"
21-
"os"
22-
"path/filepath"
23-
"runtime"
24-
"strings"
2518
"testing"
2619

27-
"github.com/disintegration/gift"
28-
qt "github.com/frankban/quicktest"
29-
"github.com/gohugoio/hugo/common/hashing"
30-
"github.com/gohugoio/hugo/common/hugio"
31-
"github.com/gohugoio/hugo/htesting"
32-
"github.com/gohugoio/hugo/hugofs"
33-
"github.com/gohugoio/hugo/hugolib"
34-
"github.com/google/go-cmp/cmp"
20+
"github.com/gohugoio/hugo/resources/images/imagetesting"
3521
)
3622

37-
var eq = qt.CmpEquals(
38-
cmp.Comparer(func(p1, p2 os.FileInfo) bool {
39-
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
40-
}),
41-
cmp.Comparer(func(d1, d2 fs.DirEntry) bool {
42-
p1, err1 := d1.Info()
43-
p2, err2 := d2.Info()
44-
if err1 != nil || err2 != nil {
45-
return false
46-
}
47-
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
48-
}),
49-
)
50-
51-
var goldenOpts = struct {
52-
// Toggle this to write golden files to disk.
53-
// Note: Remember to set this to false before committing.
54-
writeGoldenFiles bool
55-
56-
// This will skip any assertions. Useful when adding new golden variants to a test.
57-
devMode bool
58-
}{
59-
writeGoldenFiles: false,
60-
devMode: false,
61-
}
62-
6323
// Note, if you're enabling writeGoldenFiles on a MacOS ARM 64 you need to run the test with GOARCH=amd64, e.g.
64-
// GOARCH=amd64 go test -count 1 -timeout 30s -run "^TestGolden" ./resources/images
65-
func TestGoldenFiltersMisc(t *testing.T) {
24+
func TestImagesGoldenFiltersMisc(t *testing.T) {
6625
t.Parallel()
6726

68-
if skipGolden {
27+
if imagetesting.SkipGoldenTests {
6928
t.Skip("Skip golden test on this architecture")
7029
}
7130

72-
// Will be used to generate golden files.
73-
name := "filters_misc"
31+
// Will be used as the base folder for generated images.
32+
name := "filters/misc"
7433

7534
files := `
7635
-- hugo.toml --
@@ -82,9 +41,9 @@ sourcefilename: ../testdata/sunset.jpg
8241
sourcefilename: ../testdata/gopher-hero8.png
8342
-- layouts/index.html --
8443
Home.
85-
{{ $sunset := resources.Get "sunset.jpg" }}
44+
{{ $sunset := (resources.Get "sunset.jpg").Resize "x300" }}
8645
{{ $sunsetGrayscale := $sunset.Filter (images.Grayscale) }}
87-
{{ $gopher := resources.Get "gopher.png" }}
46+
{{ $gopher := (resources.Get "gopher.png").Resize "x80" }}
8847
{{ $overlayFilter := images.Overlay $gopher 20 20 }}
8948
9049
{{ $textOpts := dict
@@ -130,18 +89,23 @@ Home.
13089
{{ end }}
13190
`
13291

133-
runGolden(t, name, files)
92+
opts := imagetesting.DefaultGoldenOpts
93+
opts.T = t
94+
opts.Name = name
95+
opts.Files = files
96+
97+
imagetesting.RunGolden(opts)
13498
}
13599

136-
func TestGoldenFiltersMask(t *testing.T) {
100+
func TestImagesGoldenFiltersMask(t *testing.T) {
137101
t.Parallel()
138102

139-
if skipGolden {
103+
if imagetesting.SkipGoldenTests {
140104
t.Skip("Skip golden test on this architecture")
141105
}
142106

143-
// Will be used to generate golden files.
144-
name := "filters_mask"
107+
// Will be used as the base folder for generated images.
108+
name := "filters/mask"
145109

146110
files := `
147111
-- hugo.toml --
@@ -163,15 +127,20 @@ Home.
163127
{{ template "mask" (dict "name" "transparant.png" "base" $sunset "mask" $mask) }}
164128
{{ template "mask" (dict "name" "yellow.jpg" "base" $sunset "mask" $mask) }}
165129
{{ template "mask" (dict "name" "wide.jpg" "base" $sunset "mask" $mask "spec" "resize 600x200") }}
166-
130+
{{/* This looks a little odd, but is correct and the recommended way to do this.
131+
This will 1. Scale the image to x300, 2. Apply the mask, 3. Create the final image with background color #323ea.
132+
It's possible to have multiple images.Process filters in the chain, but for the options for the final image (target format, bgGolor etc.),
133+
the last entry will win.
134+
*/}}
135+
{{ template "mask" (dict "name" "blue.jpg" "base" $sunset "mask" $mask "spec" "resize x300 #323ea8") }}
167136
168137
{{ define "mask"}}
169138
{{ $ext := path.Ext .name }}
170139
{{ if lt (len (path.Ext .name)) 4 }}
171140
{{ errorf "No extension in %q" .name }}
172141
{{ end }}
173142
{{ $format := strings.TrimPrefix "." $ext }}
174-
{{ $spec := .spec | default (printf "resize 300x300 %s" $format) }}
143+
{{ $spec := .spec | default (printf "resize x300 %s" $format) }}
175144
{{ $filters := slice (images.Process $spec) (images.Mask .mask) }}
176145
{{ $name := printf "images/%s" .name }}
177146
{{ $img := .base.Filter $filters }}
@@ -181,18 +150,23 @@ Home.
181150
{{ end }}
182151
`
183152

184-
runGolden(t, name, files)
153+
opts := imagetesting.DefaultGoldenOpts
154+
opts.T = t
155+
opts.Name = name
156+
opts.Files = files
157+
158+
imagetesting.RunGolden(opts)
185159
}
186160

187-
func TestGoldenFiltersText(t *testing.T) {
161+
func TestImagesGoldenFiltersText(t *testing.T) {
188162
t.Parallel()
189163

190-
if skipGolden {
164+
if imagetesting.SkipGoldenTests {
191165
t.Skip("Skip golden test on this architecture")
192166
}
193167

194-
// Will be used to generate golden files.
195-
name := "filters_text"
168+
// Will be used as the base folder for generated images.
169+
name := "filters/text"
196170

197171
files := `
198172
-- hugo.toml --
@@ -230,18 +204,23 @@ Home.
230204
{{ end }}
231205
`
232206

233-
runGolden(t, name, files)
207+
opts := imagetesting.DefaultGoldenOpts
208+
opts.T = t
209+
opts.Name = name
210+
opts.Files = files
211+
212+
imagetesting.RunGolden(opts)
234213
}
235214

236-
func TestGoldenProcessMisc(t *testing.T) {
215+
func TestImagesGoldenProcessMisc(t *testing.T) {
237216
t.Parallel()
238217

239-
if skipGolden {
218+
if imagetesting.SkipGoldenTests {
240219
t.Skip("Skip golden test on this architecture")
241220
}
242221

243-
// Will be used to generate golden files.
244-
name := "process_misc"
222+
// Will be used as the base folder for generated images.
223+
name := "process/misc"
245224

246225
files := `
247226
-- hugo.toml --
@@ -277,180 +256,10 @@ Home.
277256
{{ end }}
278257
`
279258

280-
runGolden(t, name, files)
281-
}
282-
283-
func TestGoldenFuncs(t *testing.T) {
284-
t.Parallel()
285-
286-
if skipGolden {
287-
t.Skip("Skip golden test on this architecture")
288-
}
289-
290-
// Will be used to generate golden files.
291-
name := "funcs"
292-
293-
files := `
294-
-- hugo.toml --
295-
-- assets/sunset.jpg --
296-
sourcefilename: ../testdata/sunset.jpg
297-
298-
-- layouts/index.html --
299-
Home.
300-
301-
{{ template "copy" (dict "name" "qr-default.png" "img" (images.QR "https://gohugo.io")) }}
302-
{{ template "copy" (dict "name" "qr-level-high_scale-6.png" "img" (images.QR "https://gohugo.io" (dict "level" "high" "scale" 6))) }}
303-
304-
{{ define "copy"}}
305-
{{ if lt (len (path.Ext .name)) 4 }}
306-
{{ errorf "No extension in %q" .name }}
307-
{{ end }}
308-
{{ $img := .img }}
309-
{{ $name := printf "images/%s" .name }}
310-
{{ with $img | resources.Copy $name }}
311-
{{ .Publish }}
312-
{{ end }}
313-
{{ end }}
314-
`
315-
316-
runGolden(t, name, files)
317-
}
318-
319-
func runGolden(t testing.TB, name, files string) *hugolib.IntegrationTestBuilder {
320-
t.Helper()
321-
322-
c := hugolib.Test(t, files, hugolib.TestOptWithOSFs()) // hugolib.TestOptWithPrintAndKeepTempDir(true))
323-
c.AssertFileContent("public/index.html", "Home.")
324-
325-
outputDir := filepath.Join(c.H.Conf.WorkingDir(), "public", "images")
326-
goldenBaseDir := filepath.Join("testdata", "images_golden")
327-
goldenDir := filepath.Join(goldenBaseDir, name)
328-
if goldenOpts.writeGoldenFiles {
329-
c.Assert(htesting.IsRealCI(), qt.IsFalse)
330-
c.Assert(os.MkdirAll(goldenBaseDir, 0o777), qt.IsNil)
331-
c.Assert(os.RemoveAll(goldenDir), qt.IsNil)
332-
c.Assert(hugio.CopyDir(hugofs.Os, outputDir, goldenDir, nil), qt.IsNil)
333-
return c
334-
}
259+
opts := imagetesting.DefaultGoldenOpts
260+
opts.T = t
261+
opts.Name = name
262+
opts.Files = files
335263

336-
if goldenOpts.devMode {
337-
c.Assert(htesting.IsRealCI(), qt.IsFalse)
338-
return c
339-
}
340-
341-
decodeAll := func(f *os.File) []image.Image {
342-
c.Helper()
343-
344-
var images []image.Image
345-
346-
if strings.HasSuffix(f.Name(), ".gif") {
347-
gif, err := gif.DecodeAll(f)
348-
c.Assert(err, qt.IsNil, qt.Commentf(f.Name()))
349-
images = make([]image.Image, len(gif.Image))
350-
for i, img := range gif.Image {
351-
images[i] = img
352-
}
353-
} else {
354-
img, _, err := image.Decode(f)
355-
c.Assert(err, qt.IsNil, qt.Commentf(f.Name()))
356-
images = append(images, img)
357-
}
358-
return images
359-
}
360-
361-
entries1, err := os.ReadDir(outputDir)
362-
c.Assert(err, qt.IsNil)
363-
entries2, err := os.ReadDir(goldenDir)
364-
c.Assert(err, qt.IsNil)
365-
c.Assert(len(entries1), qt.Equals, len(entries2))
366-
for i, e1 := range entries1 {
367-
c.Assert(filepath.Ext(e1.Name()), qt.Not(qt.Equals), "")
368-
func() {
369-
e2 := entries2[i]
370-
371-
f1, err := os.Open(filepath.Join(outputDir, e1.Name()))
372-
c.Assert(err, qt.IsNil)
373-
defer f1.Close()
374-
375-
f2, err := os.Open(filepath.Join(goldenDir, e2.Name()))
376-
c.Assert(err, qt.IsNil)
377-
defer f2.Close()
378-
379-
imgs2 := decodeAll(f2)
380-
imgs1 := decodeAll(f1)
381-
c.Assert(len(imgs1), qt.Equals, len(imgs2))
382-
383-
if !usesFMA {
384-
c.Assert(e1, eq, e2)
385-
_, err = f1.Seek(0, 0)
386-
c.Assert(err, qt.IsNil)
387-
_, err = f2.Seek(0, 0)
388-
c.Assert(err, qt.IsNil)
389-
390-
hash1, _, err := hashing.XXHashFromReader(f1)
391-
c.Assert(err, qt.IsNil)
392-
hash2, _, err := hashing.XXHashFromReader(f2)
393-
c.Assert(err, qt.IsNil)
394-
395-
c.Assert(hash1, qt.Equals, hash2)
396-
}
397-
398-
for i, img1 := range imgs1 {
399-
img2 := imgs2[i]
400-
nrgba1 := image.NewNRGBA(img1.Bounds())
401-
gift.New().Draw(nrgba1, img1)
402-
nrgba2 := image.NewNRGBA(img2.Bounds())
403-
gift.New().Draw(nrgba2, img2)
404-
c.Assert(goldenEqual(nrgba1, nrgba2), qt.Equals, true, qt.Commentf(e1.Name()))
405-
}
406-
}()
407-
}
408-
return c
264+
imagetesting.RunGolden(opts)
409265
}
410-
411-
// goldenEqual compares two NRGBA images. It is used in golden tests only.
412-
// A small tolerance is allowed on architectures using "fused multiply and add"
413-
// (FMA) instruction to accommodate for floating-point rounding differences
414-
// with control golden images that were generated on amd64 architecture.
415-
// See https://golang.org/ref/spec#Floating_point_operators
416-
// and https://github.com/gohugoio/hugo/issues/6387 for more information.
417-
//
418-
// Based on https://github.com/disintegration/gift/blob/a999ff8d5226e5ab14b64a94fca07c4ac3f357cf/gift_test.go#L598-L625
419-
// Copyright (c) 2014-2019 Grigory Dryapak
420-
// Licensed under the MIT License.
421-
func goldenEqual(img1, img2 *image.NRGBA) bool {
422-
maxDiff := 0
423-
if runtime.GOARCH != "amd64" {
424-
// The golden files are created using the AMD64 architecture.
425-
// Be lenient on other platforms due to floaging point and dithering differences.
426-
maxDiff = 15
427-
}
428-
if !img1.Rect.Eq(img2.Rect) {
429-
return false
430-
}
431-
if len(img1.Pix) != len(img2.Pix) {
432-
return false
433-
}
434-
for i := 0; i < len(img1.Pix); i++ {
435-
diff := int(img1.Pix[i]) - int(img2.Pix[i])
436-
if diff < 0 {
437-
diff = -diff
438-
}
439-
if diff > maxDiff {
440-
return false
441-
}
442-
}
443-
return true
444-
}
445-
446-
// We don't have a CI test environment for these, and there are known dithering issues that makes these time consuming to maintain.
447-
var skipGolden = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "s390x"
448-
449-
// usesFMA indicates whether "fused multiply and add" (FMA) instruction is
450-
// used. The command "grep FMADD go/test/codegen/floats.go" can help keep
451-
// the FMA-using architecture list updated.
452-
var usesFMA = runtime.GOARCH == "s390x" ||
453-
runtime.GOARCH == "ppc64" ||
454-
runtime.GOARCH == "ppc64le" ||
455-
runtime.GOARCH == "arm64" ||
456-
runtime.GOARCH == "riscv64"

0 commit comments

Comments
 (0)