github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/hugolib/pagecollections_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  	"fmt"
    18  	"math/rand"
    19  	"path"
    20  	"path/filepath"
    21  	"testing"
    22  	"time"
    23  
    24  	qt "github.com/frankban/quicktest"
    25  	"github.com/gohugoio/hugo/resources/page"
    26  
    27  	"github.com/gohugoio/hugo/deps"
    28  )
    29  
    30  const pageCollectionsPageTemplate = `---
    31  title: "%s"
    32  categories:
    33  - Hugo
    34  ---
    35  # Doc
    36  `
    37  
    38  func BenchmarkGetPage(b *testing.B) {
    39  	var (
    40  		cfg, fs = newTestCfg()
    41  		r       = rand.New(rand.NewSource(time.Now().UnixNano()))
    42  	)
    43  
    44  	for i := 0; i < 10; i++ {
    45  		for j := 0; j < 100; j++ {
    46  			writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), "CONTENT")
    47  		}
    48  	}
    49  
    50  	s := buildSingleSite(b, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
    51  
    52  	pagePaths := make([]string, b.N)
    53  
    54  	for i := 0; i < b.N; i++ {
    55  		pagePaths[i] = fmt.Sprintf("sect%d", r.Intn(10))
    56  	}
    57  
    58  	b.ResetTimer()
    59  	for i := 0; i < b.N; i++ {
    60  		home, _ := s.getPageNew(nil, "/")
    61  		if home == nil {
    62  			b.Fatal("Home is nil")
    63  		}
    64  
    65  		p, _ := s.getPageNew(nil, pagePaths[i])
    66  		if p == nil {
    67  			b.Fatal("Section is nil")
    68  		}
    69  
    70  	}
    71  }
    72  
    73  func createGetPageRegularBenchmarkSite(t testing.TB) *Site {
    74  	var (
    75  		c       = qt.New(t)
    76  		cfg, fs = newTestCfg()
    77  	)
    78  
    79  	pc := func(title string) string {
    80  		return fmt.Sprintf(pageCollectionsPageTemplate, title)
    81  	}
    82  
    83  	for i := 0; i < 10; i++ {
    84  		for j := 0; j < 100; j++ {
    85  			content := pc(fmt.Sprintf("Title%d_%d", i, j))
    86  			writeSource(c, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content)
    87  		}
    88  	}
    89  
    90  	return buildSingleSite(c, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
    91  }
    92  
    93  func TestBenchmarkGetPageRegular(t *testing.T) {
    94  	c := qt.New(t)
    95  	s := createGetPageRegularBenchmarkSite(t)
    96  
    97  	for i := 0; i < 10; i++ {
    98  		pp := path.Join("/", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", i))
    99  		page, _ := s.getPageNew(nil, pp)
   100  		c.Assert(page, qt.Not(qt.IsNil), qt.Commentf(pp))
   101  	}
   102  }
   103  
   104  func BenchmarkGetPageRegular(b *testing.B) {
   105  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   106  
   107  	b.Run("From root", func(b *testing.B) {
   108  		s := createGetPageRegularBenchmarkSite(b)
   109  		c := qt.New(b)
   110  
   111  		pagePaths := make([]string, b.N)
   112  
   113  		for i := 0; i < b.N; i++ {
   114  			pagePaths[i] = path.Join(fmt.Sprintf("/sect%d", r.Intn(10)), fmt.Sprintf("page%d.md", r.Intn(100)))
   115  		}
   116  
   117  		b.ResetTimer()
   118  		for i := 0; i < b.N; i++ {
   119  			page, _ := s.getPageNew(nil, pagePaths[i])
   120  			c.Assert(page, qt.Not(qt.IsNil))
   121  		}
   122  	})
   123  
   124  	b.Run("Page relative", func(b *testing.B) {
   125  		s := createGetPageRegularBenchmarkSite(b)
   126  		c := qt.New(b)
   127  		allPages := s.RegularPages()
   128  
   129  		pagePaths := make([]string, b.N)
   130  		pages := make([]page.Page, b.N)
   131  
   132  		for i := 0; i < b.N; i++ {
   133  			pagePaths[i] = fmt.Sprintf("page%d.md", r.Intn(100))
   134  			pages[i] = allPages[r.Intn(len(allPages)/3)]
   135  		}
   136  
   137  		b.ResetTimer()
   138  		for i := 0; i < b.N; i++ {
   139  			page, _ := s.getPageNew(pages[i], pagePaths[i])
   140  			c.Assert(page, qt.Not(qt.IsNil))
   141  		}
   142  	})
   143  }
   144  
   145  type getPageTest struct {
   146  	name          string
   147  	kind          string
   148  	context       page.Page
   149  	pathVariants  []string
   150  	expectedTitle string
   151  }
   152  
   153  func (t *getPageTest) check(p page.Page, err error, errorMsg string, c *qt.C) {
   154  	c.Helper()
   155  	errorComment := qt.Commentf(errorMsg)
   156  	switch t.kind {
   157  	case "Ambiguous":
   158  		c.Assert(err, qt.Not(qt.IsNil))
   159  		c.Assert(p, qt.IsNil, errorComment)
   160  	case "NoPage":
   161  		c.Assert(err, qt.IsNil)
   162  		c.Assert(p, qt.IsNil, errorComment)
   163  	default:
   164  		c.Assert(err, qt.IsNil, errorComment)
   165  		c.Assert(p, qt.Not(qt.IsNil), errorComment)
   166  		c.Assert(p.Kind(), qt.Equals, t.kind, errorComment)
   167  		c.Assert(p.Title(), qt.Equals, t.expectedTitle, errorComment)
   168  	}
   169  }
   170  
   171  func TestGetPage(t *testing.T) {
   172  	var (
   173  		cfg, fs = newTestCfg()
   174  		c       = qt.New(t)
   175  	)
   176  
   177  	pc := func(title string) string {
   178  		return fmt.Sprintf(pageCollectionsPageTemplate, title)
   179  	}
   180  
   181  	for i := 0; i < 10; i++ {
   182  		for j := 0; j < 10; j++ {
   183  			content := pc(fmt.Sprintf("Title%d_%d", i, j))
   184  			writeSource(t, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content)
   185  		}
   186  	}
   187  
   188  	content := pc("home page")
   189  	writeSource(t, fs, filepath.Join("content", "_index.md"), content)
   190  
   191  	content = pc("about page")
   192  	writeSource(t, fs, filepath.Join("content", "about.md"), content)
   193  
   194  	content = pc("section 3")
   195  	writeSource(t, fs, filepath.Join("content", "sect3", "_index.md"), content)
   196  
   197  	writeSource(t, fs, filepath.Join("content", "sect3", "unique.md"), pc("UniqueBase"))
   198  	writeSource(t, fs, filepath.Join("content", "sect3", "Unique2.md"), pc("UniqueBase2"))
   199  
   200  	content = pc("another sect7")
   201  	writeSource(t, fs, filepath.Join("content", "sect3", "sect7", "_index.md"), content)
   202  
   203  	content = pc("deep page")
   204  	writeSource(t, fs, filepath.Join("content", "sect3", "subsect", "deep.md"), content)
   205  
   206  	// Bundle variants
   207  	writeSource(t, fs, filepath.Join("content", "sect3", "b1", "index.md"), pc("b1 bundle"))
   208  	writeSource(t, fs, filepath.Join("content", "sect3", "index", "index.md"), pc("index bundle"))
   209  
   210  	writeSource(t, fs, filepath.Join("content", "section_bundle_overlap", "_index.md"), pc("index overlap section"))
   211  	writeSource(t, fs, filepath.Join("content", "section_bundle_overlap_bundle", "index.md"), pc("index overlap bundle"))
   212  
   213  	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
   214  
   215  	sec3, err := s.getPageNew(nil, "/sect3")
   216  	c.Assert(err, qt.IsNil)
   217  	c.Assert(sec3, qt.Not(qt.IsNil))
   218  
   219  	tests := []getPageTest{
   220  		// legacy content root relative paths
   221  		{"Root relative, no slash, home", page.KindHome, nil, []string{""}, "home page"},
   222  		{"Root relative, no slash, root page", page.KindPage, nil, []string{"about.md", "ABOUT.md"}, "about page"},
   223  		{"Root relative, no slash, section", page.KindSection, nil, []string{"sect3"}, "section 3"},
   224  		{"Root relative, no slash, section page", page.KindPage, nil, []string{"sect3/page1.md"}, "Title3_1"},
   225  		{"Root relative, no slash, sub setion", page.KindSection, nil, []string{"sect3/sect7"}, "another sect7"},
   226  		{"Root relative, no slash, nested page", page.KindPage, nil, []string{"sect3/subsect/deep.md"}, "deep page"},
   227  		{"Root relative, no slash, OS slashes", page.KindPage, nil, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"},
   228  
   229  		{"Short ref, unique", page.KindPage, nil, []string{"unique.md", "unique"}, "UniqueBase"},
   230  		{"Short ref, unique, upper case", page.KindPage, nil, []string{"Unique2.md", "unique2.md", "unique2"}, "UniqueBase2"},
   231  		{"Short ref, ambiguous", "Ambiguous", nil, []string{"page1.md"}, ""},
   232  
   233  		// ISSUE: This is an ambiguous ref, but because we have to support the legacy
   234  		// content root relative paths without a leading slash, the lookup
   235  		// returns /sect7. This undermines ambiguity detection, but we have no choice.
   236  		//{"Ambiguous", nil, []string{"sect7"}, ""},
   237  		{"Section, ambigous", page.KindSection, nil, []string{"sect7"}, "Sect7s"},
   238  
   239  		{"Absolute, home", page.KindHome, nil, []string{"/", ""}, "home page"},
   240  		{"Absolute, page", page.KindPage, nil, []string{"/about.md", "/about"}, "about page"},
   241  		{"Absolute, sect", page.KindSection, nil, []string{"/sect3"}, "section 3"},
   242  		{"Absolute, page in subsection", page.KindPage, nil, []string{"/sect3/page1.md", "/Sect3/Page1.md"}, "Title3_1"},
   243  		{"Absolute, section, subsection with same name", page.KindSection, nil, []string{"/sect3/sect7"}, "another sect7"},
   244  		{"Absolute, page, deep", page.KindPage, nil, []string{"/sect3/subsect/deep.md"}, "deep page"},
   245  		{"Absolute, page, OS slashes", page.KindPage, nil, []string{filepath.FromSlash("/sect5/page3.md")}, "Title5_3"}, // test OS-specific path
   246  		{"Absolute, unique", page.KindPage, nil, []string{"/sect3/unique.md"}, "UniqueBase"},
   247  		{"Absolute, unique, case", page.KindPage, nil, []string{"/sect3/Unique2.md", "/sect3/unique2.md", "/sect3/unique2", "/sect3/Unique2"}, "UniqueBase2"},
   248  		// next test depends on this page existing
   249  		// {"NoPage", nil, []string{"/unique.md"}, ""},  // ISSUE #4969: this is resolving to /sect3/unique.md
   250  		{"Absolute, missing page", "NoPage", nil, []string{"/missing-page.md"}, ""},
   251  		{"Absolute, missing section", "NoPage", nil, []string{"/missing-section"}, ""},
   252  
   253  		// relative paths
   254  		{"Dot relative, home", page.KindHome, sec3, []string{".."}, "home page"},
   255  		{"Dot relative, home, slash", page.KindHome, sec3, []string{"../"}, "home page"},
   256  		{"Dot relative about", page.KindPage, sec3, []string{"../about.md"}, "about page"},
   257  		{"Dot", page.KindSection, sec3, []string{"."}, "section 3"},
   258  		{"Dot slash", page.KindSection, sec3, []string{"./"}, "section 3"},
   259  		{"Page relative, no dot", page.KindPage, sec3, []string{"page1.md"}, "Title3_1"},
   260  		{"Page relative, dot", page.KindPage, sec3, []string{"./page1.md"}, "Title3_1"},
   261  		{"Up and down another section", page.KindPage, sec3, []string{"../sect4/page2.md"}, "Title4_2"},
   262  		{"Rel sect7", page.KindSection, sec3, []string{"sect7"}, "another sect7"},
   263  		{"Rel sect7 dot", page.KindSection, sec3, []string{"./sect7"}, "another sect7"},
   264  		{"Dot deep", page.KindPage, sec3, []string{"./subsect/deep.md"}, "deep page"},
   265  		{"Dot dot inner", page.KindPage, sec3, []string{"./subsect/../../sect7/page9.md"}, "Title7_9"},
   266  		{"Dot OS slash", page.KindPage, sec3, []string{filepath.FromSlash("../sect5/page3.md")}, "Title5_3"}, // test OS-specific path
   267  		{"Dot unique", page.KindPage, sec3, []string{"./unique.md"}, "UniqueBase"},
   268  		{"Dot sect", "NoPage", sec3, []string{"./sect2"}, ""},
   269  		//{"NoPage", sec3, []string{"sect2"}, ""}, // ISSUE: /sect3 page relative query is resolving to /sect2
   270  
   271  		{"Abs, ignore context, home", page.KindHome, sec3, []string{"/"}, "home page"},
   272  		{"Abs, ignore context, about", page.KindPage, sec3, []string{"/about.md"}, "about page"},
   273  		{"Abs, ignore context, page in section", page.KindPage, sec3, []string{"/sect4/page2.md"}, "Title4_2"},
   274  		{"Abs, ignore context, page subsect deep", page.KindPage, sec3, []string{"/sect3/subsect/deep.md"}, "deep page"}, // next test depends on this page existing
   275  		{"Abs, ignore context, page deep", "NoPage", sec3, []string{"/subsect/deep.md"}, ""},
   276  
   277  		// Taxonomies
   278  		{"Taxonomy term", page.KindTaxonomy, nil, []string{"categories"}, "Categories"},
   279  		{"Taxonomy", page.KindTerm, nil, []string{"categories/hugo", "categories/Hugo"}, "Hugo"},
   280  
   281  		// Bundle variants
   282  		{"Bundle regular", page.KindPage, nil, []string{"sect3/b1", "sect3/b1/index.md", "sect3/b1/index.en.md"}, "b1 bundle"},
   283  		{"Bundle index name", page.KindPage, nil, []string{"sect3/index/index.md", "sect3/index"}, "index bundle"},
   284  
   285  		// https://github.com/gohugoio/hugo/issues/7301
   286  		{"Section and bundle overlap", page.KindPage, nil, []string{"section_bundle_overlap_bundle"}, "index overlap bundle"},
   287  	}
   288  
   289  	for _, test := range tests {
   290  		c.Run(test.name, func(c *qt.C) {
   291  			errorMsg := fmt.Sprintf("Test case %v %v -> %s", test.context, test.pathVariants, test.expectedTitle)
   292  
   293  			// test legacy public Site.GetPage (which does not support page context relative queries)
   294  			if test.context == nil {
   295  				for _, ref := range test.pathVariants {
   296  					args := append([]string{test.kind}, ref)
   297  					page, err := s.Info.GetPage(args...)
   298  					test.check(page, err, errorMsg, c)
   299  				}
   300  			}
   301  
   302  			// test new internal Site.getPageNew
   303  			for _, ref := range test.pathVariants {
   304  				page2, err := s.getPageNew(test.context, ref)
   305  				test.check(page2, err, errorMsg, c)
   306  			}
   307  		})
   308  	}
   309  }
   310  
   311  // https://github.com/gohugoio/hugo/issues/6034
   312  func TestGetPageRelative(t *testing.T) {
   313  	b := newTestSitesBuilder(t)
   314  	for i, section := range []string{"what", "where", "who"} {
   315  		isDraft := i == 2
   316  		b.WithContent(
   317  			section+"/_index.md", fmt.Sprintf("---title: %s\n---", section),
   318  			section+"/members.md", fmt.Sprintf("---title: members %s\ndraft: %t\n---", section, isDraft),
   319  		)
   320  	}
   321  
   322  	b.WithTemplates("_default/list.html", `
   323  {{ with .GetPage "members.md" }}
   324      Members: {{ .Title }}
   325  {{ else }}
   326  NOT FOUND
   327  {{ end }}
   328  `)
   329  
   330  	b.Build(BuildCfg{})
   331  
   332  	b.AssertFileContent("public/what/index.html", `Members: members what`)
   333  	b.AssertFileContent("public/where/index.html", `Members: members where`)
   334  	b.AssertFileContent("public/who/index.html", `NOT FOUND`)
   335  }
   336  
   337  // https://github.com/gohugoio/hugo/issues/7016
   338  func TestGetPageMultilingual(t *testing.T) {
   339  	b := newTestSitesBuilder(t)
   340  
   341  	b.WithConfigFile("yaml", `
   342  baseURL: "http://example.org/"
   343  languageCode: "en-us"
   344  defaultContentLanguage: ru
   345  title: "My New Hugo Site"
   346  uglyurls: true
   347  
   348  languages:
   349    ru: {}
   350    en: {}
   351  `)
   352  
   353  	b.WithContent(
   354  		"docs/1.md", "\n---title: p1\n---",
   355  		"news/1.md", "\n---title: p1\n---",
   356  		"news/1.en.md", "\n---title: p1en\n---",
   357  		"news/about/1.md", "\n---title: about1\n---",
   358  		"news/about/1.en.md", "\n---title: about1en\n---",
   359  	)
   360  
   361  	b.WithTemplates("index.html", `
   362  {{ with site.GetPage "docs/1" }}
   363      Docs p1: {{ .Title }}
   364  {{ else }}
   365  NOT FOUND
   366  {{ end }}
   367  `)
   368  
   369  	b.Build(BuildCfg{})
   370  
   371  	b.AssertFileContent("public/index.html", `Docs p1: p1`)
   372  	b.AssertFileContent("public/en/index.html", `NOT FOUND`)
   373  }
   374  
   375  func TestShouldDoSimpleLookup(t *testing.T) {
   376  	c := qt.New(t)
   377  
   378  	c.Assert(shouldDoSimpleLookup("foo.md"), qt.Equals, true)
   379  	c.Assert(shouldDoSimpleLookup("/foo.md"), qt.Equals, true)
   380  	c.Assert(shouldDoSimpleLookup("./foo.md"), qt.Equals, false)
   381  	c.Assert(shouldDoSimpleLookup("docs/foo.md"), qt.Equals, false)
   382  }
   383  
   384  func TestRegularPagesRecursive(t *testing.T) {
   385  	b := newTestSitesBuilder(t)
   386  
   387  	b.WithConfigFile("yaml", `
   388  baseURL: "http://example.org/"
   389  title: "My New Hugo Site"
   390  
   391  `)
   392  
   393  	b.WithContent(
   394  		"docs/1.md", "\n---title: docs1\n---",
   395  		"docs/sect1/_index.md", "\n---title: docs_sect1\n---",
   396  		"docs/sect1/ps1.md", "\n---title: docs_sect1_ps1\n---",
   397  		"docs/sect1/ps2.md", "\n---title: docs_sect1_ps2\n---",
   398  		"docs/sect1/sect1_s2/_index.md", "\n---title: docs_sect1_s2\n---",
   399  		"docs/sect1/sect1_s2/ps2_1.md", "\n---title: docs_sect1_s2_1\n---",
   400  		"docs/sect2/_index.md", "\n---title: docs_sect2\n---",
   401  		"docs/sect2/ps1.md", "\n---title: docs_sect2_ps1\n---",
   402  		"docs/sect2/ps2.md", "\n---title: docs_sect2_ps2\n---",
   403  		"news/1.md", "\n---title: news1\n---",
   404  	)
   405  
   406  	b.WithTemplates("index.html", `
   407  {{ $sect1 := site.GetPage "sect1" }}
   408  
   409  Sect1 RegularPagesRecursive: {{ range $sect1.RegularPagesRecursive }}{{ .Kind }}:{{ .RelPermalink}}|{{ end }}|End.
   410  
   411  `)
   412  
   413  	b.Build(BuildCfg{})
   414  
   415  	b.AssertFileContent("public/index.html", `
   416  Sect1 RegularPagesRecursive: page:/docs/sect1/ps1/|page:/docs/sect1/ps2/|page:/docs/sect1/sect1_s2/ps2_1/||End.
   417  
   418  
   419  `)
   420  }