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 }