1414package images_test
1515
1616import (
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
8241sourcefilename: ../testdata/gopher-hero8.png
8342-- layouts/index.html --
8443Home.
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