content_map_test.go (15933B)
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 "path/filepath" 19 "strings" 20 "testing" 21 22 "github.com/gohugoio/hugo/common/paths" 23 24 "github.com/gohugoio/hugo/htesting/hqt" 25 26 "github.com/gohugoio/hugo/hugofs/files" 27 28 "github.com/gohugoio/hugo/hugofs" 29 "github.com/spf13/afero" 30 31 qt "github.com/frankban/quicktest" 32 ) 33 34 func BenchmarkContentMap(b *testing.B) { 35 writeFile := func(c *qt.C, fs afero.Fs, filename, content string) hugofs.FileMetaInfo { 36 c.Helper() 37 filename = filepath.FromSlash(filename) 38 c.Assert(fs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil) 39 c.Assert(afero.WriteFile(fs, filename, []byte(content), 0777), qt.IsNil) 40 41 fi, err := fs.Stat(filename) 42 c.Assert(err, qt.IsNil) 43 44 mfi := fi.(hugofs.FileMetaInfo) 45 return mfi 46 } 47 48 createFs := func(fs afero.Fs, lang string) afero.Fs { 49 return hugofs.NewBaseFileDecorator(fs, 50 func(fi hugofs.FileMetaInfo) { 51 meta := fi.Meta() 52 // We have a more elaborate filesystem setup in the 53 // real flow, so simulate this here. 54 meta.Lang = lang 55 meta.Path = meta.Filename 56 meta.Classifier = files.ClassifyContentFile(fi.Name(), meta.OpenFunc) 57 }) 58 } 59 60 b.Run("CreateMissingNodes", func(b *testing.B) { 61 c := qt.New(b) 62 b.StopTimer() 63 mps := make([]*contentMap, b.N) 64 for i := 0; i < b.N; i++ { 65 m := newContentMap(contentMapConfig{lang: "en"}) 66 mps[i] = m 67 memfs := afero.NewMemMapFs() 68 fs := createFs(memfs, "en") 69 for i := 1; i <= 20; i++ { 70 c.Assert(m.AddFilesBundle(writeFile(c, fs, fmt.Sprintf("sect%d/a/index.md", i), "page")), qt.IsNil) 71 c.Assert(m.AddFilesBundle(writeFile(c, fs, fmt.Sprintf("sect2%d/%sindex.md", i, strings.Repeat("b/", i)), "page")), qt.IsNil) 72 } 73 74 } 75 76 b.StartTimer() 77 78 for i := 0; i < b.N; i++ { 79 m := mps[i] 80 c.Assert(m.CreateMissingNodes(), qt.IsNil) 81 82 b.StopTimer() 83 m.pages.DeletePrefix("/") 84 m.sections.DeletePrefix("/") 85 b.StartTimer() 86 } 87 }) 88 } 89 90 func TestContentMap(t *testing.T) { 91 c := qt.New(t) 92 93 writeFile := func(c *qt.C, fs afero.Fs, filename, content string) hugofs.FileMetaInfo { 94 c.Helper() 95 filename = filepath.FromSlash(filename) 96 c.Assert(fs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil) 97 c.Assert(afero.WriteFile(fs, filename, []byte(content), 0777), qt.IsNil) 98 99 fi, err := fs.Stat(filename) 100 c.Assert(err, qt.IsNil) 101 102 mfi := fi.(hugofs.FileMetaInfo) 103 return mfi 104 } 105 106 createFs := func(fs afero.Fs, lang string) afero.Fs { 107 return hugofs.NewBaseFileDecorator(fs, 108 func(fi hugofs.FileMetaInfo) { 109 meta := fi.Meta() 110 // We have a more elaborate filesystem setup in the 111 // real flow, so simulate this here. 112 meta.Lang = lang 113 meta.Path = meta.Filename 114 meta.TranslationBaseName = paths.Filename(fi.Name()) 115 meta.Classifier = files.ClassifyContentFile(fi.Name(), meta.OpenFunc) 116 }) 117 } 118 119 c.Run("AddFiles", func(c *qt.C) { 120 memfs := afero.NewMemMapFs() 121 122 fsl := func(lang string) afero.Fs { 123 return createFs(memfs, lang) 124 } 125 126 fs := fsl("en") 127 128 header := writeFile(c, fs, "blog/a/index.md", "page") 129 130 c.Assert(header.Meta().Lang, qt.Equals, "en") 131 132 resources := []hugofs.FileMetaInfo{ 133 writeFile(c, fs, "blog/a/b/data.json", "data"), 134 writeFile(c, fs, "blog/a/logo.png", "image"), 135 } 136 137 m := newContentMap(contentMapConfig{lang: "en"}) 138 139 c.Assert(m.AddFilesBundle(header, resources...), qt.IsNil) 140 141 c.Assert(m.AddFilesBundle(writeFile(c, fs, "blog/b/c/index.md", "page")), qt.IsNil) 142 143 c.Assert(m.AddFilesBundle( 144 writeFile(c, fs, "blog/_index.md", "section page"), 145 writeFile(c, fs, "blog/sectiondata.json", "section resource"), 146 ), qt.IsNil) 147 148 got := m.testDump() 149 150 expect := ` 151 Tree 0: 152 /blog/__hb_a__hl_ 153 /blog/__hb_b/c__hl_ 154 Tree 1: 155 /blog/ 156 Tree 2: 157 /blog/__hb_a__hl_b/data.json 158 /blog/__hb_a__hl_logo.png 159 /blog/__hl_sectiondata.json 160 en/pages/blog/__hb_a__hl_|f:blog/a/index.md 161 - R: blog/a/b/data.json 162 - R: blog/a/logo.png 163 en/pages/blog/__hb_b/c__hl_|f:blog/b/c/index.md 164 en/sections/blog/|f:blog/_index.md 165 - P: blog/a/index.md 166 - P: blog/b/c/index.md 167 - R: blog/sectiondata.json 168 169 ` 170 171 c.Assert(got, hqt.IsSameString, expect, qt.Commentf(got)) 172 173 // Add a data file to the section bundle 174 c.Assert(m.AddFiles( 175 writeFile(c, fs, "blog/sectiondata2.json", "section resource"), 176 ), qt.IsNil) 177 178 // And then one to the leaf bundles 179 c.Assert(m.AddFiles( 180 writeFile(c, fs, "blog/a/b/data2.json", "data2"), 181 ), qt.IsNil) 182 183 c.Assert(m.AddFiles( 184 writeFile(c, fs, "blog/b/c/d/data3.json", "data3"), 185 ), qt.IsNil) 186 187 got = m.testDump() 188 189 expect = ` 190 Tree 0: 191 /blog/__hb_a__hl_ 192 /blog/__hb_b/c__hl_ 193 Tree 1: 194 /blog/ 195 Tree 2: 196 /blog/__hb_a__hl_b/data.json 197 /blog/__hb_a__hl_b/data2.json 198 /blog/__hb_a__hl_logo.png 199 /blog/__hb_b/c__hl_d/data3.json 200 /blog/__hl_sectiondata.json 201 /blog/__hl_sectiondata2.json 202 en/pages/blog/__hb_a__hl_|f:blog/a/index.md 203 - R: blog/a/b/data.json 204 - R: blog/a/b/data2.json 205 - R: blog/a/logo.png 206 en/pages/blog/__hb_b/c__hl_|f:blog/b/c/index.md 207 - R: blog/b/c/d/data3.json 208 en/sections/blog/|f:blog/_index.md 209 - P: blog/a/index.md 210 - P: blog/b/c/index.md 211 - R: blog/sectiondata.json 212 - R: blog/sectiondata2.json 213 214 ` 215 216 c.Assert(got, hqt.IsSameString, expect, qt.Commentf(got)) 217 218 // Add a regular page (i.e. not a bundle) 219 c.Assert(m.AddFilesBundle(writeFile(c, fs, "blog/b.md", "page")), qt.IsNil) 220 221 c.Assert(m.testDump(), hqt.IsSameString, ` 222 Tree 0: 223 /blog/__hb_a__hl_ 224 /blog/__hb_b/c__hl_ 225 /blog/__hb_b__hl_ 226 Tree 1: 227 /blog/ 228 Tree 2: 229 /blog/__hb_a__hl_b/data.json 230 /blog/__hb_a__hl_b/data2.json 231 /blog/__hb_a__hl_logo.png 232 /blog/__hb_b/c__hl_d/data3.json 233 /blog/__hl_sectiondata.json 234 /blog/__hl_sectiondata2.json 235 en/pages/blog/__hb_a__hl_|f:blog/a/index.md 236 - R: blog/a/b/data.json 237 - R: blog/a/b/data2.json 238 - R: blog/a/logo.png 239 en/pages/blog/__hb_b/c__hl_|f:blog/b/c/index.md 240 - R: blog/b/c/d/data3.json 241 en/pages/blog/__hb_b__hl_|f:blog/b.md 242 en/sections/blog/|f:blog/_index.md 243 - P: blog/a/index.md 244 - P: blog/b/c/index.md 245 - P: blog/b.md 246 - R: blog/sectiondata.json 247 - R: blog/sectiondata2.json 248 249 250 `, qt.Commentf(m.testDump())) 251 }) 252 253 c.Run("CreateMissingNodes", func(c *qt.C) { 254 memfs := afero.NewMemMapFs() 255 256 fsl := func(lang string) afero.Fs { 257 return createFs(memfs, lang) 258 } 259 260 fs := fsl("en") 261 262 m := newContentMap(contentMapConfig{lang: "en"}) 263 264 c.Assert(m.AddFilesBundle(writeFile(c, fs, "blog/page.md", "page")), qt.IsNil) 265 c.Assert(m.AddFilesBundle(writeFile(c, fs, "blog/a/index.md", "page")), qt.IsNil) 266 c.Assert(m.AddFilesBundle(writeFile(c, fs, "bundle/index.md", "page")), qt.IsNil) 267 268 c.Assert(m.CreateMissingNodes(), qt.IsNil) 269 270 got := m.testDump() 271 272 c.Assert(got, hqt.IsSameString, ` 273 274 Tree 0: 275 /__hb_bundle__hl_ 276 /blog/__hb_a__hl_ 277 /blog/__hb_page__hl_ 278 Tree 1: 279 / 280 /blog/ 281 Tree 2: 282 en/pages/__hb_bundle__hl_|f:bundle/index.md 283 en/pages/blog/__hb_a__hl_|f:blog/a/index.md 284 en/pages/blog/__hb_page__hl_|f:blog/page.md 285 en/sections/ 286 - P: bundle/index.md 287 en/sections/blog/ 288 - P: blog/a/index.md 289 - P: blog/page.md 290 291 `, qt.Commentf(got)) 292 }) 293 294 c.Run("cleanKey", func(c *qt.C) { 295 for _, test := range []struct { 296 in string 297 expected string 298 }{ 299 {"/a/b/", "/a/b"}, 300 {filepath.FromSlash("/a/b/"), "/a/b"}, 301 {"/a//b/", "/a/b"}, 302 } { 303 c.Assert(cleanTreeKey(test.in), qt.Equals, test.expected) 304 } 305 }) 306 } 307 308 func TestContentMapSite(t *testing.T) { 309 b := newTestSitesBuilder(t) 310 311 pageTempl := ` 312 --- 313 title: "Page %d" 314 date: "2019-06-0%d" 315 lastMod: "2019-06-0%d" 316 categories: ["funny"] 317 --- 318 319 Page content. 320 ` 321 createPage := func(i int) string { 322 return fmt.Sprintf(pageTempl, i, i, i+1) 323 } 324 325 draftTemplate := `--- 326 title: "Draft" 327 draft: true 328 --- 329 330 ` 331 332 b.WithContent("_index.md", ` 333 --- 334 title: "Hugo Home" 335 cascade: 336 description: "Common Description" 337 338 --- 339 340 Home Content. 341 `) 342 343 b.WithContent("blog/page1.md", createPage(1)) 344 b.WithContent("blog/page2.md", createPage(2)) 345 b.WithContent("blog/page3.md", createPage(3)) 346 b.WithContent("blog/bundle/index.md", createPage(12)) 347 b.WithContent("blog/bundle/data.json", "data") 348 b.WithContent("blog/bundle/page.md", createPage(99)) 349 b.WithContent("blog/subsection/_index.md", createPage(3)) 350 b.WithContent("blog/subsection/subdata.json", "data") 351 b.WithContent("blog/subsection/page4.md", createPage(8)) 352 b.WithContent("blog/subsection/page5.md", createPage(10)) 353 b.WithContent("blog/subsection/draft/index.md", draftTemplate) 354 b.WithContent("blog/subsection/draft/data.json", "data") 355 b.WithContent("blog/draftsection/_index.md", draftTemplate) 356 b.WithContent("blog/draftsection/page/index.md", createPage(12)) 357 b.WithContent("blog/draftsection/page/folder/data.json", "data") 358 b.WithContent("blog/draftsection/sub/_index.md", createPage(12)) 359 b.WithContent("blog/draftsection/sub/page.md", createPage(13)) 360 b.WithContent("docs/page6.md", createPage(11)) 361 b.WithContent("tags/_index.md", createPage(32)) 362 b.WithContent("overlap/_index.md", createPage(33)) 363 b.WithContent("overlap2/_index.md", createPage(34)) 364 365 b.WithTemplatesAdded("layouts/index.html", ` 366 Num Regular: {{ len .Site.RegularPages }} 367 Main Sections: {{ .Site.Params.mainSections }} 368 Pag Num Pages: {{ len .Paginator.Pages }} 369 {{ $home := .Site.Home }} 370 {{ $blog := .Site.GetPage "blog" }} 371 {{ $categories := .Site.GetPage "categories" }} 372 {{ $funny := .Site.GetPage "categories/funny" }} 373 {{ $blogSub := .Site.GetPage "blog/subsection" }} 374 {{ $page := .Site.GetPage "blog/page1" }} 375 {{ $page2 := .Site.GetPage "blog/page2" }} 376 {{ $page4 := .Site.GetPage "blog/subsection/page4" }} 377 {{ $bundle := .Site.GetPage "blog/bundle" }} 378 {{ $overlap1 := .Site.GetPage "overlap" }} 379 {{ $overlap2 := .Site.GetPage "overlap2" }} 380 381 Home: {{ template "print-page" $home }} 382 Blog Section: {{ template "print-page" $blog }} 383 Blog Sub Section: {{ template "print-page" $blogSub }} 384 Page: {{ template "print-page" $page }} 385 Bundle: {{ template "print-page" $bundle }} 386 IsDescendant: true: {{ $page.IsDescendant $blog }} true: {{ $blogSub.IsDescendant $blog }} true: {{ $bundle.IsDescendant $blog }} true: {{ $page4.IsDescendant $blog }} true: {{ $blog.IsDescendant $home }} false: {{ $blog.IsDescendant $blog }} false: {{ $home.IsDescendant $blog }} 387 IsAncestor: true: {{ $blog.IsAncestor $page }} true: {{ $home.IsAncestor $blog }} true: {{ $blog.IsAncestor $blogSub }} true: {{ $blog.IsAncestor $bundle }} true: {{ $blog.IsAncestor $page4 }} true: {{ $home.IsAncestor $page }} false: {{ $blog.IsAncestor $blog }} false: {{ $page.IsAncestor $blog }} false: {{ $blog.IsAncestor $home }} false: {{ $blogSub.IsAncestor $blog }} 388 IsDescendant overlap1: false: {{ $overlap1.IsDescendant $overlap2 }} 389 IsDescendant overlap2: false: {{ $overlap2.IsDescendant $overlap1 }} 390 IsAncestor overlap1: false: {{ $overlap1.IsAncestor $overlap2 }} 391 IsAncestor overlap2: false: {{ $overlap2.IsAncestor $overlap1 }} 392 FirstSection: {{ $blogSub.FirstSection.RelPermalink }} {{ $blog.FirstSection.RelPermalink }} {{ $home.FirstSection.RelPermalink }} {{ $page.FirstSection.RelPermalink }} 393 InSection: true: {{ $page.InSection $blog }} false: {{ $page.InSection $blogSub }} 394 Next: {{ $page2.Next.RelPermalink }} 395 NextInSection: {{ $page2.NextInSection.RelPermalink }} 396 Pages: {{ range $blog.Pages }}{{ .RelPermalink }}|{{ end }} 397 Sections: {{ range $home.Sections }}{{ .RelPermalink }}|{{ end }} 398 Categories: {{ range .Site.Taxonomies.categories }}{{ .Page.RelPermalink }}; {{ .Page.Title }}; {{ .Count }}|{{ end }} 399 Category Terms: {{ $categories.Kind}}: {{ range $categories.Data.Terms.Alphabetical }}{{ .Page.RelPermalink }}; {{ .Page.Title }}; {{ .Count }}|{{ end }} 400 Category Funny: {{ $funny.Kind}}; {{ $funny.Data.Term }}: {{ range $funny.Pages }}{{ .RelPermalink }};|{{ end }} 401 Pag Num Pages: {{ len .Paginator.Pages }} 402 Pag Blog Num Pages: {{ len $blog.Paginator.Pages }} 403 Blog Num RegularPages: {{ len $blog.RegularPages }} 404 Blog Num Pages: {{ len $blog.Pages }} 405 406 Draft1: {{ if (.Site.GetPage "blog/subsection/draft") }}FOUND{{ end }}| 407 Draft2: {{ if (.Site.GetPage "blog/draftsection") }}FOUND{{ end }}| 408 Draft3: {{ if (.Site.GetPage "blog/draftsection/page") }}FOUND{{ end }}| 409 Draft4: {{ if (.Site.GetPage "blog/draftsection/sub") }}FOUND{{ end }}| 410 Draft5: {{ if (.Site.GetPage "blog/draftsection/sub/page") }}FOUND{{ end }}| 411 412 {{ define "print-page" }}{{ .Title }}|{{ .RelPermalink }}|{{ .Date.Format "2006-01-02" }}|Current Section: {{ .CurrentSection.SectionsPath }}|Resources: {{ range .Resources }}{{ .ResourceType }}: {{ .RelPermalink }}|{{ end }}{{ end }} 413 `) 414 415 b.Build(BuildCfg{}) 416 417 b.AssertFileContent("public/index.html", 418 419 ` 420 Num Regular: 7 421 Main Sections: [blog] 422 Pag Num Pages: 7 423 424 Home: Hugo Home|/|2019-06-08|Current Section: |Resources: 425 Blog Section: Blogs|/blog/|2019-06-08|Current Section: blog|Resources: 426 Blog Sub Section: Page 3|/blog/subsection/|2019-06-03|Current Section: blog/subsection|Resources: application: /blog/subsection/subdata.json| 427 Page: Page 1|/blog/page1/|2019-06-01|Current Section: blog|Resources: 428 Bundle: Page 12|/blog/bundle/|0001-01-01|Current Section: blog|Resources: application: /blog/bundle/data.json|page: | 429 IsDescendant: true: true true: true true: true true: true true: true false: false false: false 430 IsAncestor: true: true true: true true: true true: true true: true true: true false: false false: false false: false false: false 431 IsDescendant overlap1: false: false 432 IsDescendant overlap2: false: false 433 IsAncestor overlap1: false: false 434 IsAncestor overlap2: false: false 435 FirstSection: /blog/ /blog/ / /blog/ 436 InSection: true: true false: false 437 Next: /blog/page3/ 438 NextInSection: /blog/page3/ 439 Pages: /blog/page3/|/blog/subsection/|/blog/page2/|/blog/page1/|/blog/bundle/| 440 Sections: /blog/|/docs/| 441 Categories: /categories/funny/; funny; 11| 442 Category Terms: taxonomy: /categories/funny/; funny; 11| 443 Category Funny: term; funny: /blog/subsection/page4/;|/blog/page3/;|/blog/subsection/;|/blog/page2/;|/blog/page1/;|/blog/subsection/page5/;|/docs/page6/;|/blog/bundle/;|;| 444 Pag Num Pages: 7 445 Pag Blog Num Pages: 4 446 Blog Num RegularPages: 4 447 Blog Num Pages: 5 448 449 Draft1: | 450 Draft2: | 451 Draft3: | 452 Draft4: | 453 Draft5: | 454 455 `) 456 }