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