hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

transform_test.go (14646B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package resources
   15 
   16 import (
   17 	"encoding/base64"
   18 	"fmt"
   19 	"io"
   20 	"path/filepath"
   21 	"strconv"
   22 	"strings"
   23 	"sync"
   24 	"testing"
   25 
   26 	"github.com/gohugoio/hugo/htesting"
   27 
   28 	"github.com/gohugoio/hugo/common/herrors"
   29 	"github.com/gohugoio/hugo/hugofs"
   30 
   31 	"github.com/gohugoio/hugo/media"
   32 	"github.com/gohugoio/hugo/resources/images"
   33 	"github.com/gohugoio/hugo/resources/internal"
   34 
   35 	"github.com/gohugoio/hugo/helpers"
   36 
   37 	"github.com/gohugoio/hugo/resources/resource"
   38 	"github.com/spf13/afero"
   39 
   40 	qt "github.com/frankban/quicktest"
   41 )
   42 
   43 const gopher = `iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDsJkoEpMrY/vO2BIYQ6LLvm0ThY3MzDzzeSJeeWNyTkgnIE5ePKsvKlcg/0T9QMzXalwXMlj54z4c0rh/mzEfr+FgWEz2w6uk8dkzFAgcARAgNp1ZYef8bH2AgvuStbc2/i6CiWGj98y2tw2l4FAXKkQBIf+exyRnteY83LfEwDQAYCoK+P6bxkZm/0966LxcAAILHB56kgD95PPxltuYcMtFTWw/FKkY/6Opf3GGd9ZF+Qp6mzJxzuRSractOmJrH1u8XTvWFHINNkLQLMR+XHXvfPPHw967raE1xxwtA36IMRfkAAG29/7mLuQcb2WOnsJReZGfpiHsSBX81cvMKywYZHhX5hFPtOqPGWZCXnhWGAu6lX91ElKXSalcLXu3UaOXVay57ZSe5f6Gpx7J2MXAsi7EqSp09b/MirKSyJfnfEEgeDjl8FgDAfvewP03zZ+AJ0m9aFRM8eEHBDRKjfcreDXnZdQuAxXpT2NRJ7xl3UkLBhuVGU16gZiGOgZmrSbRdqkILuL/yYoSXHHkl9KXgqNu3PB8oRg0geC5vFmLjad6mUyTKLmF3OtraWDIfACyXqmephaDABawfpi6tqqBZytfQMqOz6S09iWXhktrRaB8Xz4Yi/8gyABDm5NVe6qq/3VzPrcjELWrebVuyY2T7ar4zQyybUCtsQ5Es1FGaZVrRVQwAgHGW2ZCRZshI5bGQi7HesyE972pOSeMM0dSktlzxRdrlqb3Osa6CCS8IJoQQQgBAbTAa5l5epO34rJszibJI8rxLfGzcp1dRosutGeb2VDNgqYrwTiPNsLxXiPi3dz7LiS1WBRBDBOnqEjyy3aQb+/bLiJzz9dIkscVBBLxMfSEac7kO4Fpkngi0ruNBeSOal+u8jgOuqPz12nryMLCniEjtOOOmpt+KEIqsEdocJjYXwrh9OZqWJQyPCTo67LNS/TdxLAv6R5ZNK9npEjbYdT33gRo4o5oTqR34R+OmaSzDBWsAIPhuRcgyoteNi9gF0KzNYWVItPf2TLoXEg+7isNC7uJkgo1iQWOfRSP9NR11RtbZZ3OMG/VhL6jvx+J1m87+RCfJChAtEBQkSBX2PnSiihc/Twh3j0h7qdYQAoRVsRGmq7HU2QRbaxVGa1D6nIOqaIWRjyRZpHMQKWKpZM5feA+lzC4ZFultV8S6T0mzQGhQohi5I8iw+CsqBSxhFMuwyLgSwbghGb0AiIKkSDmGZVmJSiKihsiyOAUs70UkywooYP0bii9GdH4sfr1UNysd3fUyLLMQN+rsmo3grHl9VNJHbbwxoa47Vw5gupIqrZcjPh9R4Nye3nRDk199V+aetmvVtDRE8/+cbgAAgMIWGb3UA0MGLE9SCbWX670TDy1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2SnssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg==`
   44 
   45 func gopherPNG() io.Reader { return base64.NewDecoder(base64.StdEncoding, strings.NewReader(gopher)) }
   46 
   47 func TestTransform(t *testing.T) {
   48 	c := qt.New(t)
   49 
   50 	createTransformer := func(spec *Spec, filename, content string) Transformer {
   51 		filename = filepath.FromSlash(filename)
   52 		fs := spec.Fs.Source
   53 		afero.WriteFile(fs, filename, []byte(content), 0777)
   54 		r, _ := spec.New(ResourceSourceDescriptor{Fs: fs, SourceFilename: filename})
   55 		return r.(Transformer)
   56 	}
   57 
   58 	createContentReplacer := func(name, old, new string) ResourceTransformation {
   59 		return &testTransformation{
   60 			name: name,
   61 			transform: func(ctx *ResourceTransformationCtx) error {
   62 				in := helpers.ReaderToString(ctx.From)
   63 				in = strings.Replace(in, old, new, 1)
   64 				ctx.AddOutPathIdentifier("." + name)
   65 				fmt.Fprint(ctx.To, in)
   66 				return nil
   67 			},
   68 		}
   69 	}
   70 
   71 	// Verify that we publish the same file once only.
   72 	assertNoDuplicateWrites := func(c *qt.C, spec *Spec) {
   73 		c.Helper()
   74 		d := spec.Fs.PublishDir.(hugofs.DuplicatesReporter)
   75 		c.Assert(d.ReportDuplicates(), qt.Equals, "")
   76 	}
   77 
   78 	assertShouldExist := func(c *qt.C, spec *Spec, filename string, should bool) {
   79 		c.Helper()
   80 		exists, _ := helpers.Exists(filepath.FromSlash(filename), spec.Fs.WorkingDirReadOnly)
   81 		c.Assert(exists, qt.Equals, should)
   82 	}
   83 
   84 	c.Run("All values", func(c *qt.C) {
   85 		c.Parallel()
   86 
   87 		spec := newTestResourceSpec(specDescriptor{c: c})
   88 
   89 		transformation := &testTransformation{
   90 			name: "test",
   91 			transform: func(ctx *ResourceTransformationCtx) error {
   92 				// Content
   93 				in := helpers.ReaderToString(ctx.From)
   94 				in = strings.Replace(in, "blue", "green", 1)
   95 				fmt.Fprint(ctx.To, in)
   96 
   97 				// Media type
   98 				ctx.OutMediaType = media.CSVType
   99 
  100 				// Change target
  101 				ctx.ReplaceOutPathExtension(".csv")
  102 
  103 				// Add some data to context
  104 				ctx.Data["mydata"] = "Hugo Rocks!"
  105 
  106 				return nil
  107 			},
  108 		}
  109 
  110 		r := createTransformer(spec, "f1.txt", "color is blue")
  111 
  112 		tr, err := r.Transform(transformation)
  113 		c.Assert(err, qt.IsNil)
  114 		content, err := tr.(resource.ContentProvider).Content()
  115 		c.Assert(err, qt.IsNil)
  116 
  117 		c.Assert(content, qt.Equals, "color is green")
  118 		c.Assert(tr.MediaType(), eq, media.CSVType)
  119 		c.Assert(tr.RelPermalink(), qt.Equals, "/f1.csv")
  120 		assertShouldExist(c, spec, "public/f1.csv", true)
  121 
  122 		data := tr.Data().(map[string]any)
  123 		c.Assert(data["mydata"], qt.Equals, "Hugo Rocks!")
  124 
  125 		assertNoDuplicateWrites(c, spec)
  126 	})
  127 
  128 	c.Run("Meta only", func(c *qt.C) {
  129 		c.Parallel()
  130 
  131 		spec := newTestResourceSpec(specDescriptor{c: c})
  132 
  133 		transformation := &testTransformation{
  134 			name: "test",
  135 			transform: func(ctx *ResourceTransformationCtx) error {
  136 				// Change media type only
  137 				ctx.OutMediaType = media.CSVType
  138 				ctx.ReplaceOutPathExtension(".csv")
  139 
  140 				return nil
  141 			},
  142 		}
  143 
  144 		r := createTransformer(spec, "f1.txt", "color is blue")
  145 
  146 		tr, err := r.Transform(transformation)
  147 		c.Assert(err, qt.IsNil)
  148 		content, err := tr.(resource.ContentProvider).Content()
  149 		c.Assert(err, qt.IsNil)
  150 
  151 		c.Assert(content, qt.Equals, "color is blue")
  152 		c.Assert(tr.MediaType(), eq, media.CSVType)
  153 
  154 		// The transformed file should only be published if RelPermalink
  155 		// or Permalink is called.
  156 		n := htesting.Rnd.Intn(3)
  157 		shouldExist := true
  158 		switch n {
  159 		case 0:
  160 			tr.RelPermalink()
  161 		case 1:
  162 			tr.Permalink()
  163 		default:
  164 			shouldExist = false
  165 		}
  166 
  167 		assertShouldExist(c, spec, "public/f1.csv", shouldExist)
  168 		assertNoDuplicateWrites(c, spec)
  169 	})
  170 
  171 	c.Run("Memory-cached transformation", func(c *qt.C) {
  172 		c.Parallel()
  173 
  174 		spec := newTestResourceSpec(specDescriptor{c: c})
  175 
  176 		// Two transformations with same id, different behaviour.
  177 		t1 := createContentReplacer("t1", "blue", "green")
  178 		t2 := createContentReplacer("t1", "color", "car")
  179 
  180 		for i, transformation := range []ResourceTransformation{t1, t2} {
  181 			r := createTransformer(spec, "f1.txt", "color is blue")
  182 			tr, _ := r.Transform(transformation)
  183 			content, err := tr.(resource.ContentProvider).Content()
  184 			c.Assert(err, qt.IsNil)
  185 			c.Assert(content, qt.Equals, "color is green", qt.Commentf("i=%d", i))
  186 
  187 			assertShouldExist(c, spec, "public/f1.t1.txt", false)
  188 		}
  189 
  190 		assertNoDuplicateWrites(c, spec)
  191 	})
  192 
  193 	c.Run("File-cached transformation", func(c *qt.C) {
  194 		c.Parallel()
  195 
  196 		fs := afero.NewMemMapFs()
  197 
  198 		for i := 0; i < 2; i++ {
  199 			spec := newTestResourceSpec(specDescriptor{c: c, fs: fs})
  200 
  201 			r := createTransformer(spec, "f1.txt", "color is blue")
  202 
  203 			var transformation ResourceTransformation
  204 
  205 			if i == 0 {
  206 				// There is currently a hardcoded list of transformations that we
  207 				// persist to disk (tocss, postcss).
  208 				transformation = &testTransformation{
  209 					name: "tocss",
  210 					transform: func(ctx *ResourceTransformationCtx) error {
  211 						in := helpers.ReaderToString(ctx.From)
  212 						in = strings.Replace(in, "blue", "green", 1)
  213 						ctx.AddOutPathIdentifier("." + "cached")
  214 						ctx.OutMediaType = media.CSVType
  215 						ctx.Data = map[string]any{
  216 							"Hugo": "Rocks!",
  217 						}
  218 						fmt.Fprint(ctx.To, in)
  219 						return nil
  220 					},
  221 				}
  222 			} else {
  223 				// Force read from file cache.
  224 				transformation = &testTransformation{
  225 					name: "tocss",
  226 					transform: func(ctx *ResourceTransformationCtx) error {
  227 						return herrors.ErrFeatureNotAvailable
  228 					},
  229 				}
  230 			}
  231 
  232 			msg := qt.Commentf("i=%d", i)
  233 
  234 			tr, _ := r.Transform(transformation)
  235 			c.Assert(tr.RelPermalink(), qt.Equals, "/f1.cached.txt", msg)
  236 			content, err := tr.(resource.ContentProvider).Content()
  237 			c.Assert(err, qt.IsNil)
  238 			c.Assert(content, qt.Equals, "color is green", msg)
  239 			c.Assert(tr.MediaType(), eq, media.CSVType)
  240 			c.Assert(tr.Data(), qt.DeepEquals, map[string]any{
  241 				"Hugo": "Rocks!",
  242 			})
  243 
  244 			assertNoDuplicateWrites(c, spec)
  245 			assertShouldExist(c, spec, "public/f1.cached.txt", true)
  246 
  247 		}
  248 	})
  249 
  250 	c.Run("Access RelPermalink first", func(c *qt.C) {
  251 		c.Parallel()
  252 
  253 		spec := newTestResourceSpec(specDescriptor{c: c})
  254 
  255 		t1 := createContentReplacer("t1", "blue", "green")
  256 
  257 		r := createTransformer(spec, "f1.txt", "color is blue")
  258 
  259 		tr, _ := r.Transform(t1)
  260 
  261 		relPermalink := tr.RelPermalink()
  262 
  263 		content, err := tr.(resource.ContentProvider).Content()
  264 		c.Assert(err, qt.IsNil)
  265 
  266 		c.Assert(relPermalink, qt.Equals, "/f1.t1.txt")
  267 		c.Assert(content, qt.Equals, "color is green")
  268 		c.Assert(tr.MediaType(), eq, media.TextType)
  269 
  270 		assertNoDuplicateWrites(c, spec)
  271 		assertShouldExist(c, spec, "public/f1.t1.txt", true)
  272 	})
  273 
  274 	c.Run("Content two", func(c *qt.C) {
  275 		c.Parallel()
  276 
  277 		spec := newTestResourceSpec(specDescriptor{c: c})
  278 
  279 		t1 := createContentReplacer("t1", "blue", "green")
  280 		t2 := createContentReplacer("t1", "color", "car")
  281 
  282 		r := createTransformer(spec, "f1.txt", "color is blue")
  283 
  284 		tr, _ := r.Transform(t1, t2)
  285 		content, err := tr.(resource.ContentProvider).Content()
  286 		c.Assert(err, qt.IsNil)
  287 
  288 		c.Assert(content, qt.Equals, "car is green")
  289 		c.Assert(tr.MediaType(), eq, media.TextType)
  290 
  291 		assertNoDuplicateWrites(c, spec)
  292 	})
  293 
  294 	c.Run("Content two chained", func(c *qt.C) {
  295 		c.Parallel()
  296 
  297 		spec := newTestResourceSpec(specDescriptor{c: c})
  298 
  299 		t1 := createContentReplacer("t1", "blue", "green")
  300 		t2 := createContentReplacer("t2", "color", "car")
  301 
  302 		r := createTransformer(spec, "f1.txt", "color is blue")
  303 
  304 		tr1, _ := r.Transform(t1)
  305 		tr2, _ := tr1.Transform(t2)
  306 
  307 		content1, err := tr1.(resource.ContentProvider).Content()
  308 		c.Assert(err, qt.IsNil)
  309 		content2, err := tr2.(resource.ContentProvider).Content()
  310 		c.Assert(err, qt.IsNil)
  311 
  312 		c.Assert(content1, qt.Equals, "color is green")
  313 		c.Assert(content2, qt.Equals, "car is green")
  314 
  315 		assertNoDuplicateWrites(c, spec)
  316 	})
  317 
  318 	c.Run("Content many", func(c *qt.C) {
  319 		c.Parallel()
  320 
  321 		spec := newTestResourceSpec(specDescriptor{c: c})
  322 
  323 		const count = 26 // A-Z
  324 
  325 		transformations := make([]ResourceTransformation, count)
  326 		for i := 0; i < count; i++ {
  327 			transformations[i] = createContentReplacer(fmt.Sprintf("t%d", i), fmt.Sprint(i), string(rune(i+65)))
  328 		}
  329 
  330 		var countstr strings.Builder
  331 		for i := 0; i < count; i++ {
  332 			countstr.WriteString(fmt.Sprint(i))
  333 		}
  334 
  335 		r := createTransformer(spec, "f1.txt", countstr.String())
  336 
  337 		tr, _ := r.Transform(transformations...)
  338 		content, err := tr.(resource.ContentProvider).Content()
  339 		c.Assert(err, qt.IsNil)
  340 
  341 		c.Assert(content, qt.Equals, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
  342 
  343 		assertNoDuplicateWrites(c, spec)
  344 	})
  345 
  346 	c.Run("Image", func(c *qt.C) {
  347 		c.Parallel()
  348 
  349 		spec := newTestResourceSpec(specDescriptor{c: c})
  350 
  351 		transformation := &testTransformation{
  352 			name: "test",
  353 			transform: func(ctx *ResourceTransformationCtx) error {
  354 				ctx.AddOutPathIdentifier(".changed")
  355 				return nil
  356 			},
  357 		}
  358 
  359 		r := createTransformer(spec, "gopher.png", helpers.ReaderToString(gopherPNG()))
  360 
  361 		tr, err := r.Transform(transformation)
  362 		c.Assert(err, qt.IsNil)
  363 		c.Assert(tr.MediaType(), eq, media.PNGType)
  364 
  365 		img, ok := tr.(images.ImageResource)
  366 		c.Assert(ok, qt.Equals, true)
  367 
  368 		c.Assert(img.Width(), qt.Equals, 75)
  369 		c.Assert(img.Height(), qt.Equals, 60)
  370 
  371 		// RelPermalink called.
  372 		resizedPublished1, err := img.Resize("40x40")
  373 		c.Assert(err, qt.IsNil)
  374 		c.Assert(resizedPublished1.Height(), qt.Equals, 40)
  375 		c.Assert(resizedPublished1.RelPermalink(), qt.Equals, "/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_40x40_resize_linear_3.png")
  376 		assertShouldExist(c, spec, "public/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_40x40_resize_linear_3.png", true)
  377 
  378 		// Permalink called.
  379 		resizedPublished2, err := img.Resize("30x30")
  380 		c.Assert(err, qt.IsNil)
  381 		c.Assert(resizedPublished2.Height(), qt.Equals, 30)
  382 		c.Assert(resizedPublished2.Permalink(), qt.Equals, "https://example.com/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_30x30_resize_linear_3.png")
  383 		assertShouldExist(c, spec, "public/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_30x30_resize_linear_3.png", true)
  384 
  385 		// Not published because none of RelPermalink or Permalink was called.
  386 		resizedNotPublished, err := img.Resize("50x50")
  387 		c.Assert(err, qt.IsNil)
  388 		c.Assert(resizedNotPublished.Height(), qt.Equals, 50)
  389 		// c.Assert(resized.RelPermalink(), qt.Equals, "/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_50x50_resize_linear_2.png")
  390 		assertShouldExist(c, spec, "public/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_50x50_resize_linear_3.png", false)
  391 
  392 		assertNoDuplicateWrites(c, spec)
  393 	})
  394 
  395 	c.Run("Concurrent", func(c *qt.C) {
  396 		spec := newTestResourceSpec(specDescriptor{c: c})
  397 
  398 		transformers := make([]Transformer, 10)
  399 		transformations := make([]ResourceTransformation, 10)
  400 
  401 		for i := 0; i < 10; i++ {
  402 			transformers[i] = createTransformer(spec, fmt.Sprintf("f%d.txt", i), fmt.Sprintf("color is %d", i))
  403 			transformations[i] = createContentReplacer("test", strconv.Itoa(i), "blue")
  404 		}
  405 
  406 		var wg sync.WaitGroup
  407 
  408 		for i := 0; i < 13; i++ {
  409 			wg.Add(1)
  410 			go func(i int) {
  411 				defer wg.Done()
  412 				for j := 0; j < 23; j++ {
  413 					id := (i + j) % 10
  414 					tr, err := transformers[id].Transform(transformations[id])
  415 					c.Assert(err, qt.IsNil)
  416 					content, err := tr.(resource.ContentProvider).Content()
  417 					c.Assert(err, qt.IsNil)
  418 					c.Assert(content, qt.Equals, "color is blue")
  419 					c.Assert(tr.RelPermalink(), qt.Equals, fmt.Sprintf("/f%d.test.txt", id))
  420 				}
  421 			}(i)
  422 		}
  423 		wg.Wait()
  424 
  425 		assertNoDuplicateWrites(c, spec)
  426 	})
  427 }
  428 
  429 type testTransformation struct {
  430 	name      string
  431 	transform func(ctx *ResourceTransformationCtx) error
  432 }
  433 
  434 func (t *testTransformation) Key() internal.ResourceTransformationKey {
  435 	return internal.NewResourceTransformationKey(t.name)
  436 }
  437 
  438 func (t *testTransformation) Transform(ctx *ResourceTransformationCtx) error {
  439 	return t.transform(ctx)
  440 }