Skip to content

Commit

Permalink
Tests etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Jun 11, 2024
1 parent d180a46 commit 32d232b
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 25 deletions.
40 changes: 35 additions & 5 deletions common/herrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ func (e *TimeoutError) Is(target error) bool {
return ok
}

// errMessage wraps an error with a message.
type errMessage struct {
msg string
err error
}

func (e *errMessage) Error() string {
return e.msg
}

func (e *errMessage) Unwrap() error {
return e.err
}

// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
func IsFeatureNotAvailableError(err error) bool {
return errors.Is(err, &FeatureNotAvailableError{})
Expand Down Expand Up @@ -121,22 +135,38 @@ func IsNotExist(err error) bool {

var nilPointerErrRe = regexp.MustCompile(`at <(.*)>: error calling (.*?): runtime error: invalid memory address or nil pointer dereference`)

func ImproveIfNilPointer(inErr error) (outErr error) {
const deferredPrefix = "___hdeferred/"

var deferredStringToRemove = regexp.MustCompile(`executing "___hdeferred/.*" `)

// ImproveRenderErr improves the error message for rendering errors.
func ImproveRenderErr(inErr error) (outErr error) {
outErr = inErr
msg := improveIfNilPointerMsg(inErr)
if msg != "" {
outErr = &errMessage{msg: msg, err: outErr}
}

if strings.Contains(inErr.Error(), deferredPrefix) {
msg := deferredStringToRemove.ReplaceAllString(inErr.Error(), "executing ")
outErr = &errMessage{msg: msg, err: outErr}
}
return
}

func improveIfNilPointerMsg(inErr error) string {
m := nilPointerErrRe.FindStringSubmatch(inErr.Error())
if len(m) == 0 {
return
return ""
}
call := m[1]
field := m[2]
parts := strings.Split(call, ".")
if len(parts) < 2 {
return
return ""
}
receiverName := parts[len(parts)-2]
receiver := strings.Join(parts[:len(parts)-1], ".")
s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
outErr = errors.New(nilPointerErrRe.ReplaceAllString(inErr.Error(), s))
return
return nilPointerErrRe.ReplaceAllString(inErr.Error(), s)
}
2 changes: 1 addition & 1 deletion hugolib/hugo_sites_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ func (h *HugoSites) renderDeferred(l logg.LevelLogger) error {
}
}
if err := s.executeDeferredTemplates(de); err != nil {
return err
return herrors.ImproveRenderErr(err)
}
}

Expand Down
17 changes: 17 additions & 0 deletions hugolib/hugo_sites_build_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,3 +628,20 @@ title: "A page"

b.CreateSites().BuildFail(BuildCfg{})
}

func TestErrorTemplateRuntime(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
-- layouts/index.html --
Home.
{{ .ThisDoesNotExist }}
`

b, err := TestE(t, files)

b.Assert(err, qt.Not(qt.IsNil))
b.Assert(err.Error(), qt.Contains, `/layouts/index.html:2:3`)
b.Assert(err.Error(), qt.Contains, `can't evaluate field ThisDoesNotExist`)
}
2 changes: 1 addition & 1 deletion hugolib/site_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (s *Site) renderPages(ctx *siteRenderContext) error {

err := <-errs
if err != nil {
return fmt.Errorf("failed to render pages: %w", herrors.ImproveIfNilPointer(err))
return fmt.Errorf("failed to render pages: %w", herrors.ImproveRenderErr(err))
}
return nil
}
Expand Down
99 changes: 93 additions & 6 deletions tpl/templates/defer_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@
package templates_test

import (
"strings"
"testing"

qt "github.com/frankban/quicktest"

"github.com/gohugoio/hugo/hugolib"
)

const deferFilesCommon = `
-- hugo.toml --
disableLiveReload = true
disableKinds = ["taxonomy", "term", "rss", "sitemap", "robotsTXT", "404", "section"]
[languages]
[languages.en]
weight = 1
Expand All @@ -42,23 +47,105 @@ outputs: ["html", "amp"]
title: "Heim"
outputs: ["html", "amp"]
---
-- assets/mytext.txt --
Hello.
-- layouts/index.html --
HTML.
{{ .Store.Set "hello" "Hello" }}
{{ $data := dict "page" . }}
{{ with (defer (dict "data" $data) ) }}Title: {{ .page.Title }}|{{ .page.RelPermalink }}|Hello: {{ T "hello" }}{{ end }}$
{{ with (defer (dict "data" $data) ) }}
{{ $mytext := resources.Get "mytext.txt" }}
REPLACE_ME|Title: {{ .page.Title }}|{{ .page.RelPermalink }}|Hello: {{ T "hello" }}|Hello Store: {{ .page.Store.Get "hello" }}|Mytext: {{ $mytext.Content }}|
{{ end }}$
-- layouts/index.amp.html --
AMP.
{{ $data := dict "page" . }}
{{ with (defer (dict "data" $data) ) }}Title: {{ .page.Title }}|{{ .page.RelPermalink }}|Hello: {{ T "hello" }}{{ end }}$
{{ with (defer (dict "data" $data) ) }}Title AMP: {{ .page.Title }}|{{ .page.RelPermalink }}|Hello: {{ T "hello" }}{{ end }}$
`

func TestDefer(t *testing.T) {
func TestDeferBasic(t *testing.T) {
t.Parallel()

b := hugolib.Test(t, deferFilesCommon)

b.AssertFileContent("public/index.html", "Title: Home|/|Hello: Hello")
b.AssertFileContent("public/amp/index.html", "Title: Home|/amp/|Hello: Hello")
b.AssertFileContent("public/index.html", "Title: Home|/|Hello: Hello|Hello Store: Hello|Mytext: Hello.|")
b.AssertFileContent("public/amp/index.html", "Title AMP: Home|/amp/|Hello: Hello")
b.AssertFileContent("public/nn/index.html", "Title: Heim|/nn/|Hello: Hei")
b.AssertFileContent("public/nn/amp/index.html", "Title: Heim|/nn/amp/|Hello: Hei")
b.AssertFileContent("public/nn/amp/index.html", "Title AMP: Heim|/nn/amp/|Hello: Hei")
}

func TestDeferErrorParse(t *testing.T) {
t.Parallel()

b, err := hugolib.TestE(t, strings.ReplaceAll(deferFilesCommon, "Title AMP: {{ .page.Title }}", "{{ .page.Title }"))

b.Assert(err, qt.Not(qt.IsNil))
b.Assert(err.Error(), qt.Contains, `index.amp.html:3: unexpected "}" in operand`)
}

func TestDeferErrorRuntime(t *testing.T) {
t.Parallel()

b, err := hugolib.TestE(t, strings.ReplaceAll(deferFilesCommon, "Title AMP: {{ .page.Title }}", "{{ .page.Titles }}"))

b.Assert(err, qt.Not(qt.IsNil))
b.Assert(err.Error(), qt.Contains, `/layouts/index.amp.html:3:47`)
b.Assert(err.Error(), qt.Contains, `execute of template failed: template: index.amp.html:3:47: executing at <.page.Titles>: can't evaluate field Titles`)
}

func TestDeferEditDeferBlock(t *testing.T) {
t.Parallel()

b := hugolib.TestRunning(t, deferFilesCommon)
b.AssertRenderCountPage(4)
b.EditFileReplaceAll("layouts/index.html", "REPLACE_ME", "Edited.").Build()
b.AssertFileContent("public/index.html", "Edited.")
b.AssertRenderCountPage(2)
}

func TestDeferEditResourceUsedInDeferBlock(t *testing.T) {
t.Parallel()

b := hugolib.TestRunning(t, deferFilesCommon)
b.AssertRenderCountPage(4)
b.EditFiles("assets/mytext.txt", "Mytext Hello Edited.").Build()
b.AssertFileContent("public/index.html", "Mytext Hello Edited.")
b.AssertRenderCountPage(2)
}

// This is currently not recommended, but see https://github.com/gohugoio/hugo/issues/12589
func TestDeferMountPublic(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
[module]
[[module.mounts]]
source = "content"
target = "content"
[[module.mounts]]
source = "layouts"
target = "layouts"
[[module.mounts]]
source = 'public'
target = 'assets/public'
-- public/.gitkeep --
-- layouts/index.html --
Home.
{{ $mydata := dict "v1" "v1value" }}
{{ $json := resources.FromString "mydata/data.json" ($mydata | jsonify ) }}
{{ $nop := $json.RelPermalink }}
{{ with (defer (dict "key" "foo")) }}
{{ $jsonFilePublic := resources.Get "public/mydata/data.json" }}
{{ with $jsonFilePublic }}
{{ $m := $jsonFilePublic | transform.Unmarshal }}
v1: {{ $m.v1 }}
{{ end }}
{{ end }}
`

b := hugolib.Test(t, files)

b.AssertFileContent("public/index.html", "v1: v1value")
}
30 changes: 19 additions & 11 deletions tpl/tplimpl/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,12 @@ func newTemplateNamespace(funcs map[string]any) *templateNamespace {
}
}

func newTemplateState(templ tpl.Template, info templateInfo, id identity.Identity) *templateState {
func newTemplateState(owner *templateState, templ tpl.Template, info templateInfo, id identity.Identity) *templateState {
if id == nil {
id = info
}
return &templateState{
owner: owner,
info: info,
typ: info.resolveType(),
Template: templ,
Expand Down Expand Up @@ -261,7 +262,11 @@ func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Templat

execErr := t.executor.ExecuteWithContext(ctx, templ, wr, data)
if execErr != nil {
execErr = t.addFileContext(templ, execErr)
owner := templ
if ts, ok := templ.(*templateState); ok && ts.owner != nil {
owner = ts.owner
}
execErr = t.addFileContext(owner, execErr)
}
return execErr
}
Expand Down Expand Up @@ -498,7 +503,7 @@ func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format
return nil, false, err
}

ts := newTemplateState(templ, overlay, identity.Or(base, overlay))
ts := newTemplateState(nil, templ, overlay, identity.Or(base, overlay))

if found {
ts.baseInfo = base
Expand Down Expand Up @@ -747,7 +752,7 @@ func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *t
}

for k, v := range c.deferNodes {
if err := t.main.fromListNode(k, ts.isText(), v); err != nil {
if err := t.main.addDeferredTemplate(ts, k, v); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -864,7 +869,7 @@ func (t *templateHandler) extractPartials(templ tpl.Template) error {
continue
}

ts := newTemplateState(templ, templateInfo{name: templ.Name()}, nil)
ts := newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
ts.typ = templatePartial

t.main.mu.RLock()
Expand Down Expand Up @@ -1002,19 +1007,19 @@ func (t *templateNamespace) newTemplateLookup(in *templateState) func(name strin
return templ
}
if templ, found := findTemplateIn(name, in); found {
return newTemplateState(templ, templateInfo{name: templ.Name()}, nil)
return newTemplateState(nil, templ, templateInfo{name: templ.Name()}, nil)
}
return nil
}
}

func (t *templateNamespace) fromListNode(name string, isText bool, n *parse.ListNode) error {
func (t *templateNamespace) addDeferredTemplate(owner *templateState, name string, n *parse.ListNode) error {
t.mu.Lock()
defer t.mu.Unlock()

var templ tpl.Template

if isText {
if owner.isText() {
prototype := t.prototypeText
tt, err := prototype.New(name).Parse("")
if err != nil {
Expand All @@ -1032,7 +1037,7 @@ func (t *templateNamespace) fromListNode(name string, isText bool, n *parse.List
templ = tt
}

t.templates[name] = newTemplateState(templ, templateInfo{name: name}, nil)
t.templates[name] = newTemplateState(owner, templ, templateInfo{name: name}, nil)

return nil
}
Expand All @@ -1049,7 +1054,7 @@ func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
return nil, err
}

ts := newTemplateState(templ, info, nil)
ts := newTemplateState(nil, templ, info, nil)

t.templates[info.name] = ts

Expand All @@ -1063,7 +1068,7 @@ func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
return nil, err
}

ts := newTemplateState(templ, info, nil)
ts := newTemplateState(nil, templ, info, nil)

t.templates[info.name] = ts

Expand All @@ -1075,6 +1080,9 @@ var _ tpl.IsInternalTemplateProvider = (*templateState)(nil)
type templateState struct {
tpl.Template

// Set for deferred templates.
owner *templateState

typ templateType
parseInfo tpl.ParseInfo
id identity.Identity
Expand Down
2 changes: 1 addition & 1 deletion tpl/tplimpl/template_ast_transformers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestTransformRecursiveTemplate(t *testing.T) {
}

func newTestTemplate(templ tpl.Template) *templateState {
return newTemplateState(
return newTemplateState(nil,
templ,
templateInfo{
name: templ.Name(),
Expand Down

0 comments on commit 32d232b

Please sign in to comment.