github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/hugolib/cascade_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 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]interface{}) 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]interface{}{
   396  				"title": "Home",
   397  				"cascade": map[string]interface{}{
   398  					"title":   "Cascade Home",
   399  					"ICoN":    "home.png",
   400  					"outputs": []string{"HTML"},
   401  					"weight":  42,
   402  				},
   403  			}),
   404  			"p1.md", p(map[string]interface{}{
   405  				"title": "p1",
   406  			}),
   407  			"p2.md", p(map[string]interface{}{}),
   408  			"sect1/_index.md", p(map[string]interface{}{
   409  				"title": "Sect1",
   410  				"type":  "stype",
   411  				"cascade": map[string]interface{}{
   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]interface{}{
   419  				"title": "Sect1_2",
   420  			}),
   421  			"sect1/s1_2/p1.md", p(map[string]interface{}{
   422  				"title": "Sect1_2_p1",
   423  			}),
   424  			"sect1/s1_2/p2.md", p(map[string]interface{}{
   425  				"title": "Sect1_2_p2",
   426  			}),
   427  			"sect2/_index.md", p(map[string]interface{}{
   428  				"title": "Sect2",
   429  			}),
   430  			"sect2/p1.md", p(map[string]interface{}{
   431  				"title":      "Sect2_p1",
   432  				"categories": []string{"cool", "funny", "sad"},
   433  				"tags":       []string{"blue", "green"},
   434  			}),
   435  			"sect2/p2.md", p(map[string]interface{}{}),
   436  			"sect3/p1.md", p(map[string]interface{}{}),
   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]interface{}{
   444  				"title": "Sect4",
   445  				"cascade": map[string]interface{}{
   446  					"weight":  52,
   447  					"outputs": []string{"RSS"},
   448  				},
   449  			}),
   450  			"sect4/p1.md", p(map[string]interface{}{}),
   451  			"p2.md", p(map[string]interface{}{}),
   452  			"bundle1/index.md", p(map[string]interface{}{}),
   453  			"bundle1/bp1.md", p(map[string]interface{}{}),
   454  			"categories/_index.md", p(map[string]interface{}{
   455  				"title": "My Categories",
   456  				"cascade": map[string]interface{}{
   457  					"title":  "Cascade Category",
   458  					"icoN":   "cat.png",
   459  					"weight": 12,
   460  				},
   461  			}),
   462  			"categories/cool/_index.md", p(map[string]interface{}{}),
   463  			"categories/sad/_index.md", p(map[string]interface{}{
   464  				"cascade": map[string]interface{}{
   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 yaml _target", func(c *qt.C) {
   554  		b := newBuilder(c)
   555  
   556  		b.WithContent("_index.md", `---
   557  title: "Home"
   558  cascade:
   559  - p1: p1
   560    _target:
   561      path: "**p1**"
   562  - p2: p2
   563    _target:
   564      kind: "section"
   565  ---
   566  `)
   567  
   568  		b.Build(BuildCfg{})
   569  
   570  		b.AssertFileContent("public/index.html", `
   571  P1|p1:p1|p2:|
   572  S1|p1:|p2:p2|
   573  `)
   574  	})
   575  
   576  	c.Run("slice with json _target", func(c *qt.C) {
   577  		b := newBuilder(c)
   578  
   579  		b.WithContent("_index.md", `{
   580  "title": "Home",
   581  "cascade": [
   582    {
   583      "p1": "p1",
   584  	"_target": {
   585  	  "path": "**p1**"
   586      }
   587    },{
   588      "p2": "p2",
   589  	"_target": {
   590        "kind": "section"
   591      }
   592    }
   593  ]
   594  }
   595  `)
   596  
   597  		b.Build(BuildCfg{})
   598  
   599  		b.AssertFileContent("public/index.html", `
   600  		P1|p1:p1|p2:|
   601  		S1|p1:|p2:p2|
   602  		`)
   603  	})
   604  }