github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/hugolib/hugo_sites_build_errors_test.go (about)

     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 TestErrorNestedShortcode(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  }