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 }