Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] master from gohugoio:master #1460

Merged
merged 1 commit into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 49 additions & 240 deletions resources/images/images_golden_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,63 +14,22 @@
package images_test

import (
"image"
"image/gif"
_ "image/jpeg"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/disintegration/gift"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
"github.com/google/go-cmp/cmp"
"github.com/gohugoio/hugo/resources/images/imagetesting"
)

var eq = qt.CmpEquals(
cmp.Comparer(func(p1, p2 os.FileInfo) bool {
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
}),
cmp.Comparer(func(d1, d2 fs.DirEntry) bool {
p1, err1 := d1.Info()
p2, err2 := d2.Info()
if err1 != nil || err2 != nil {
return false
}
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
}),
)

var goldenOpts = struct {
// Toggle this to write golden files to disk.
// Note: Remember to set this to false before committing.
writeGoldenFiles bool

// This will skip any assertions. Useful when adding new golden variants to a test.
devMode bool
}{
writeGoldenFiles: false,
devMode: false,
}

// Note, if you're enabling writeGoldenFiles on a MacOS ARM 64 you need to run the test with GOARCH=amd64, e.g.
// GOARCH=amd64 go test -count 1 -timeout 30s -run "^TestGolden" ./resources/images
func TestGoldenFiltersMisc(t *testing.T) {
func TestImagesGoldenFiltersMisc(t *testing.T) {
t.Parallel()

if skipGolden {
if imagetesting.SkipGoldenTests {
t.Skip("Skip golden test on this architecture")
}

// Will be used to generate golden files.
name := "filters_misc"
// Will be used as the base folder for generated images.
name := "filters/misc"

files := `
-- hugo.toml --
Expand All @@ -82,9 +41,9 @@ sourcefilename: ../testdata/sunset.jpg
sourcefilename: ../testdata/gopher-hero8.png
-- layouts/index.html --
Home.
{{ $sunset := resources.Get "sunset.jpg" }}
{{ $sunset := (resources.Get "sunset.jpg").Resize "x300" }}
{{ $sunsetGrayscale := $sunset.Filter (images.Grayscale) }}
{{ $gopher := resources.Get "gopher.png" }}
{{ $gopher := (resources.Get "gopher.png").Resize "x80" }}
{{ $overlayFilter := images.Overlay $gopher 20 20 }}

{{ $textOpts := dict
Expand Down Expand Up @@ -130,18 +89,23 @@ Home.
{{ end }}
`

runGolden(t, name, files)
opts := imagetesting.DefaultGoldenOpts
opts.T = t
opts.Name = name
opts.Files = files

imagetesting.RunGolden(opts)
}

func TestGoldenFiltersMask(t *testing.T) {
func TestImagesGoldenFiltersMask(t *testing.T) {
t.Parallel()

if skipGolden {
if imagetesting.SkipGoldenTests {
t.Skip("Skip golden test on this architecture")
}

// Will be used to generate golden files.
name := "filters_mask"
// Will be used as the base folder for generated images.
name := "filters/mask"

files := `
-- hugo.toml --
Expand All @@ -163,15 +127,20 @@ Home.
{{ template "mask" (dict "name" "transparant.png" "base" $sunset "mask" $mask) }}
{{ template "mask" (dict "name" "yellow.jpg" "base" $sunset "mask" $mask) }}
{{ template "mask" (dict "name" "wide.jpg" "base" $sunset "mask" $mask "spec" "resize 600x200") }}

{{/* This looks a little odd, but is correct and the recommended way to do this.
This will 1. Scale the image to x300, 2. Apply the mask, 3. Create the final image with background color #323ea.
It's possible to have multiple images.Process filters in the chain, but for the options for the final image (target format, bgGolor etc.),
the last entry will win.
*/}}
{{ template "mask" (dict "name" "blue.jpg" "base" $sunset "mask" $mask "spec" "resize x300 #323ea8") }}

{{ define "mask"}}
{{ $ext := path.Ext .name }}
{{ if lt (len (path.Ext .name)) 4 }}
{{ errorf "No extension in %q" .name }}
{{ end }}
{{ $format := strings.TrimPrefix "." $ext }}
{{ $spec := .spec | default (printf "resize 300x300 %s" $format) }}
{{ $spec := .spec | default (printf "resize x300 %s" $format) }}
{{ $filters := slice (images.Process $spec) (images.Mask .mask) }}
{{ $name := printf "images/%s" .name }}
{{ $img := .base.Filter $filters }}
Expand All @@ -181,18 +150,23 @@ Home.
{{ end }}
`

runGolden(t, name, files)
opts := imagetesting.DefaultGoldenOpts
opts.T = t
opts.Name = name
opts.Files = files

imagetesting.RunGolden(opts)
}

func TestGoldenFiltersText(t *testing.T) {
func TestImagesGoldenFiltersText(t *testing.T) {
t.Parallel()

if skipGolden {
if imagetesting.SkipGoldenTests {
t.Skip("Skip golden test on this architecture")
}

// Will be used to generate golden files.
name := "filters_text"
// Will be used as the base folder for generated images.
name := "filters/text"

files := `
-- hugo.toml --
Expand Down Expand Up @@ -230,18 +204,23 @@ Home.
{{ end }}
`

runGolden(t, name, files)
opts := imagetesting.DefaultGoldenOpts
opts.T = t
opts.Name = name
opts.Files = files

imagetesting.RunGolden(opts)
}

func TestGoldenProcessMisc(t *testing.T) {
func TestImagesGoldenProcessMisc(t *testing.T) {
t.Parallel()

if skipGolden {
if imagetesting.SkipGoldenTests {
t.Skip("Skip golden test on this architecture")
}

// Will be used to generate golden files.
name := "process_misc"
// Will be used as the base folder for generated images.
name := "process/misc"

files := `
-- hugo.toml --
Expand Down Expand Up @@ -277,180 +256,10 @@ Home.
{{ end }}
`

runGolden(t, name, files)
}

func TestGoldenFuncs(t *testing.T) {
t.Parallel()

if skipGolden {
t.Skip("Skip golden test on this architecture")
}

// Will be used to generate golden files.
name := "funcs"

files := `
-- hugo.toml --
-- assets/sunset.jpg --
sourcefilename: ../testdata/sunset.jpg

-- layouts/index.html --
Home.

{{ template "copy" (dict "name" "qr-default.png" "img" (images.QR "https://gohugo.io")) }}
{{ template "copy" (dict "name" "qr-level-high_scale-6.png" "img" (images.QR "https://gohugo.io" (dict "level" "high" "scale" 6))) }}

{{ define "copy"}}
{{ if lt (len (path.Ext .name)) 4 }}
{{ errorf "No extension in %q" .name }}
{{ end }}
{{ $img := .img }}
{{ $name := printf "images/%s" .name }}
{{ with $img | resources.Copy $name }}
{{ .Publish }}
{{ end }}
{{ end }}
`

runGolden(t, name, files)
}

func runGolden(t testing.TB, name, files string) *hugolib.IntegrationTestBuilder {
t.Helper()

c := hugolib.Test(t, files, hugolib.TestOptWithOSFs()) // hugolib.TestOptWithPrintAndKeepTempDir(true))
c.AssertFileContent("public/index.html", "Home.")

outputDir := filepath.Join(c.H.Conf.WorkingDir(), "public", "images")
goldenBaseDir := filepath.Join("testdata", "images_golden")
goldenDir := filepath.Join(goldenBaseDir, name)
if goldenOpts.writeGoldenFiles {
c.Assert(htesting.IsRealCI(), qt.IsFalse)
c.Assert(os.MkdirAll(goldenBaseDir, 0o777), qt.IsNil)
c.Assert(os.RemoveAll(goldenDir), qt.IsNil)
c.Assert(hugio.CopyDir(hugofs.Os, outputDir, goldenDir, nil), qt.IsNil)
return c
}
opts := imagetesting.DefaultGoldenOpts
opts.T = t
opts.Name = name
opts.Files = files

if goldenOpts.devMode {
c.Assert(htesting.IsRealCI(), qt.IsFalse)
return c
}

decodeAll := func(f *os.File) []image.Image {
c.Helper()

var images []image.Image

if strings.HasSuffix(f.Name(), ".gif") {
gif, err := gif.DecodeAll(f)
c.Assert(err, qt.IsNil, qt.Commentf(f.Name()))
images = make([]image.Image, len(gif.Image))
for i, img := range gif.Image {
images[i] = img
}
} else {
img, _, err := image.Decode(f)
c.Assert(err, qt.IsNil, qt.Commentf(f.Name()))
images = append(images, img)
}
return images
}

entries1, err := os.ReadDir(outputDir)
c.Assert(err, qt.IsNil)
entries2, err := os.ReadDir(goldenDir)
c.Assert(err, qt.IsNil)
c.Assert(len(entries1), qt.Equals, len(entries2))
for i, e1 := range entries1 {
c.Assert(filepath.Ext(e1.Name()), qt.Not(qt.Equals), "")
func() {
e2 := entries2[i]

f1, err := os.Open(filepath.Join(outputDir, e1.Name()))
c.Assert(err, qt.IsNil)
defer f1.Close()

f2, err := os.Open(filepath.Join(goldenDir, e2.Name()))
c.Assert(err, qt.IsNil)
defer f2.Close()

imgs2 := decodeAll(f2)
imgs1 := decodeAll(f1)
c.Assert(len(imgs1), qt.Equals, len(imgs2))

if !usesFMA {
c.Assert(e1, eq, e2)
_, err = f1.Seek(0, 0)
c.Assert(err, qt.IsNil)
_, err = f2.Seek(0, 0)
c.Assert(err, qt.IsNil)

hash1, _, err := hashing.XXHashFromReader(f1)
c.Assert(err, qt.IsNil)
hash2, _, err := hashing.XXHashFromReader(f2)
c.Assert(err, qt.IsNil)

c.Assert(hash1, qt.Equals, hash2)
}

for i, img1 := range imgs1 {
img2 := imgs2[i]
nrgba1 := image.NewNRGBA(img1.Bounds())
gift.New().Draw(nrgba1, img1)
nrgba2 := image.NewNRGBA(img2.Bounds())
gift.New().Draw(nrgba2, img2)
c.Assert(goldenEqual(nrgba1, nrgba2), qt.Equals, true, qt.Commentf(e1.Name()))
}
}()
}
return c
imagetesting.RunGolden(opts)
}

// goldenEqual compares two NRGBA images. It is used in golden tests only.
// A small tolerance is allowed on architectures using "fused multiply and add"
// (FMA) instruction to accommodate for floating-point rounding differences
// with control golden images that were generated on amd64 architecture.
// See https://golang.org/ref/spec#Floating_point_operators
// and https://github.com/gohugoio/hugo/issues/6387 for more information.
//
// Based on https://github.com/disintegration/gift/blob/a999ff8d5226e5ab14b64a94fca07c4ac3f357cf/gift_test.go#L598-L625
// Copyright (c) 2014-2019 Grigory Dryapak
// Licensed under the MIT License.
func goldenEqual(img1, img2 *image.NRGBA) bool {
maxDiff := 0
if runtime.GOARCH != "amd64" {
// The golden files are created using the AMD64 architecture.
// Be lenient on other platforms due to floaging point and dithering differences.
maxDiff = 15
}
if !img1.Rect.Eq(img2.Rect) {
return false
}
if len(img1.Pix) != len(img2.Pix) {
return false
}
for i := 0; i < len(img1.Pix); i++ {
diff := int(img1.Pix[i]) - int(img2.Pix[i])
if diff < 0 {
diff = -diff
}
if diff > maxDiff {
return false
}
}
return true
}

// We don't have a CI test environment for these, and there are known dithering issues that makes these time consuming to maintain.
var skipGolden = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "s390x"

// usesFMA indicates whether "fused multiply and add" (FMA) instruction is
// used. The command "grep FMADD go/test/codegen/floats.go" can help keep
// the FMA-using architecture list updated.
var usesFMA = runtime.GOARCH == "s390x" ||
runtime.GOARCH == "ppc64" ||
runtime.GOARCH == "ppc64le" ||
runtime.GOARCH == "arm64" ||
runtime.GOARCH == "riscv64"
Loading
Loading