Skip to content

Commit

Permalink
Avoid sticky errors in subsequent *Init.Do calls
Browse files Browse the repository at this point in the history
When *Init.Do executes the *Init's f function, it stores the error
in the *Init's err field and returns ini.err. If we call an *Init's
Do method again, and there is no error in the subsequent call,
ini.err remains unchanged, and *Init.Do returns it again.

This becomes an issue for the "hugo server" command, which calls
h.init.data.Do within *HugoSites.Data every time the site is
rebuilt. If "hugo server" is started with a valid data file, and
that data file is edited to make it invalid (e.g., with poorly
formatted JSON), then any attempt to fix the data file only causes
Hugo to return the same error.

The fix is to ensure that *Init.Do resets ini.err to nil with each
call.

Fixes gohugoio#7043
  • Loading branch information
ptgott committed Nov 21, 2021
1 parent 75a823a commit aecd3c2
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 0 deletions.
101 changes: 101 additions & 0 deletions commands/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ import (
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"testing"
"time"

"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugofs"

qt "github.com/frankban/quicktest"
)
Expand Down Expand Up @@ -131,3 +135,100 @@ ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s)
func isWindowsCI() bool {
return runtime.GOOS == "windows" && os.Getenv("CI") != ""
}

// Issue 7043
func TestServerReloadWithBadData(t *testing.T) {
c := qt.New(t)
dir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-cli")

cfgStr := `
baseURL = "https://example.org"
title = "Hugo Commands"
`
os.MkdirAll(filepath.Join(dir, "public"), 0777)
os.MkdirAll(filepath.Join(dir, "data"), 0777)
os.MkdirAll(filepath.Join(dir, "layouts"), 0777)

writeFile(t, filepath.Join(dir, "config.toml"), cfgStr)
writeFile(t, filepath.Join(dir, "content", "p1.md"), `
---
title: "P1"
weight: 1
---
Content
`)
defer clean()
c.Assert(err, qt.IsNil)

// The first time we write to the data file, the JSON is valid.
writeFile(t, path.Join(dir, "data", "testdata.json"), `{
"key1": "value1",
"key2": "value2"
}`)

writeFile(t, path.Join(dir, "layouts", "index.html"), `{{- range $k, $v := .Site.Data.testdata -}}
<p>{{$k}} has the value {{$v}}</p>
{{ end }}`)

port := 1331

b := newCommandsBuilder()
stop := make(chan bool)
scmd := b.newServerCmdSignaled(stop)

defer func() {
// Stop the server.
stop <- true
}()

cmd := scmd.getCommand()
cmd.SetArgs([]string{
"-s=" + dir,
fmt.Sprintf("-p=%d", port),
"-d=" + path.Join(dir, "public"),
})

go func() {
_, err = cmd.ExecuteC()
c.Assert(err, qt.IsNil)
}()

// There is no way to know exactly when the server is ready for connections.
// We could improve by something like https://golang.org/pkg/net/http/httptest/#Server
// But for now, let us sleep and pray!
time.Sleep(500 * time.Millisecond)

// Break the JSON by removing the comma
writeFile(t, path.Join(dir, "data", "testdata.json"), `{
"key1": "value1"
"key2": "value2"
}`)

// Wait for the server to make the change
time.Sleep(500 * time.Millisecond)

// Fix the JSON and add a line
writeFile(t, path.Join(dir, "data", "testdata.json"), `{
"key1": "value1",
"key2": "value2",
"key3": "value3"
}`)

// Wait for the server to make the change
time.Sleep(500 * time.Millisecond)

exp := `<p>key1 has the value value1</p>
<p>key2 has the value value2</p>
<p>key3 has the value value3</p>
`

af, err := os.ReadFile(path.Join(dir, "public", "index.html"))
c.Assert(err, qt.IsNil)
c.Assert(string(af), qt.Equals, exp)

return
}
4 changes: 4 additions & 0 deletions lazy/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ func (ini *Init) Do() (interface{}, error) {
panic("init is nil")
}

// Reset the error in case we have already called Do, e.g., when hugo
// server is running.
ini.err = nil

ini.init.Do(func() {
prev := ini.prev
if prev != nil {
Expand Down
21 changes: 21 additions & 0 deletions lazy/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,24 @@ func TestInitBranchOrder(t *testing.T) {

c.Assert(state.V2, qt.Equals, "ABAB")
}

// Sometimes need to call the same *Init's Do method multiple times, e.g., when
// hugo server is running. In these cases, we need to make sure that an error
// returned by an earlier call does not persist in memory for later calls.
// See issue 7043
func TestAvoidRepeatDoError(t *testing.T) {
r := false
i := New().Add(func() (interface{}, error) {
if r {
return nil, nil
}
return nil, errors.New("r is false")
})
i.Do()
r = true
i.Do()
if i.err != nil {
t.Errorf("expected a nil error but got: %v", i.err)
}

}

0 comments on commit aecd3c2

Please sign in to comment.