resources/images: Refactor golden image tests to locate them closer to the implementation
|
@ -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 --
|
||||
|
@ -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
|
||||
|
@ -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 --
|
||||
|
@ -163,7 +127,12 @@ 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 }}
|
||||
|
@ -171,7 +140,7 @@ Home.
|
|||
{{ 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 }}
|
||||
|
@ -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 --
|
||||
|
@ -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 --
|
||||
|
@ -277,180 +256,10 @@ Home.
|
|||
{{ end }}
|
||||
`
|
||||
|
||||
runGolden(t, name, files)
|
||||
opts := imagetesting.DefaultGoldenOpts
|
||||
opts.T = t
|
||||
opts.Name = name
|
||||
opts.Files = files
|
||||
|
||||
imagetesting.RunGolden(opts)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
|
215
resources/images/imagetesting/testing.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package imagetesting
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/gif"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/disintegration/gift"
|
||||
"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"
|
||||
)
|
||||
|
||||
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()
|
||||
}),
|
||||
)
|
||||
|
||||
// GoldenImageTestOpts provides options for a golden image test.
|
||||
type GoldenImageTestOpts struct {
|
||||
// The test.
|
||||
T testing.TB
|
||||
|
||||
// Name of the test. Will be used as the base folder for generated images.
|
||||
// Slashes allowed and encouraged.
|
||||
Name string
|
||||
|
||||
// The test site's files in txttar format.
|
||||
Files string
|
||||
|
||||
// Set to true to write golden files to disk.
|
||||
WriteFiles bool
|
||||
|
||||
// Set to true to skip any assertions. Useful when adding new golden variants to a test.
|
||||
DevMode bool
|
||||
}
|
||||
|
||||
// To rebuild all Golden image tests, toggle WriteFiles=true and run:
|
||||
// GOARCH=amd64 go test -count 1 -timeout 30s -run "^TestImagesGolden" ./...
|
||||
// TODO(bep) see if we can do this via flags.
|
||||
var DefaultGoldenOpts = GoldenImageTestOpts{
|
||||
WriteFiles: false,
|
||||
DevMode: false,
|
||||
}
|
||||
|
||||
func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder {
|
||||
opts.T.Helper()
|
||||
|
||||
c := hugolib.Test(opts.T, opts.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, filepath.FromSlash(opts.Name))
|
||||
if opts.WriteFiles {
|
||||
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
|
||||
}
|
||||
|
||||
if opts.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
|
||||
}
|
||||
|
||||
// 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 SkipGoldenTests = 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"
|
BIN
resources/images/testdata/images_golden/filters/mask/blue.jpg
vendored
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
resources/images/testdata/images_golden/filters/mask/transparant.png
vendored
Normal file
After Width: | Height: | Size: 147 KiB |
Before Width: | Height: | Size: 8 KiB After Width: | Height: | Size: 8 KiB |
BIN
resources/images/testdata/images_golden/filters/mask/yellow.jpg
vendored
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/brightness-40.jpg
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/colorbalance-180-50-20.jpg
vendored
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/contrast-50.jpg
vendored
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/dither-default.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/gamma-1.667.jpg
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/gaussianblur-5.jpg
vendored
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/grayscale+colorize-180-50-20.jpg
vendored
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/grayscale.jpg
vendored
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/hue--15.jpg
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/invert.jpg
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/opacity-0.65.jpg
vendored
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/overlay-20-20.jpg
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/padding-20-40-#976941.jpg
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/pixelate-10.jpg
vendored
Normal file
After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 688 B After Width: | Height: | Size: 688 B |
BIN
resources/images/testdata/images_golden/filters/misc/saturation-65.jpg
vendored
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/sepia-80.jpg
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/sigmoid-0.6--4.jpg
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/text.jpg
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
resources/images/testdata/images_golden/filters/misc/unsharpmask.jpg
vendored
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 6 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 206 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 8 KiB After Width: | Height: | Size: 8 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
@ -19,6 +19,7 @@ import (
|
|||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/resources/images/imagetesting"
|
||||
)
|
||||
|
||||
func TestImageConfigFromModule(t *testing.T) {
|
||||
|
@ -100,3 +101,44 @@ disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
|||
b, err = hugolib.TestE(t, files)
|
||||
b.Assert(err.Error(), qt.Contains, "cannot encode an empty string")
|
||||
}
|
||||
|
||||
func TestImagesGoldenFuncs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if imagetesting.SkipGoldenTests {
|
||||
t.Skip("Skip golden test on this architecture")
|
||||
}
|
||||
|
||||
// Will be used as the base folder for generated images.
|
||||
name := "funcs"
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/sunset.jpg --
|
||||
sourcefilename: ../../resources/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 }}
|
||||
`
|
||||
|
||||
opts := imagetesting.DefaultGoldenOpts
|
||||
opts.T = t
|
||||
opts.Name = name
|
||||
opts.Files = files
|
||||
|
||||
imagetesting.RunGolden(opts)
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 663 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |