hugo_sites_build_errors_test.go (16339B)
1 package hugolib
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/gohugoio/hugo/htesting"
11
12 qt "github.com/frankban/quicktest"
13 "github.com/gohugoio/hugo/common/herrors"
14 )
15
16 type testSiteBuildErrorAsserter struct {
17 name string
18 c *qt.C
19 }
20
21 func (t testSiteBuildErrorAsserter) getFileError(err error) herrors.FileError {
22 t.c.Assert(err, qt.Not(qt.IsNil), qt.Commentf(t.name))
23 fe := herrors.UnwrapFileError(err)
24 t.c.Assert(fe, qt.Not(qt.IsNil))
25 return fe
26 }
27
28 func (t testSiteBuildErrorAsserter) assertLineNumber(lineNumber int, err error) {
29 t.c.Helper()
30 fe := t.getFileError(err)
31 t.c.Assert(fe.Position().LineNumber, qt.Equals, lineNumber, qt.Commentf(err.Error()))
32 }
33
34 func (t testSiteBuildErrorAsserter) assertErrorMessage(e1, e2 string) {
35 // The error message will contain filenames with OS slashes. Normalize before compare.
36 e1, e2 = filepath.ToSlash(e1), filepath.ToSlash(e2)
37 t.c.Assert(e2, qt.Contains, e1)
38 }
39
40 func TestSiteBuildErrors(t *testing.T) {
41 const (
42 yamlcontent = "yamlcontent"
43 tomlcontent = "tomlcontent"
44 jsoncontent = "jsoncontent"
45 shortcode = "shortcode"
46 base = "base"
47 single = "single"
48 )
49
50 // TODO(bep) add content tests after https://github.com/gohugoio/hugo/issues/5324
51 // is implemented.
52
53 tests := []struct {
54 name string
55 fileType string
56 fileFixer func(content string) string
57 assertCreateError func(a testSiteBuildErrorAsserter, err error)
58 assertBuildError func(a testSiteBuildErrorAsserter, err error)
59 }{
60
61 {
62 name: "Base template parse failed",
63 fileType: base,
64 fileFixer: func(content string) string {
65 return strings.Replace(content, ".Title }}", ".Title }", 1)
66 },
67 // Base templates gets parsed at build time.
68 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
69 a.assertLineNumber(4, err)
70 },
71 },
72 {
73 name: "Base template execute failed",
74 fileType: base,
75 fileFixer: func(content string) string {
76 return strings.Replace(content, ".Title", ".Titles", 1)
77 },
78 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
79 a.assertLineNumber(4, err)
80 },
81 },
82 {
83 name: "Single template parse failed",
84 fileType: single,
85 fileFixer: func(content string) string {
86 return strings.Replace(content, ".Title }}", ".Title }", 1)
87 },
88 assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
89 fe := a.getFileError(err)
90 a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
91 a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 1)
92 a.assertErrorMessage("\"layouts/foo/single.html:5:1\": parse failed: template: foo/single.html:5: unexpected \"}\" in operand", fe.Error())
93 },
94 },
95 {
96 name: "Single template execute failed",
97 fileType: single,
98 fileFixer: func(content string) string {
99 return strings.Replace(content, ".Title", ".Titles", 1)
100 },
101 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
102 fe := a.getFileError(err)
103 a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
104 a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 14)
105 a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error())
106 },
107 },
108 {
109 name: "Single template execute failed, long keyword",
110 fileType: single,
111 fileFixer: func(content string) string {
112 return strings.Replace(content, ".Title", ".ThisIsAVeryLongTitle", 1)
113 },
114 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
115 fe := a.getFileError(err)
116 a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
117 a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 14)
118 a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error())
119 },
120 },
121 {
122 name: "Shortcode parse failed",
123 fileType: shortcode,
124 fileFixer: func(content string) string {
125 return strings.Replace(content, ".Title }}", ".Title }", 1)
126 },
127 assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
128 a.assertLineNumber(4, err)
129 },
130 },
131 {
132 name: "Shortcode execute failed",
133 fileType: shortcode,
134 fileFixer: func(content string) string {
135 return strings.Replace(content, ".Title", ".Titles", 1)
136 },
137 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
138 fe := a.getFileError(err)
139 // Make sure that it contains both the content file and template
140 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())
141 a.c.Assert(fe.Position().LineNumber, qt.Equals, 7)
142
143 },
144 },
145 {
146 name: "Shortode does not exist",
147 fileType: yamlcontent,
148 fileFixer: func(content string) string {
149 return strings.Replace(content, "{{< sc >}}", "{{< nono >}}", 1)
150 },
151 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
152 fe := a.getFileError(err)
153 a.c.Assert(fe.Position().LineNumber, qt.Equals, 7)
154 a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 10)
155 a.assertErrorMessage(`"content/myyaml.md:7:10": failed to extract shortcode: template for shortcode "nono" not found`, fe.Error())
156 },
157 },
158 {
159 name: "Invalid YAML front matter",
160 fileType: yamlcontent,
161 fileFixer: func(content string) string {
162 return `---
163 title: "My YAML Content"
164 foo bar
165 ---
166 `
167 },
168 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
169 a.assertLineNumber(3, err)
170 },
171 },
172 {
173 name: "Invalid TOML front matter",
174 fileType: tomlcontent,
175 fileFixer: func(content string) string {
176 return strings.Replace(content, "description = ", "description &", 1)
177 },
178 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
179 fe := a.getFileError(err)
180 a.c.Assert(fe.Position().LineNumber, qt.Equals, 6)
181 },
182 },
183 {
184 name: "Invalid JSON front matter",
185 fileType: jsoncontent,
186 fileFixer: func(content string) string {
187 return strings.Replace(content, "\"description\":", "\"description\"", 1)
188 },
189 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
190 fe := a.getFileError(err)
191 a.c.Assert(fe.Position().LineNumber, qt.Equals, 3)
192 },
193 },
194 {
195 // See https://github.com/gohugoio/hugo/issues/5327
196 name: "Panic in template Execute",
197 fileType: single,
198 fileFixer: func(content string) string {
199 return strings.Replace(content, ".Title", ".Parent.Parent.Parent", 1)
200 },
201
202 assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
203 a.c.Assert(err, qt.Not(qt.IsNil))
204 fe := a.getFileError(err)
205 a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
206 a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 21)
207 },
208 },
209 }
210
211 for _, test := range tests {
212 if test.name != "Invalid JSON front matter" {
213 continue
214 }
215 test := test
216 t.Run(test.name, func(t *testing.T) {
217 t.Parallel()
218 c := qt.New(t)
219 errorAsserter := testSiteBuildErrorAsserter{
220 c: c,
221 name: test.name,
222 }
223
224 b := newTestSitesBuilder(t).WithSimpleConfigFile()
225
226 f := func(fileType, content string) string {
227 if fileType != test.fileType {
228 return content
229 }
230 return test.fileFixer(content)
231 }
232
233 b.WithTemplatesAdded("layouts/shortcodes/sc.html", f(shortcode, `SHORTCODE L1
234 SHORTCODE L2
235 SHORTCODE L3:
236 SHORTCODE L4: {{ .Page.Title }}
237 `))
238 b.WithTemplatesAdded("layouts/_default/baseof.html", f(base, `BASEOF L1
239 BASEOF L2
240 BASEOF L3
241 BASEOF L4{{ if .Title }}{{ end }}
242 {{block "main" .}}This is the main content.{{end}}
243 BASEOF L6
244 `))
245
246 b.WithTemplatesAdded("layouts/_default/single.html", f(single, `{{ define "main" }}
247 SINGLE L2:
248 SINGLE L3:
249 SINGLE L4:
250 SINGLE L5: {{ .Title }} {{ .Content }}
251 {{ end }}
252 `))
253
254 b.WithTemplatesAdded("layouts/foo/single.html", f(single, `
255 SINGLE L2:
256 SINGLE L3:
257 SINGLE L4:
258 SINGLE L5: {{ .Title }} {{ .Content }}
259 `))
260
261 b.WithContent("myyaml.md", f(yamlcontent, `---
262 title: "The YAML"
263 ---
264
265 Some content.
266
267 {{< sc >}}
268
269 Some more text.
270
271 The end.
272
273 `))
274
275 b.WithContent("mytoml.md", f(tomlcontent, `+++
276 title = "The TOML"
277 p1 = "v"
278 p2 = "v"
279 p3 = "v"
280 description = "Descriptioon"
281 +++
282
283 Some content.
284
285
286 `))
287
288 b.WithContent("myjson.md", f(jsoncontent, `{
289 "title": "This is a title",
290 "description": "This is a description."
291 }
292
293 Some content.
294
295
296 `))
297
298 createErr := b.CreateSitesE()
299 if test.assertCreateError != nil {
300 test.assertCreateError(errorAsserter, createErr)
301 } else {
302 c.Assert(createErr, qt.IsNil)
303 }
304
305 if createErr == nil {
306 buildErr := b.BuildE(BuildCfg{})
307 if test.assertBuildError != nil {
308 test.assertBuildError(errorAsserter, buildErr)
309 } else {
310 c.Assert(buildErr, qt.IsNil)
311 }
312 }
313 })
314 }
315
316 }
317
318 // Issue 9852
319 func TestErrorMinify(t *testing.T) {
320 t.Parallel()
321
322 files := `
323 -- config.toml --
324 minify = true
325
326 -- layouts/index.html --
327 <body>
328 <script>=;</script>
329 </body>
330
331 `
332
333 b, err := NewIntegrationTestBuilder(
334 IntegrationTestConfig{
335 T: t,
336 TxtarString: files,
337 },
338 ).BuildE()
339
340 fe := herrors.UnwrapFileError(err)
341 b.Assert(fe, qt.IsNotNil)
342 b.Assert(fe.Position().LineNumber, qt.Equals, 2)
343 b.Assert(fe.Position().ColumnNumber, qt.Equals, 9)
344 b.Assert(fe.Error(), qt.Contains, "unexpected = in expression on line 2 and column 9")
345 b.Assert(filepath.ToSlash(fe.Position().Filename), qt.Contains, "hugo-transform-error")
346 b.Assert(os.Remove(fe.Position().Filename), qt.IsNil)
347
348 }
349
350 func TestErrorNestedRender(t *testing.T) {
351 t.Parallel()
352
353 files := `
354 -- config.toml --
355 -- content/_index.md --
356 ---
357 title: "Home"
358 ---
359 -- layouts/index.html --
360 line 1
361 line 2
362 1{{ .Render "myview" }}
363 -- layouts/_default/myview.html --
364 line 1
365 12{{ partial "foo.html" . }}
366 line 4
367 line 5
368 -- layouts/partials/foo.html --
369 line 1
370 line 2
371 123{{ .ThisDoesNotExist }}
372 line 4
373 `
374
375 b, err := NewIntegrationTestBuilder(
376 IntegrationTestConfig{
377 T: t,
378 TxtarString: files,
379 },
380 ).BuildE()
381
382 b.Assert(err, qt.IsNotNil)
383 errors := herrors.UnwrapFileErrorsWithErrorContext(err)
384 b.Assert(errors, qt.HasLen, 4)
385 b.Assert(errors[0].Position().LineNumber, qt.Equals, 3)
386 b.Assert(errors[0].Position().ColumnNumber, qt.Equals, 4)
387 b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/layouts/index.html:3:4": execute of template failed`))
388 b.Assert(errors[0].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "1{{ .Render \"myview\" }}"})
389 b.Assert(errors[2].Position().LineNumber, qt.Equals, 2)
390 b.Assert(errors[2].Position().ColumnNumber, qt.Equals, 5)
391 b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"})
392
393 b.Assert(errors[3].Position().LineNumber, qt.Equals, 3)
394 b.Assert(errors[3].Position().ColumnNumber, qt.Equals, 6)
395 b.Assert(errors[3].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"})
396
397 }
398
399 func TestErrorNestedShortocde(t *testing.T) {
400 t.Parallel()
401
402 files := `
403 -- config.toml --
404 -- content/_index.md --
405 ---
406 title: "Home"
407 ---
408
409 ## Hello
410 {{< hello >}}
411
412 -- layouts/index.html --
413 line 1
414 line 2
415 {{ .Content }}
416 line 5
417 -- layouts/shortcodes/hello.html --
418 line 1
419 12{{ partial "foo.html" . }}
420 line 4
421 line 5
422 -- layouts/partials/foo.html --
423 line 1
424 line 2
425 123{{ .ThisDoesNotExist }}
426 line 4
427 `
428
429 b, err := NewIntegrationTestBuilder(
430 IntegrationTestConfig{
431 T: t,
432 TxtarString: files,
433 },
434 ).BuildE()
435
436 b.Assert(err, qt.IsNotNil)
437 errors := herrors.UnwrapFileErrorsWithErrorContext(err)
438
439 b.Assert(errors, qt.HasLen, 3)
440
441 b.Assert(errors[0].Position().LineNumber, qt.Equals, 6)
442 b.Assert(errors[0].Position().ColumnNumber, qt.Equals, 1)
443 b.Assert(errors[0].ErrorContext().ChromaLexer, qt.Equals, "md")
444 b.Assert(errors[0].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":`))
445 b.Assert(errors[0].ErrorContext().Lines, qt.DeepEquals, []string{"", "## Hello", "{{< hello >}}", ""})
446 b.Assert(errors[1].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"})
447 b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"})
448
449 }
450
451 func TestErrorRenderHookHeading(t *testing.T) {
452 t.Parallel()
453
454 files := `
455 -- config.toml --
456 -- content/_index.md --
457 ---
458 title: "Home"
459 ---
460
461 ## Hello
462
463 -- layouts/index.html --
464 line 1
465 line 2
466 {{ .Content }}
467 line 5
468 -- layouts/_default/_markup/render-heading.html --
469 line 1
470 12{{ .Levels }}
471 line 4
472 line 5
473 `
474
475 b, err := NewIntegrationTestBuilder(
476 IntegrationTestConfig{
477 T: t,
478 TxtarString: files,
479 },
480 ).BuildE()
481
482 b.Assert(err, qt.IsNotNil)
483 errors := herrors.UnwrapFileErrorsWithErrorContext(err)
484
485 b.Assert(errors, qt.HasLen, 2)
486 b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:1:1": "/layouts/_default/_markup/render-heading.html:2:5": execute of template failed`))
487
488 }
489
490 func TestErrorRenderHookCodeblock(t *testing.T) {
491 t.Parallel()
492
493 files := `
494 -- config.toml --
495 -- content/_index.md --
496 ---
497 title: "Home"
498 ---
499
500 ## Hello
501
502 §§§ foo
503 bar
504 §§§
505
506
507 -- layouts/index.html --
508 line 1
509 line 2
510 {{ .Content }}
511 line 5
512 -- layouts/_default/_markup/render-codeblock-foo.html --
513 line 1
514 12{{ .Foo }}
515 line 4
516 line 5
517 `
518
519 b, err := NewIntegrationTestBuilder(
520 IntegrationTestConfig{
521 T: t,
522 TxtarString: files,
523 },
524 ).BuildE()
525
526 b.Assert(err, qt.IsNotNil)
527 errors := herrors.UnwrapFileErrorsWithErrorContext(err)
528
529 b.Assert(errors, qt.HasLen, 2)
530 first := errors[0]
531 b.Assert(first.Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:7:1": "/layouts/_default/_markup/render-codeblock-foo.html:2:5": execute of template failed`))
532
533 }
534
535 func TestErrorInBaseTemplate(t *testing.T) {
536 t.Parallel()
537
538 filesTemplate := `
539 -- config.toml --
540 -- content/_index.md --
541 ---
542 title: "Home"
543 ---
544 -- layouts/baseof.html --
545 line 1 base
546 line 2 base
547 {{ block "main" . }}empty{{ end }}
548 line 4 base
549 {{ block "toc" . }}empty{{ end }}
550 -- layouts/index.html --
551 {{ define "main" }}
552 line 2 index
553 line 3 index
554 line 4 index
555 {{ end }}
556 {{ define "toc" }}
557 TOC: {{ partial "toc.html" . }}
558 {{ end }}
559 -- layouts/partials/toc.html --
560 toc line 1
561 toc line 2
562 toc line 3
563 toc line 4
564
565
566
567
568 `
569
570 t.Run("base template", func(t *testing.T) {
571 files := strings.Replace(filesTemplate, "line 4 base", "123{{ .ThisDoesNotExist \"abc\" }}", 1)
572
573 b, err := NewIntegrationTestBuilder(
574 IntegrationTestConfig{
575 T: t,
576 TxtarString: files,
577 },
578 ).BuildE()
579
580 b.Assert(err, qt.IsNotNil)
581 b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`render of "home" failed: "/layouts/baseof.html:4:6"`))
582
583 })
584
585 t.Run("index template", func(t *testing.T) {
586 files := strings.Replace(filesTemplate, "line 3 index", "1234{{ .ThisDoesNotExist \"abc\" }}", 1)
587
588 b, err := NewIntegrationTestBuilder(
589 IntegrationTestConfig{
590 T: t,
591 TxtarString: files,
592 },
593 ).BuildE()
594
595 b.Assert(err, qt.IsNotNil)
596 b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`render of "home" failed: "/layouts/index.html:3:7"`))
597
598 })
599
600 t.Run("partial from define", func(t *testing.T) {
601 files := strings.Replace(filesTemplate, "toc line 2", "12345{{ .ThisDoesNotExist \"abc\" }}", 1)
602
603 b, err := NewIntegrationTestBuilder(
604 IntegrationTestConfig{
605 T: t,
606 TxtarString: files,
607 },
608 ).BuildE()
609
610 b.Assert(err, qt.IsNotNil)
611 b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`render of "home" failed: "/layouts/index.html:7:8": execute of template failed`))
612 b.Assert(err.Error(), qt.Contains, `execute of template failed: template: partials/toc.html:2:8: executing "partials/toc.html"`)
613
614 })
615
616 }
617
618 // https://github.com/gohugoio/hugo/issues/5375
619 func TestSiteBuildTimeout(t *testing.T) {
620 if !htesting.IsCI() {
621 //defer leaktest.CheckTimeout(t, 10*time.Second)()
622 }
623
624 b := newTestSitesBuilder(t)
625 b.WithConfigFile("toml", `
626 timeout = 5
627 `)
628
629 b.WithTemplatesAdded("_default/single.html", `
630 {{ .WordCount }}
631 `, "shortcodes/c.html", `
632 {{ range .Page.Site.RegularPages }}
633 {{ .WordCount }}
634 {{ end }}
635
636 `)
637
638 for i := 1; i < 100; i++ {
639 b.WithContent(fmt.Sprintf("page%d.md", i), `---
640 title: "A page"
641 ---
642
643 {{< c >}}`)
644 }
645
646 b.CreateSites().BuildFail(BuildCfg{})
647 }