mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
c6d650c8c8
This more or less completes the simplification of the template handling code in Hugo started in v0.62. The main motivation was to fix a long lasting issue about a crash in HTML content files without front matter. But this commit also comes with a big functional improvement. As we now have moved the base template evaluation to the build stage we now use the same lookup rules for `baseof` as for `list` etc. type of templates. This means that in this simple example you can have a `baseof` template for the `blog` section without having to duplicate the others: ``` layouts ├── _default │ ├── baseof.html │ ├── list.html │ └── single.html └── blog └── baseof.html ``` Also, when simplifying code, you often get rid of some double work, as shown in the "site building" benchmarks below. These benchmarks looks suspiciously good, but I have repeated the below with ca. the same result. Compared to master: ``` name old time/op new time/op delta SiteNew/Bundle_with_image-16 13.1ms ± 1% 10.5ms ± 1% -19.34% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 13.0ms ± 0% 10.7ms ± 1% -18.05% (p=0.029 n=4+4) SiteNew/Tags_and_categories-16 46.4ms ± 2% 43.1ms ± 1% -7.15% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 52.2ms ± 2% 47.8ms ± 1% -8.30% (p=0.029 n=4+4) SiteNew/Deep_content_tree-16 77.9ms ± 1% 70.9ms ± 1% -9.01% (p=0.029 n=4+4) SiteNew/Many_HTML_templates-16 43.0ms ± 0% 37.2ms ± 1% -13.54% (p=0.029 n=4+4) SiteNew/Page_collections-16 58.2ms ± 1% 52.4ms ± 1% -9.95% (p=0.029 n=4+4) name old alloc/op new alloc/op delta SiteNew/Bundle_with_image-16 3.81MB ± 0% 2.22MB ± 0% -41.70% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 3.60MB ± 0% 2.01MB ± 0% -44.20% (p=0.029 n=4+4) SiteNew/Tags_and_categories-16 19.3MB ± 1% 14.1MB ± 0% -26.91% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 70.7MB ± 0% 69.0MB ± 0% -2.40% (p=0.029 n=4+4) SiteNew/Deep_content_tree-16 37.1MB ± 0% 31.2MB ± 0% -15.94% (p=0.029 n=4+4) SiteNew/Many_HTML_templates-16 17.6MB ± 0% 10.6MB ± 0% -39.92% (p=0.029 n=4+4) SiteNew/Page_collections-16 25.9MB ± 0% 21.2MB ± 0% -17.99% (p=0.029 n=4+4) name old allocs/op new allocs/op delta SiteNew/Bundle_with_image-16 52.3k ± 0% 26.1k ± 0% -50.18% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 52.3k ± 0% 26.1k ± 0% -50.16% (p=0.029 n=4+4) SiteNew/Tags_and_categories-16 336k ± 1% 269k ± 0% -19.90% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 422k ± 0% 395k ± 0% -6.43% (p=0.029 n=4+4) SiteNew/Deep_content_tree-16 401k ± 0% 313k ± 0% -21.79% (p=0.029 n=4+4) SiteNew/Many_HTML_templates-16 247k ± 0% 143k ± 0% -42.17% (p=0.029 n=4+4) SiteNew/Page_collections-16 282k ± 0% 207k ± 0% -26.55% (p=0.029 n=4+4) ``` Fixes #6716 Fixes #6760 Fixes #6768 Fixes #6778
356 lines
9.4 KiB
Go
356 lines
9.4 KiB
Go
package hugolib
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fortytw2/leaktest"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
)
|
|
|
|
type testSiteBuildErrorAsserter struct {
|
|
name string
|
|
c *qt.C
|
|
}
|
|
|
|
func (t testSiteBuildErrorAsserter) getFileError(err error) *herrors.ErrorWithFileContext {
|
|
t.c.Assert(err, qt.Not(qt.IsNil), qt.Commentf(t.name))
|
|
ferr := herrors.UnwrapErrorWithFileContext(err)
|
|
t.c.Assert(ferr, qt.Not(qt.IsNil))
|
|
return ferr
|
|
}
|
|
|
|
func (t testSiteBuildErrorAsserter) assertLineNumber(lineNumber int, err error) {
|
|
fe := t.getFileError(err)
|
|
t.c.Assert(fe.Position().LineNumber, qt.Equals, lineNumber, qt.Commentf(err.Error()))
|
|
}
|
|
|
|
func (t testSiteBuildErrorAsserter) assertErrorMessage(e1, e2 string) {
|
|
// The error message will contain filenames with OS slashes. Normalize before compare.
|
|
e1, e2 = filepath.ToSlash(e1), filepath.ToSlash(e2)
|
|
t.c.Assert(e2, qt.Contains, e1)
|
|
|
|
}
|
|
|
|
func TestSiteBuildErrors(t *testing.T) {
|
|
|
|
const (
|
|
yamlcontent = "yamlcontent"
|
|
tomlcontent = "tomlcontent"
|
|
jsoncontent = "jsoncontent"
|
|
shortcode = "shortcode"
|
|
base = "base"
|
|
single = "single"
|
|
)
|
|
|
|
// TODO(bep) add content tests after https://github.com/gohugoio/hugo/issues/5324
|
|
// is implemented.
|
|
|
|
tests := []struct {
|
|
name string
|
|
fileType string
|
|
fileFixer func(content string) string
|
|
assertCreateError func(a testSiteBuildErrorAsserter, err error)
|
|
assertBuildError func(a testSiteBuildErrorAsserter, err error)
|
|
}{
|
|
|
|
{
|
|
name: "Base template parse failed",
|
|
fileType: base,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, ".Title }}", ".Title }", 1)
|
|
},
|
|
// Base templates gets parsed at build time.
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
a.assertLineNumber(4, err)
|
|
},
|
|
},
|
|
{
|
|
name: "Base template execute failed",
|
|
fileType: base,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, ".Title", ".Titles", 1)
|
|
},
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
a.assertLineNumber(4, err)
|
|
},
|
|
},
|
|
{
|
|
name: "Single template parse failed",
|
|
fileType: single,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, ".Title }}", ".Title }", 1)
|
|
},
|
|
assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
|
|
fe := a.getFileError(err)
|
|
a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
|
|
a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 1)
|
|
a.c.Assert(fe.ChromaLexer, qt.Equals, "go-html-template")
|
|
a.assertErrorMessage("\"layouts/foo/single.html:5:1\": parse failed: template: foo/single.html:5: unexpected \"}\" in operand", fe.Error())
|
|
|
|
},
|
|
},
|
|
{
|
|
name: "Single template execute failed",
|
|
fileType: single,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, ".Title", ".Titles", 1)
|
|
},
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
fe := a.getFileError(err)
|
|
a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
|
|
a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 14)
|
|
a.c.Assert(fe.ChromaLexer, qt.Equals, "go-html-template")
|
|
a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error())
|
|
|
|
},
|
|
},
|
|
{
|
|
name: "Single template execute failed, long keyword",
|
|
fileType: single,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, ".Title", ".ThisIsAVeryLongTitle", 1)
|
|
},
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
fe := a.getFileError(err)
|
|
a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
|
|
a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 14)
|
|
a.c.Assert(fe.ChromaLexer, qt.Equals, "go-html-template")
|
|
a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error())
|
|
|
|
},
|
|
},
|
|
{
|
|
name: "Shortcode parse failed",
|
|
fileType: shortcode,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, ".Title }}", ".Title }", 1)
|
|
},
|
|
assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
|
|
a.assertLineNumber(4, err)
|
|
},
|
|
},
|
|
{
|
|
name: "Shortode execute failed",
|
|
fileType: shortcode,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, ".Title", ".Titles", 1)
|
|
},
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
fe := a.getFileError(err)
|
|
a.c.Assert(fe.Position().LineNumber, qt.Equals, 7)
|
|
a.c.Assert(fe.ChromaLexer, qt.Equals, "md")
|
|
// Make sure that it contains both the content file and template
|
|
a.assertErrorMessage(`content/myyaml.md:7:10": failed to render shortcode "sc"`, fe.Error())
|
|
a.assertErrorMessage(`shortcodes/sc.html:4:22: executing "shortcodes/sc.html" at <.Page.Titles>: can't evaluate`, fe.Error())
|
|
},
|
|
},
|
|
{
|
|
name: "Shortode does not exist",
|
|
fileType: yamlcontent,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, "{{< sc >}}", "{{< nono >}}", 1)
|
|
},
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
fe := a.getFileError(err)
|
|
a.c.Assert(fe.Position().LineNumber, qt.Equals, 7)
|
|
a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 10)
|
|
a.c.Assert(fe.ChromaLexer, qt.Equals, "md")
|
|
a.assertErrorMessage(`"content/myyaml.md:7:10": failed to extract shortcode: template for shortcode "nono" not found`, fe.Error())
|
|
},
|
|
},
|
|
{
|
|
name: "Invalid YAML front matter",
|
|
fileType: yamlcontent,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, "title:", "title: %foo", 1)
|
|
},
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
a.assertLineNumber(2, err)
|
|
},
|
|
},
|
|
{
|
|
name: "Invalid TOML front matter",
|
|
fileType: tomlcontent,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, "description = ", "description &", 1)
|
|
},
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
fe := a.getFileError(err)
|
|
a.c.Assert(fe.Position().LineNumber, qt.Equals, 6)
|
|
a.c.Assert(fe.ErrorContext.ChromaLexer, qt.Equals, "toml")
|
|
|
|
},
|
|
},
|
|
{
|
|
name: "Invalid JSON front matter",
|
|
fileType: jsoncontent,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, "\"description\":", "\"description\"", 1)
|
|
},
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
fe := a.getFileError(err)
|
|
|
|
a.c.Assert(fe.Position().LineNumber, qt.Equals, 3)
|
|
a.c.Assert(fe.ErrorContext.ChromaLexer, qt.Equals, "json")
|
|
|
|
},
|
|
},
|
|
{
|
|
// See https://github.com/gohugoio/hugo/issues/5327
|
|
name: "Panic in template Execute",
|
|
fileType: single,
|
|
fileFixer: func(content string) string {
|
|
return strings.Replace(content, ".Title", ".Parent.Parent.Parent", 1)
|
|
},
|
|
|
|
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
|
|
a.c.Assert(err, qt.Not(qt.IsNil))
|
|
fe := a.getFileError(err)
|
|
a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
|
|
a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 21)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
c := qt.New(t)
|
|
errorAsserter := testSiteBuildErrorAsserter{
|
|
c: c,
|
|
name: test.name,
|
|
}
|
|
|
|
b := newTestSitesBuilder(t).WithSimpleConfigFile()
|
|
|
|
f := func(fileType, content string) string {
|
|
if fileType != test.fileType {
|
|
return content
|
|
}
|
|
return test.fileFixer(content)
|
|
|
|
}
|
|
|
|
b.WithTemplatesAdded("layouts/shortcodes/sc.html", f(shortcode, `SHORTCODE L1
|
|
SHORTCODE L2
|
|
SHORTCODE L3:
|
|
SHORTCODE L4: {{ .Page.Title }}
|
|
`))
|
|
b.WithTemplatesAdded("layouts/_default/baseof.html", f(base, `BASEOF L1
|
|
BASEOF L2
|
|
BASEOF L3
|
|
BASEOF L4{{ if .Title }}{{ end }}
|
|
{{block "main" .}}This is the main content.{{end}}
|
|
BASEOF L6
|
|
`))
|
|
|
|
b.WithTemplatesAdded("layouts/_default/single.html", f(single, `{{ define "main" }}
|
|
SINGLE L2:
|
|
SINGLE L3:
|
|
SINGLE L4:
|
|
SINGLE L5: {{ .Title }} {{ .Content }}
|
|
{{ end }}
|
|
`))
|
|
|
|
b.WithTemplatesAdded("layouts/foo/single.html", f(single, `
|
|
SINGLE L2:
|
|
SINGLE L3:
|
|
SINGLE L4:
|
|
SINGLE L5: {{ .Title }} {{ .Content }}
|
|
`))
|
|
|
|
b.WithContent("myyaml.md", f(yamlcontent, `---
|
|
title: "The YAML"
|
|
---
|
|
|
|
Some content.
|
|
|
|
{{< sc >}}
|
|
|
|
Some more text.
|
|
|
|
The end.
|
|
|
|
`))
|
|
|
|
b.WithContent("mytoml.md", f(tomlcontent, `+++
|
|
title = "The TOML"
|
|
p1 = "v"
|
|
p2 = "v"
|
|
p3 = "v"
|
|
description = "Descriptioon"
|
|
+++
|
|
|
|
Some content.
|
|
|
|
|
|
`))
|
|
|
|
b.WithContent("myjson.md", f(jsoncontent, `{
|
|
"title": "This is a title",
|
|
"description": "This is a description."
|
|
}
|
|
|
|
Some content.
|
|
|
|
|
|
`))
|
|
|
|
createErr := b.CreateSitesE()
|
|
if test.assertCreateError != nil {
|
|
test.assertCreateError(errorAsserter, createErr)
|
|
} else {
|
|
c.Assert(createErr, qt.IsNil)
|
|
}
|
|
|
|
if createErr == nil {
|
|
buildErr := b.BuildE(BuildCfg{})
|
|
if test.assertBuildError != nil {
|
|
test.assertBuildError(errorAsserter, buildErr)
|
|
} else {
|
|
c.Assert(buildErr, qt.IsNil)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// https://github.com/gohugoio/hugo/issues/5375
|
|
func TestSiteBuildTimeout(t *testing.T) {
|
|
if !isCI() {
|
|
defer leaktest.CheckTimeout(t, 10*time.Second)()
|
|
}
|
|
|
|
b := newTestSitesBuilder(t)
|
|
b.WithConfigFile("toml", `
|
|
timeout = 5
|
|
`)
|
|
|
|
b.WithTemplatesAdded("_default/single.html", `
|
|
{{ .WordCount }}
|
|
`, "shortcodes/c.html", `
|
|
{{ range .Page.Site.RegularPages }}
|
|
{{ .WordCount }}
|
|
{{ end }}
|
|
|
|
`)
|
|
|
|
for i := 1; i < 100; i++ {
|
|
b.WithContent(fmt.Sprintf("page%d.md", i), `---
|
|
title: "A page"
|
|
---
|
|
|
|
{{< c >}}`)
|
|
|
|
}
|
|
|
|
b.CreateSites().BuildFail(BuildCfg{})
|
|
|
|
}
|