github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/transform_test.go (about)

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