package hugolib import ( "fmt" "path/filepath" "strings" "testing" 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.FileError { t.c.Assert(err, qt.Not(qt.IsNil), qt.Commentf(t.name)) fe := herrors.UnwrapFileError(err) t.c.Assert(fe, qt.Not(qt.IsNil)) return fe } func (t testSiteBuildErrorAsserter) assertLineNumber(lineNumber int, err error) { t.c.Helper() 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" ) type testCase struct { name string fileType string fileFixer func(content string) string assertErr func(a testSiteBuildErrorAsserter, err error) } createTestFiles := func(tc testCase) string { f := func(ftype, content string) string { if ftype != tc.fileType { return content } return tc.fileFixer(content) } return ` -- hugo.toml -- baseURL = "https://example.com" -- layouts/_shortcodes/sc.html -- ` + f(shortcode, `SHORTCODE L1 SHORTCODE L2 SHORTCODE L3: SHORTCODE L4: {{ .Page.Title }} `) + ` -- layouts/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 `) + ` -- layouts/single.html -- ` + f(single, `{{ define "main" }} SINGLE L2: SINGLE L3: SINGLE L4: SINGLE L5: {{ .Title }} {{ .Content }} {{ end }} `) + ` -- layouts/foo/single.html -- ` + f(single, ` SINGLE L2: SINGLE L3: SINGLE L4: SINGLE L5: {{ .Title }} {{ .Content }} `) + ` -- content/myyaml.md -- ` + f(yamlcontent, `--- title: "The YAML" --- Some content. {{< sc >}} Some more text. The end. `) + ` -- content/mytoml.md -- ` + f(tomlcontent, `+++ title = "The TOML" p1 = "v" p2 = "v" p3 = "v" description = "Descriptioon" +++ Some content. `) + ` -- content/myjson.md -- ` + f(jsoncontent, `{ "title": "This is a title", "description": "This is a description." } Some content. `) } tests := []testCase{ { name: "Base template parse failed", fileType: base, fileFixer: func(content string) string { return strings.Replace(content, ".Title }}", ".Title }", 1) }, assertErr: 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) }, assertErr: 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) }, assertErr: 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.assertErrorMessage("\"/layouts/foo/single.html:5:1\": parse of template 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) }, assertErr: 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.assertErrorMessage("\"layouts/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) }, assertErr: 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.assertErrorMessage("\"layouts/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) }, assertErr: func(a testSiteBuildErrorAsserter, err error) { a.assertLineNumber(4, err) }, }, { name: "Shortcode execute failed", fileType: shortcode, fileFixer: func(content string) string { return strings.Replace(content, ".Title", ".Titles", 1) }, assertErr: func(a testSiteBuildErrorAsserter, err error) { fe := a.getFileError(err) // Make sure that it contains both the content file and template a.assertErrorMessage(`"content/myyaml.md:7:10": failed to render shortcode "sc": failed to process shortcode: "layouts/_shortcodes/sc.html:4:22": execute of template failed: template: shortcodes/sc.html:4:22: executing "shortcodes/sc.html" at <.Page.Titles>: can't evaluate field Titles in type page.Page`, fe.Error()) a.c.Assert(fe.Position().LineNumber, qt.Equals, 7) }, }, { name: "Shortode does not exist", fileType: yamlcontent, fileFixer: func(content string) string { return strings.Replace(content, "{{< sc >}}", "{{< nono >}}", 1) }, assertErr: 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.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 `--- title: "My YAML Content" foo bar --- ` }, assertErr: func(a testSiteBuildErrorAsserter, err error) { a.assertLineNumber(3, err) }, }, { name: "Invalid TOML front matter", fileType: tomlcontent, fileFixer: func(content string) string { return strings.Replace(content, "description = ", "description &", 1) }, assertErr: func(a testSiteBuildErrorAsserter, err error) { fe := a.getFileError(err) a.c.Assert(fe.Position().LineNumber, qt.Equals, 6) }, }, { name: "Invalid JSON front matter", fileType: jsoncontent, fileFixer: func(content string) string { return strings.Replace(content, "\"description\":", "\"description\"", 1) }, assertErr: func(a testSiteBuildErrorAsserter, err error) { fe := a.getFileError(err) a.c.Assert(fe.Position().LineNumber, qt.Equals, 3) }, }, { // 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) }, assertErr: 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 if test.name != "Base template parse failed" { continue } t.Run(test.name, func(t *testing.T) { t.Parallel() c := qt.New(t) errorAsserter := testSiteBuildErrorAsserter{ c: c, name: test.name, } files := createTestFiles(test) _, err := TestE(t, files) if test.assertErr != nil { test.assertErr(errorAsserter, err) } else { c.Assert(err, qt.IsNil) } }) } } // Issue 9852 func TestErrorMinify(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- [minify] minifyOutput = true -- layouts/home.html --
` b, err := TestE(t, files) b.Assert(err, qt.IsNotNil) fe := herrors.UnwrapFileError(err) b.Assert(fe, qt.IsNotNil) b.Assert(fe.Position().LineNumber, qt.Equals, 2) b.Assert(fe.Position().ColumnNumber, qt.Equals, 9) b.Assert(fe.Error(), qt.Contains, "unexpected = in expression on line 2 and column 9") b.Assert(filepath.ToSlash(fe.Position().Filename), qt.Contains, "hugo-transform-error") // os.Remove is not needed in txtar tests as the filesystem is ephemeral. // b.Assert(os.Remove(fe.Position().Filename), qt.IsNil) } func TestErrorNestedRender(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- -- content/_index.md -- --- title: "Home" --- -- layouts/home.html -- line 1 line 2 1{{ .Render "myview" }} -- layouts/myview.html -- line 1 12{{ partial "foo.html" . }} line 4 line 5 -- layouts/_partials/foo.html -- line 1 line 2 123{{ .ThisDoesNotExist }} line 4 ` b, err := TestE(t, files) b.Assert(err, qt.IsNotNil) errors := herrors.UnwrapFileErrorsWithErrorContext(err) b.Assert(errors, qt.HasLen, 4) b.Assert(errors[0].Position().LineNumber, qt.Equals, 3) b.Assert(errors[0].Position().ColumnNumber, qt.Equals, 4) b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/layouts/home.html:3:4": execute of template failed`)) b.Assert(errors[0].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "1{{ .Render \"myview\" }}"}) b.Assert(errors[2].Position().LineNumber, qt.Equals, 2) b.Assert(errors[2].Position().ColumnNumber, qt.Equals, 5) b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"}) b.Assert(errors[3].Position().LineNumber, qt.Equals, 3) b.Assert(errors[3].Position().ColumnNumber, qt.Equals, 6) b.Assert(errors[3].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"}) } func TestErrorNestedShortcode(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- -- content/_index.md -- --- title: "Home" --- ## Hello {{< hello >}} -- layouts/home.html -- line 1 line 2 {{ .Content }} line 5 -- layouts/_shortcodes/hello.html -- line 1 12{{ partial "foo.html" . }} line 4 line 5 -- layouts/_partials/foo.html -- line 1 line 2 123{{ .ThisDoesNotExist }} line 4 ` b, err := TestE(t, files) b.Assert(err, qt.IsNotNil) errors := herrors.UnwrapFileErrorsWithErrorContext(err) b.Assert(errors, qt.HasLen, 4) b.Assert(errors[1].Position().LineNumber, qt.Equals, 6) b.Assert(errors[1].Position().ColumnNumber, qt.Equals, 1) b.Assert(errors[1].ErrorContext().ChromaLexer, qt.Equals, "md") b.Assert(errors[1].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:6:1": failed to render shortcode "hello": failed to process shortcode: "/layouts/_shortcodes/hello.html:2:5":`)) b.Assert(errors[1].ErrorContext().Lines, qt.DeepEquals, []string{"", "## Hello", "{{< hello >}}", ""}) b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"}) b.Assert(errors[3].Position().LineNumber, qt.Equals, 3) b.Assert(errors[3].Position().ColumnNumber, qt.Equals, 6) b.Assert(errors[3].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"}) } func TestErrorRenderHookHeading(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- -- content/_index.md -- --- title: "Home" --- ## Hello -- layouts/home.html -- line 1 line 2 {{ .Content }} line 5 -- layouts/_markup/render-heading.html -- line 1 12{{ .Levels }} line 4 line 5 ` b, err := TestE(t, files) b.Assert(err, qt.IsNotNil) errors := herrors.UnwrapFileErrorsWithErrorContext(err) b.Assert(errors, qt.HasLen, 3) b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:2:5": "/layouts/_markup/render-heading.html:2:5": execute of template failed`)) } func TestErrorRenderHookCodeblock(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- -- content/_index.md -- --- title: "Home" --- ## Hello §§§ foo bar §§§ -- layouts/home.html -- line 1 line 2 {{ .Content }} line 5 -- layouts/_markup/render-codeblock-foo.html -- line 1 12{{ .Foo }} line 4 line 5 ` b, err := TestE(t, files) b.Assert(err, qt.IsNotNil) errors := herrors.UnwrapFileErrorsWithErrorContext(err) b.Assert(errors, qt.HasLen, 3) first := errors[0] b.Assert(first.Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:7:1": "/layouts/_markup/render-codeblock-foo.html:2:5": execute of template failed`)) } func TestErrorInBaseTemplate(t *testing.T) { t.Parallel() filesTemplate := ` -- hugo.toml -- -- content/_index.md -- --- title: "Home" --- -- layouts/baseof.html -- line 1 base line 2 base {{ block "main" . }}empty{{ end }} line 4 base {{ block "toc" . }}empty{{ end }} -- layouts/home.html -- {{ define "main" }} line 2 index line 3 index line 4 index {{ end }} {{ define "toc" }} TOC: {{ partial "toc.html" . }} {{ end }} -- layouts/_partials/toc.html -- toc line 1 toc line 2 toc line 3 toc line 4 ` t.Run("base template", func(t *testing.T) { files := strings.Replace(filesTemplate, "line 4 base", "123{{ .ThisDoesNotExist \"abc\" }}", 1) b, err := TestE(t, files) b.Assert(err, qt.IsNotNil) b.Assert(err.Error(), qt.Contains, `baseof.html:4:6`) }) t.Run("home template", func(t *testing.T) { files := strings.Replace(filesTemplate, "line 3 index", "1234{{ .ThisDoesNotExist \"abc\" }}", 1) b, err := TestE(t, files) b.Assert(err, qt.IsNotNil) b.Assert(err.Error(), qt.Contains, `home.html:3:7"`) }) t.Run("partial from define", func(t *testing.T) { files := strings.Replace(filesTemplate, "toc line 2", "12345{{ .ThisDoesNotExist \"abc\" }}", 1) b, err := TestE(t, files) b.Assert(err, qt.IsNotNil) b.Assert(err.Error(), qt.Contains, `toc.html:2:8"`) }) } // https://github.com/gohugoio/hugo/issues/5375 func TestSiteBuildTimeout(t *testing.T) { t.Parallel() var filesBuilder strings.Builder filesBuilder.WriteString(` -- hugo.toml -- timeout = 5 -- layouts/single.html -- {{ .WordCount }} -- layouts/_shortcodes/c.html -- {{ range .Page.Site.RegularPages }} {{ .WordCount }} {{ end }} `) for i := 1; i < 100; i++ { filesBuilder.WriteString(fmt.Sprintf(` -- content/page%d.md -- --- title: "A page" --- {{< c >}} `, i)) } _, err := TestE(t, filesBuilder.String()) qt.Assert(t, err, qt.Not(qt.IsNil)) qt.Assert(t, err.Error(), qt.Contains, "timed out rendering the page content") } func TestErrorTemplateRuntime(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- -- layouts/home.html -- Home. {{ .ThisDoesNotExist }} ` b, err := TestE(t, files) b.Assert(err, qt.Not(qt.IsNil)) b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/layouts/home.html:2:3`)) b.Assert(err.Error(), qt.Contains, `can't evaluate field ThisDoesNotExist`) } func TestErrorFrontmatterYAMLSyntax(t *testing.T) { t.Parallel() files := ` -- hugo.toml -- -- content/_index.md -- --- line1: 'value1' x line2: 'value2' line3: 'value3' --- ` b, err := TestE(t, files) b.Assert(err, qt.Not(qt.IsNil)) b.Assert(err.Error(), qt.Contains, "[2:1] non-map value is specified") fe := herrors.UnwrapFileError(err) b.Assert(fe, qt.Not(qt.IsNil)) pos := fe.Position() b.Assert(pos.Filename, qt.Contains, filepath.FromSlash("content/_index.md")) b.Assert(fe.ErrorContext(), qt.Not(qt.IsNil)) b.Assert(pos.LineNumber, qt.Equals, 8) b.Assert(pos.ColumnNumber, qt.Equals, 1) }