cascade_test.go (15597B)
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 hugolib 15 16 import ( 17 "bytes" 18 "fmt" 19 "path" 20 "strings" 21 "testing" 22 23 "github.com/gohugoio/hugo/common/maps" 24 25 qt "github.com/frankban/quicktest" 26 "github.com/gohugoio/hugo/parser" 27 "github.com/gohugoio/hugo/parser/metadecoders" 28 ) 29 30 func BenchmarkCascade(b *testing.B) { 31 allLangs := []string{"en", "nn", "nb", "sv", "ab", "aa", "af", "sq", "kw", "da"} 32 33 for i := 1; i <= len(allLangs); i += 2 { 34 langs := allLangs[0:i] 35 b.Run(fmt.Sprintf("langs-%d", len(langs)), func(b *testing.B) { 36 c := qt.New(b) 37 b.StopTimer() 38 builders := make([]*sitesBuilder, b.N) 39 for i := 0; i < b.N; i++ { 40 builders[i] = newCascadeTestBuilder(b, langs) 41 } 42 b.StartTimer() 43 44 for i := 0; i < b.N; i++ { 45 builder := builders[i] 46 err := builder.BuildE(BuildCfg{}) 47 c.Assert(err, qt.IsNil) 48 first := builder.H.Sites[0] 49 c.Assert(first, qt.Not(qt.IsNil)) 50 } 51 }) 52 } 53 } 54 55 func BenchmarkCascadeTarget(b *testing.B) { 56 files := ` 57 -- content/_index.md -- 58 background = 'yosemite.jpg' 59 [cascade._target] 60 kind = '{section,term}' 61 -- content/posts/_index.md -- 62 -- content/posts/funny/_index.md -- 63 ` 64 65 for i := 1; i < 100; i++ { 66 files += fmt.Sprintf("\n-- content/posts/p%d.md --\n", i+1) 67 } 68 69 for i := 1; i < 100; i++ { 70 files += fmt.Sprintf("\n-- content/posts/funny/pf%d.md --\n", i+1) 71 } 72 73 b.Run("Kind", func(b *testing.B) { 74 cfg := IntegrationTestConfig{ 75 T: b, 76 TxtarString: files, 77 } 78 builders := make([]*IntegrationTestBuilder, b.N) 79 80 for i := range builders { 81 builders[i] = NewIntegrationTestBuilder(cfg) 82 } 83 84 b.ResetTimer() 85 86 for i := 0; i < b.N; i++ { 87 builders[i].Build() 88 } 89 }) 90 } 91 92 func TestCascadeConfig(t *testing.T) { 93 c := qt.New(t) 94 95 // Make sure the cascade from config gets applied even if we're not 96 // having a content file for the home page. 97 for _, withHomeContent := range []bool{true, false} { 98 testName := "Home content file" 99 if !withHomeContent { 100 testName = "No home content file" 101 } 102 c.Run(testName, func(c *qt.C) { 103 b := newTestSitesBuilder(c) 104 105 b.WithConfigFile("toml", ` 106 baseURL="https://example.org" 107 108 [cascade] 109 img1 = "img1-config.jpg" 110 imgconfig = "img-config.jpg" 111 112 `) 113 114 if withHomeContent { 115 b.WithContent("_index.md", ` 116 --- 117 title: "Home" 118 cascade: 119 img1: "img1-home.jpg" 120 img2: "img2-home.jpg" 121 --- 122 `) 123 } 124 125 b.WithContent("p1.md", ``) 126 127 b.Build(BuildCfg{}) 128 129 p1 := b.H.Sites[0].getPage("p1") 130 131 if withHomeContent { 132 b.Assert(p1.Params(), qt.DeepEquals, maps.Params{ 133 "imgconfig": "img-config.jpg", 134 "draft": bool(false), 135 "iscjklanguage": bool(false), 136 "img1": "img1-home.jpg", 137 "img2": "img2-home.jpg", 138 }) 139 } else { 140 b.Assert(p1.Params(), qt.DeepEquals, maps.Params{ 141 "img1": "img1-config.jpg", 142 "imgconfig": "img-config.jpg", 143 "draft": bool(false), 144 "iscjklanguage": bool(false), 145 }) 146 } 147 }) 148 149 } 150 } 151 152 func TestCascade(t *testing.T) { 153 allLangs := []string{"en", "nn", "nb", "sv"} 154 155 langs := allLangs[:3] 156 157 t.Run(fmt.Sprintf("langs-%d", len(langs)), func(t *testing.T) { 158 b := newCascadeTestBuilder(t, langs) 159 b.Build(BuildCfg{}) 160 161 b.AssertFileContent("public/index.html", ` 162 12|term|categories/cool/_index.md|Cascade Category|cat.png|categories|HTML-| 163 12|term|categories/catsect1|catsect1|cat.png|categories|HTML-| 164 12|term|categories/funny|funny|cat.png|categories|HTML-| 165 12|taxonomy|categories/_index.md|My Categories|cat.png|categories|HTML-| 166 32|term|categories/sad/_index.md|Cascade Category|sad.png|categories|HTML-| 167 42|term|tags/blue|blue|home.png|tags|HTML-| 168 42|taxonomy|tags|Cascade Home|home.png|tags|HTML-| 169 42|section|sectnocontent|Cascade Home|home.png|sectnocontent|HTML-| 170 42|section|sect3|Cascade Home|home.png|sect3|HTML-| 171 42|page|bundle1/index.md|Cascade Home|home.png|page|HTML-| 172 42|page|p2.md|Cascade Home|home.png|page|HTML-| 173 42|page|sect2/p2.md|Cascade Home|home.png|sect2|HTML-| 174 42|page|sect3/nofrontmatter.md|Cascade Home|home.png|sect3|HTML-| 175 42|page|sect3/p1.md|Cascade Home|home.png|sect3|HTML-| 176 42|page|sectnocontent/p1.md|Cascade Home|home.png|sectnocontent|HTML-| 177 42|section|sectnofrontmatter/_index.md|Cascade Home|home.png|sectnofrontmatter|HTML-| 178 42|term|tags/green|green|home.png|tags|HTML-| 179 42|home|_index.md|Home|home.png|page|HTML-| 180 42|page|p1.md|p1|home.png|page|HTML-| 181 42|section|sect1/_index.md|Sect1|sect1.png|stype|HTML-| 182 42|section|sect1/s1_2/_index.md|Sect1_2|sect1.png|stype|HTML-| 183 42|page|sect1/s1_2/p1.md|Sect1_2_p1|sect1.png|stype|HTML-| 184 42|page|sect1/s1_2/p2.md|Sect1_2_p2|sect1.png|stype|HTML-| 185 42|section|sect2/_index.md|Sect2|home.png|sect2|HTML-| 186 42|page|sect2/p1.md|Sect2_p1|home.png|sect2|HTML-| 187 52|page|sect4/p1.md|Cascade Home|home.png|sect4|RSS-| 188 52|section|sect4/_index.md|Sect4|home.png|sect4|RSS-| 189 `) 190 191 // Check that type set in cascade gets the correct layout. 192 b.AssertFileContent("public/sect1/index.html", `stype list: Sect1`) 193 b.AssertFileContent("public/sect1/s1_2/p2/index.html", `stype single: Sect1_2_p2`) 194 195 // Check output formats set in cascade 196 b.AssertFileContent("public/sect4/index.xml", `<link>https://example.org/sect4/index.xml</link>`) 197 b.AssertFileContent("public/sect4/p1/index.xml", `<link>https://example.org/sect4/p1/index.xml</link>`) 198 b.C.Assert(b.CheckExists("public/sect2/index.xml"), qt.Equals, false) 199 200 // Check cascade into bundled page 201 b.AssertFileContent("public/bundle1/index.html", `Resources: bp1.md|home.png|`) 202 }) 203 } 204 205 func TestCascadeEdit(t *testing.T) { 206 p1Content := `--- 207 title: P1 208 --- 209 ` 210 211 indexContentNoCascade := ` 212 --- 213 title: Home 214 --- 215 ` 216 217 indexContentCascade := ` 218 --- 219 title: Section 220 cascade: 221 banner: post.jpg 222 layout: postlayout 223 type: posttype 224 --- 225 ` 226 227 layout := `Banner: {{ .Params.banner }}|Layout: {{ .Layout }}|Type: {{ .Type }}|Content: {{ .Content }}` 228 229 newSite := func(t *testing.T, cascade bool) *sitesBuilder { 230 b := newTestSitesBuilder(t).Running() 231 b.WithTemplates("_default/single.html", layout) 232 b.WithTemplates("_default/list.html", layout) 233 if cascade { 234 b.WithContent("post/_index.md", indexContentCascade) 235 } else { 236 b.WithContent("post/_index.md", indexContentNoCascade) 237 } 238 b.WithContent("post/dir/p1.md", p1Content) 239 240 return b 241 } 242 243 t.Run("Edit descendant", func(t *testing.T) { 244 t.Parallel() 245 246 b := newSite(t, true) 247 b.Build(BuildCfg{}) 248 249 assert := func() { 250 b.Helper() 251 b.AssertFileContent("public/post/dir/p1/index.html", 252 `Banner: post.jpg|`, 253 `Layout: postlayout`, 254 `Type: posttype`, 255 ) 256 } 257 258 assert() 259 260 b.EditFiles("content/post/dir/p1.md", p1Content+"\ncontent edit") 261 b.Build(BuildCfg{}) 262 263 assert() 264 b.AssertFileContent("public/post/dir/p1/index.html", 265 `content edit 266 Banner: post.jpg`, 267 ) 268 }) 269 270 t.Run("Edit ancestor", func(t *testing.T) { 271 t.Parallel() 272 273 b := newSite(t, true) 274 b.Build(BuildCfg{}) 275 276 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content:`) 277 278 b.EditFiles("content/post/_index.md", strings.Replace(indexContentCascade, "post.jpg", "edit.jpg", 1)) 279 280 b.Build(BuildCfg{}) 281 282 b.AssertFileContent("public/post/index.html", `Banner: edit.jpg|Layout: postlayout|Type: posttype|`) 283 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: edit.jpg|Layout: postlayout|Type: posttype|`) 284 }) 285 286 t.Run("Edit ancestor, add cascade", func(t *testing.T) { 287 t.Parallel() 288 289 b := newSite(t, true) 290 b.Build(BuildCfg{}) 291 292 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg`) 293 294 b.EditFiles("content/post/_index.md", indexContentCascade) 295 296 b.Build(BuildCfg{}) 297 298 b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|`) 299 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`) 300 }) 301 302 t.Run("Edit ancestor, remove cascade", func(t *testing.T) { 303 t.Parallel() 304 305 b := newSite(t, false) 306 b.Build(BuildCfg{}) 307 308 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: |Layout: |`) 309 310 b.EditFiles("content/post/_index.md", indexContentNoCascade) 311 312 b.Build(BuildCfg{}) 313 314 b.AssertFileContent("public/post/index.html", `Banner: |Layout: |Type: post|`) 315 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: |Layout: |`) 316 }) 317 318 t.Run("Edit ancestor, content only", func(t *testing.T) { 319 t.Parallel() 320 321 b := newSite(t, true) 322 b.Build(BuildCfg{}) 323 324 b.EditFiles("content/post/_index.md", indexContentCascade+"\ncontent edit") 325 326 counters := &testCounters{} 327 b.Build(BuildCfg{testCounters: counters}) 328 // As we only changed the content, not the cascade front matter, 329 // only the home page is re-rendered. 330 b.Assert(int(counters.contentRenderCounter), qt.Equals, 1) 331 332 b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content: <p>content edit</p>`) 333 b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`) 334 }) 335 } 336 337 func newCascadeTestBuilder(t testing.TB, langs []string) *sitesBuilder { 338 p := func(m map[string]any) string { 339 var yamlStr string 340 341 if len(m) > 0 { 342 var b bytes.Buffer 343 344 parser.InterfaceToConfig(m, metadecoders.YAML, &b) 345 yamlStr = b.String() 346 } 347 348 metaStr := "---\n" + yamlStr + "\n---" 349 350 return metaStr 351 } 352 353 createLangConfig := func(lang string) string { 354 const langEntry = ` 355 [languages.%s] 356 ` 357 return fmt.Sprintf(langEntry, lang) 358 } 359 360 createMount := func(lang string) string { 361 const mountsTempl = ` 362 [[module.mounts]] 363 source="content/%s" 364 target="content" 365 lang="%s" 366 ` 367 return fmt.Sprintf(mountsTempl, lang, lang) 368 } 369 370 config := ` 371 baseURL = "https://example.org" 372 defaultContentLanguage = "en" 373 defaultContentLanguageInSubDir = false 374 375 [languages]` 376 for _, lang := range langs { 377 config += createLangConfig(lang) 378 } 379 380 config += "\n\n[module]\n" 381 for _, lang := range langs { 382 config += createMount(lang) 383 } 384 385 b := newTestSitesBuilder(t).WithConfigFile("toml", config) 386 387 createContentFiles := func(lang string) { 388 withContent := func(filenameContent ...string) { 389 for i := 0; i < len(filenameContent); i += 2 { 390 b.WithContent(path.Join(lang, filenameContent[i]), filenameContent[i+1]) 391 } 392 } 393 394 withContent( 395 "_index.md", p(map[string]any{ 396 "title": "Home", 397 "cascade": map[string]any{ 398 "title": "Cascade Home", 399 "ICoN": "home.png", 400 "outputs": []string{"HTML"}, 401 "weight": 42, 402 }, 403 }), 404 "p1.md", p(map[string]any{ 405 "title": "p1", 406 }), 407 "p2.md", p(map[string]any{}), 408 "sect1/_index.md", p(map[string]any{ 409 "title": "Sect1", 410 "type": "stype", 411 "cascade": map[string]any{ 412 "title": "Cascade Sect1", 413 "icon": "sect1.png", 414 "type": "stype", 415 "categories": []string{"catsect1"}, 416 }, 417 }), 418 "sect1/s1_2/_index.md", p(map[string]any{ 419 "title": "Sect1_2", 420 }), 421 "sect1/s1_2/p1.md", p(map[string]any{ 422 "title": "Sect1_2_p1", 423 }), 424 "sect1/s1_2/p2.md", p(map[string]any{ 425 "title": "Sect1_2_p2", 426 }), 427 "sect2/_index.md", p(map[string]any{ 428 "title": "Sect2", 429 }), 430 "sect2/p1.md", p(map[string]any{ 431 "title": "Sect2_p1", 432 "categories": []string{"cool", "funny", "sad"}, 433 "tags": []string{"blue", "green"}, 434 }), 435 "sect2/p2.md", p(map[string]any{}), 436 "sect3/p1.md", p(map[string]any{}), 437 438 // No front matter, see #6855 439 "sect3/nofrontmatter.md", `**Hello**`, 440 "sectnocontent/p1.md", `**Hello**`, 441 "sectnofrontmatter/_index.md", `**Hello**`, 442 443 "sect4/_index.md", p(map[string]any{ 444 "title": "Sect4", 445 "cascade": map[string]any{ 446 "weight": 52, 447 "outputs": []string{"RSS"}, 448 }, 449 }), 450 "sect4/p1.md", p(map[string]any{}), 451 "p2.md", p(map[string]any{}), 452 "bundle1/index.md", p(map[string]any{}), 453 "bundle1/bp1.md", p(map[string]any{}), 454 "categories/_index.md", p(map[string]any{ 455 "title": "My Categories", 456 "cascade": map[string]any{ 457 "title": "Cascade Category", 458 "icoN": "cat.png", 459 "weight": 12, 460 }, 461 }), 462 "categories/cool/_index.md", p(map[string]any{}), 463 "categories/sad/_index.md", p(map[string]any{ 464 "cascade": map[string]any{ 465 "icon": "sad.png", 466 "weight": 32, 467 }, 468 }), 469 ) 470 } 471 472 createContentFiles("en") 473 474 b.WithTemplates("index.html", ` 475 476 {{ range .Site.Pages }} 477 {{- .Weight }}|{{ .Kind }}|{{ path.Join .Path }}|{{ .Title }}|{{ .Params.icon }}|{{ .Type }}|{{ range .OutputFormats }}{{ .Name }}-{{ end }}| 478 {{ end }} 479 `, 480 481 "_default/single.html", "default single: {{ .Title }}|{{ .RelPermalink }}|{{ .Content }}|Resources: {{ range .Resources }}{{ .Name }}|{{ .Params.icon }}|{{ .Content }}{{ end }}", 482 "_default/list.html", "default list: {{ .Title }}", 483 "stype/single.html", "stype single: {{ .Title }}|{{ .RelPermalink }}|{{ .Content }}", 484 "stype/list.html", "stype list: {{ .Title }}", 485 ) 486 487 return b 488 } 489 490 func TestCascadeTarget(t *testing.T) { 491 t.Parallel() 492 493 c := qt.New(t) 494 495 newBuilder := func(c *qt.C) *sitesBuilder { 496 b := newTestSitesBuilder(c) 497 498 b.WithTemplates("index.html", ` 499 {{ $p1 := site.GetPage "s1/p1" }} 500 {{ $s1 := site.GetPage "s1" }} 501 502 P1|p1:{{ $p1.Params.p1 }}|p2:{{ $p1.Params.p2 }}| 503 S1|p1:{{ $s1.Params.p1 }}|p2:{{ $s1.Params.p2 }}| 504 `) 505 b.WithContent("s1/_index.md", "---\ntitle: s1 section\n---") 506 b.WithContent("s1/p1/index.md", "---\ntitle: p1\n---") 507 b.WithContent("s1/p2/index.md", "---\ntitle: p2\n---") 508 b.WithContent("s2/p1/index.md", "---\ntitle: p1_2\n---") 509 510 return b 511 } 512 513 c.Run("slice", func(c *qt.C) { 514 b := newBuilder(c) 515 b.WithContent("_index.md", `+++ 516 title = "Home" 517 [[cascade]] 518 p1 = "p1" 519 [[cascade]] 520 p2 = "p2" 521 +++ 522 `) 523 524 b.Build(BuildCfg{}) 525 526 b.AssertFileContent("public/index.html", "P1|p1:p1|p2:p2") 527 }) 528 529 c.Run("slice with _target", func(c *qt.C) { 530 b := newBuilder(c) 531 532 b.WithContent("_index.md", `+++ 533 title = "Home" 534 [[cascade]] 535 p1 = "p1" 536 [cascade._target] 537 path="**p1**" 538 [[cascade]] 539 p2 = "p2" 540 [cascade._target] 541 kind="section" 542 +++ 543 `) 544 545 b.Build(BuildCfg{}) 546 547 b.AssertFileContent("public/index.html", ` 548 P1|p1:p1|p2:| 549 S1|p1:|p2:p2| 550 `) 551 }) 552 553 c.Run("slice with environment _target", func(c *qt.C) { 554 b := newBuilder(c) 555 556 b.WithContent("_index.md", `+++ 557 title = "Home" 558 [[cascade]] 559 p1 = "p1" 560 [cascade._target] 561 path="**p1**" 562 environment="testing" 563 [[cascade]] 564 p2 = "p2" 565 [cascade._target] 566 kind="section" 567 environment="production" 568 +++ 569 `) 570 571 b.Build(BuildCfg{}) 572 573 b.AssertFileContent("public/index.html", ` 574 P1|p1:|p2:| 575 S1|p1:|p2:p2| 576 `) 577 }) 578 579 c.Run("slice with yaml _target", func(c *qt.C) { 580 b := newBuilder(c) 581 582 b.WithContent("_index.md", `--- 583 title: "Home" 584 cascade: 585 - p1: p1 586 _target: 587 path: "**p1**" 588 - p2: p2 589 _target: 590 kind: "section" 591 --- 592 `) 593 594 b.Build(BuildCfg{}) 595 596 b.AssertFileContent("public/index.html", ` 597 P1|p1:p1|p2:| 598 S1|p1:|p2:p2| 599 `) 600 }) 601 602 c.Run("slice with json _target", func(c *qt.C) { 603 b := newBuilder(c) 604 605 b.WithContent("_index.md", `{ 606 "title": "Home", 607 "cascade": [ 608 { 609 "p1": "p1", 610 "_target": { 611 "path": "**p1**" 612 } 613 },{ 614 "p2": "p2", 615 "_target": { 616 "kind": "section" 617 } 618 } 619 ] 620 } 621 `) 622 623 b.Build(BuildCfg{}) 624 625 b.AssertFileContent("public/index.html", ` 626 P1|p1:p1|p2:| 627 S1|p1:|p2:p2| 628 `) 629 }) 630 }