mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
commands: Improve server startup to make tests less flaky
Do this by announcing/listen on the local address before we start the server.
This commit is contained in:
parent
0e305d6958
commit
9539069f5e
5 changed files with 84 additions and 55 deletions
|
@ -17,6 +17,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -88,7 +89,8 @@ type commandeer struct {
|
||||||
// Used in cases where we get flooded with events in server mode.
|
// Used in cases where we get flooded with events in server mode.
|
||||||
debounce func(f func())
|
debounce func(f func())
|
||||||
|
|
||||||
serverPorts []int
|
serverPorts []serverPortListener
|
||||||
|
|
||||||
languagesConfigured bool
|
languagesConfigured bool
|
||||||
languages langs.Languages
|
languages langs.Languages
|
||||||
doLiveReload bool
|
doLiveReload bool
|
||||||
|
@ -105,6 +107,11 @@ type commandeer struct {
|
||||||
buildErr error
|
buildErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type serverPortListener struct {
|
||||||
|
p int
|
||||||
|
ln net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
func newCommandeerHugoState() *commandeerHugoState {
|
func newCommandeerHugoState() *commandeerHugoState {
|
||||||
return &commandeerHugoState{
|
return &commandeerHugoState{
|
||||||
created: make(chan struct{}),
|
created: make(chan struct{}),
|
||||||
|
@ -420,6 +427,7 @@ func (c *commandeer) loadConfig() error {
|
||||||
if h == nil || c.failOnInitErr {
|
if h == nil || c.failOnInitErr {
|
||||||
err = createErr
|
err = createErr
|
||||||
}
|
}
|
||||||
|
|
||||||
c.hugoSites = h
|
c.hugoSites = h
|
||||||
// TODO(bep) improve.
|
// TODO(bep) improve.
|
||||||
if c.buildLock == nil && h != nil {
|
if c.buildLock == nil && h != nil {
|
||||||
|
|
|
@ -48,7 +48,7 @@ import (
|
||||||
|
|
||||||
type serverCmd struct {
|
type serverCmd struct {
|
||||||
// Can be used to stop the server. Useful in tests
|
// Can be used to stop the server. Useful in tests
|
||||||
stop <-chan bool
|
stop chan bool
|
||||||
|
|
||||||
disableLiveReload bool
|
disableLiveReload bool
|
||||||
navigateToChanged bool
|
navigateToChanged bool
|
||||||
|
@ -70,7 +70,7 @@ func (b *commandsBuilder) newServerCmd() *serverCmd {
|
||||||
return b.newServerCmdSignaled(nil)
|
return b.newServerCmdSignaled(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *commandsBuilder) newServerCmdSignaled(stop <-chan bool) *serverCmd {
|
func (b *commandsBuilder) newServerCmdSignaled(stop chan bool) *serverCmd {
|
||||||
cc := &serverCmd{stop: stop}
|
cc := &serverCmd{stop: stop}
|
||||||
|
|
||||||
cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{
|
cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{
|
||||||
|
@ -89,7 +89,13 @@ By default hugo will also watch your files for any changes you make and
|
||||||
automatically rebuild the site. It will then live reload any open browser pages
|
automatically rebuild the site. It will then live reload any open browser pages
|
||||||
and push the latest content to them. As most Hugo sites are built in a fraction
|
and push the latest content to them. As most Hugo sites are built in a fraction
|
||||||
of a second, you will be able to save and see your changes nearly instantly.`,
|
of a second, you will be able to save and see your changes nearly instantly.`,
|
||||||
RunE: cc.server,
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
err := cc.server(cmd, args)
|
||||||
|
if err != nil && cc.stop != nil {
|
||||||
|
cc.stop <- true
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen")
|
cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen")
|
||||||
|
@ -130,8 +136,6 @@ func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverPorts []int
|
|
||||||
|
|
||||||
func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||||
// If a Destination is provided via flag write to disk
|
// If a Destination is provided via flag write to disk
|
||||||
destination, _ := cmd.Flags().GetString("destination")
|
destination, _ := cmd.Flags().GetString("destination")
|
||||||
|
@ -166,22 +170,21 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
// We can only do this once.
|
// We can only do this once.
|
||||||
serverCfgInit.Do(func() {
|
serverCfgInit.Do(func() {
|
||||||
serverPorts = make([]int, 1)
|
c.serverPorts = make([]serverPortListener, 1)
|
||||||
|
|
||||||
if c.languages.IsMultihost() {
|
if c.languages.IsMultihost() {
|
||||||
if !sc.serverAppend {
|
if !sc.serverAppend {
|
||||||
rerr = newSystemError("--appendPort=false not supported when in multihost mode")
|
rerr = newSystemError("--appendPort=false not supported when in multihost mode")
|
||||||
}
|
}
|
||||||
serverPorts = make([]int, len(c.languages))
|
c.serverPorts = make([]serverPortListener, len(c.languages))
|
||||||
}
|
}
|
||||||
|
|
||||||
currentServerPort := sc.serverPort
|
currentServerPort := sc.serverPort
|
||||||
|
|
||||||
for i := 0; i < len(serverPorts); i++ {
|
for i := 0; i < len(c.serverPorts); i++ {
|
||||||
l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort)))
|
l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort)))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
l.Close()
|
c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
|
||||||
serverPorts[i] = currentServerPort
|
|
||||||
} else {
|
} else {
|
||||||
if i == 0 && sc.cmd.Flags().Changed("port") {
|
if i == 0 && sc.cmd.Flags().Changed("port") {
|
||||||
// port set explicitly by user -- he/she probably meant it!
|
// port set explicitly by user -- he/she probably meant it!
|
||||||
|
@ -189,15 +192,15 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.logger.Println("port", sc.serverPort, "already in use, attempting to use an available port")
|
c.logger.Println("port", sc.serverPort, "already in use, attempting to use an available port")
|
||||||
sp, err := helpers.FindAvailablePort()
|
l, sp, err := helpers.TCPListen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rerr = newSystemError("Unable to find alternative port to use:", err)
|
rerr = newSystemError("Unable to find alternative port to use:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serverPorts[i] = sp.Port
|
c.serverPorts[i] = serverPortListener{ln: l, p: sp.Port}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentServerPort = serverPorts[i] + 1
|
currentServerPort = c.serverPorts[i].p + 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -205,22 +208,20 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.serverPorts = serverPorts
|
|
||||||
|
|
||||||
c.Set("port", sc.serverPort)
|
c.Set("port", sc.serverPort)
|
||||||
if sc.liveReloadPort != -1 {
|
if sc.liveReloadPort != -1 {
|
||||||
c.Set("liveReloadPort", sc.liveReloadPort)
|
c.Set("liveReloadPort", sc.liveReloadPort)
|
||||||
} else {
|
} else {
|
||||||
c.Set("liveReloadPort", serverPorts[0])
|
c.Set("liveReloadPort", c.serverPorts[0].p)
|
||||||
}
|
}
|
||||||
|
|
||||||
isMultiHost := c.languages.IsMultihost()
|
isMultiHost := c.languages.IsMultihost()
|
||||||
for i, language := range c.languages {
|
for i, language := range c.languages {
|
||||||
var serverPort int
|
var serverPort int
|
||||||
if isMultiHost {
|
if isMultiHost {
|
||||||
serverPort = serverPorts[i]
|
serverPort = c.serverPorts[i].p
|
||||||
} else {
|
} else {
|
||||||
serverPort = serverPorts[0]
|
serverPort = c.serverPorts[0].p
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURL, err := sc.fixURL(language, sc.baseURL, serverPort)
|
baseURL, err := sc.fixURL(language, sc.baseURL, serverPort)
|
||||||
|
@ -320,10 +321,11 @@ func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Reques
|
||||||
return r2
|
return r2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) {
|
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string, string, error) {
|
||||||
baseURL := f.baseURLs[i]
|
baseURL := f.baseURLs[i]
|
||||||
root := f.roots[i]
|
root := f.roots[i]
|
||||||
port := f.c.serverPorts[i]
|
port := f.c.serverPorts[i].p
|
||||||
|
listener := f.c.serverPorts[i].ln
|
||||||
|
|
||||||
publishDir := f.c.Cfg.GetString("publishDir")
|
publishDir := f.c.Cfg.GetString("publishDir")
|
||||||
|
|
||||||
|
@ -353,7 +355,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||||
// We're only interested in the path
|
// We're only interested in the path
|
||||||
u, err := url.Parse(baseURL)
|
u, err := url.Parse(baseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", "", errors.Wrap(err, "Invalid baseURL")
|
return nil, nil, "", "", errors.Wrap(err, "Invalid baseURL")
|
||||||
}
|
}
|
||||||
|
|
||||||
decorate := func(h http.Handler) http.Handler {
|
decorate := func(h http.Handler) http.Handler {
|
||||||
|
@ -459,7 +461,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||||
|
|
||||||
endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port))
|
endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port))
|
||||||
|
|
||||||
return mu, u.String(), endpoint, nil
|
return mu, listener, u.String(), endpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
|
var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
|
||||||
|
@ -514,8 +516,10 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
var servers []*http.Server
|
var servers []*http.Server
|
||||||
|
|
||||||
|
wg1, ctx := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
for i := range baseURLs {
|
for i := range baseURLs {
|
||||||
mu, serverURL, endpoint, err := srv.createEndpoint(i)
|
mu, listener, serverURL, endpoint, err := srv.createEndpoint(i)
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: endpoint,
|
Addr: endpoint,
|
||||||
Handler: mu,
|
Handler: mu,
|
||||||
|
@ -532,13 +536,13 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||||
mu.HandleFunc(u.Path+"/livereload", livereload.Handler)
|
mu.HandleFunc(u.Path+"/livereload", livereload.Handler)
|
||||||
}
|
}
|
||||||
jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface)
|
jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface)
|
||||||
go func() {
|
wg1.Go(func() error {
|
||||||
err = srv.ListenAndServe()
|
err = srv.Serve(listener)
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
c.logger.Errorf("Error: %s\n", err.Error())
|
return err
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}()
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
jww.FEEDBACK.Println("Press Ctrl+C to stop")
|
jww.FEEDBACK.Println("Press Ctrl+C to stop")
|
||||||
|
@ -556,15 +560,19 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
wg, ctx := errgroup.WithContext(ctx)
|
wg2, ctx := errgroup.WithContext(ctx)
|
||||||
for _, srv := range servers {
|
for _, srv := range servers {
|
||||||
srv := srv
|
srv := srv
|
||||||
wg.Go(func() error {
|
wg2.Go(func() error {
|
||||||
return srv.Shutdown(ctx)
|
return srv.Shutdown(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return wg.Wait()
|
err1, err2 := wg1.Wait(), wg2.Wait()
|
||||||
|
if err1 != nil {
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
return err2
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixURL massages the baseURL into a form needed for serving
|
// fixURL massages the baseURL into a form needed for serving
|
||||||
|
|
|
@ -34,7 +34,7 @@ import (
|
||||||
func TestServer(t *testing.T) {
|
func TestServer(t *testing.T) {
|
||||||
c := qt.New(t)
|
c := qt.New(t)
|
||||||
|
|
||||||
r := runServerTest(c, "")
|
r := runServerTest(c, true, "")
|
||||||
|
|
||||||
c.Assert(r.err, qt.IsNil)
|
c.Assert(r.err, qt.IsNil)
|
||||||
c.Assert(r.homeContent, qt.Contains, "List: Hugo Commands")
|
c.Assert(r.homeContent, qt.Contains, "List: Hugo Commands")
|
||||||
|
@ -51,7 +51,7 @@ func TestServerPanicOnConfigError(t *testing.T) {
|
||||||
linenos='table'
|
linenos='table'
|
||||||
`
|
`
|
||||||
|
|
||||||
r := runServerTest(c, config)
|
r := runServerTest(c, false, config)
|
||||||
|
|
||||||
c.Assert(r.err, qt.IsNotNil)
|
c.Assert(r.err, qt.IsNotNil)
|
||||||
c.Assert(r.err.Error(), qt.Contains, "cannot parse 'Highlight.LineNos' as bool:")
|
c.Assert(r.err.Error(), qt.Contains, "cannot parse 'Highlight.LineNos' as bool:")
|
||||||
|
@ -88,7 +88,7 @@ baseURL="https://example.org"
|
||||||
args = strings.Split(test.flag, "=")
|
args = strings.Split(test.flag, "=")
|
||||||
}
|
}
|
||||||
|
|
||||||
r := runServerTest(c, config, args...)
|
r := runServerTest(c, true, config, args...)
|
||||||
|
|
||||||
test.assert(c, r)
|
test.assert(c, r)
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ type serverTestResult struct {
|
||||||
publicDirnames map[string]bool
|
publicDirnames map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func runServerTest(c *qt.C, config string, args ...string) (result serverTestResult) {
|
func runServerTest(c *qt.C, getHome bool, config string, args ...string) (result serverTestResult) {
|
||||||
dir, clean, err := createSimpleTestSite(c, testSiteConfig{configTOML: config})
|
dir, clean, err := createSimpleTestSite(c, testSiteConfig{configTOML: config})
|
||||||
defer clean()
|
defer clean()
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
@ -135,34 +135,32 @@ func runServerTest(c *qt.C, config string, args ...string) (result serverTestRes
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
select {
|
if getHome {
|
||||||
// There is no way to know exactly when the server is ready for connections.
|
// Esp. on slow CI machines, we need to wait a little before the web
|
||||||
// We could improve by something like https://golang.org/pkg/net/http/httptest/#Server
|
// server is ready.
|
||||||
// But for now, let us sleep and pray!
|
time.Sleep(567 * time.Millisecond)
|
||||||
case <-time.After(2 * time.Second):
|
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port))
|
||||||
case <-ctx.Done():
|
c.Check(err, qt.IsNil)
|
||||||
result.err = wg.Wait()
|
if err == nil {
|
||||||
return
|
defer resp.Body.Close()
|
||||||
|
result.homeContent = helpers.ReaderToString(resp.Body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port))
|
select {
|
||||||
c.Assert(err, qt.IsNil)
|
case <-stop:
|
||||||
defer resp.Body.Close()
|
case stop <- true:
|
||||||
homeContent := helpers.ReaderToString(resp.Body)
|
}
|
||||||
|
|
||||||
// Stop the server.
|
|
||||||
stop <- true
|
|
||||||
|
|
||||||
result.homeContent = homeContent
|
|
||||||
|
|
||||||
pubFiles, err := os.ReadDir(filepath.Join(dir, "public"))
|
pubFiles, err := os.ReadDir(filepath.Join(dir, "public"))
|
||||||
c.Assert(err, qt.IsNil)
|
c.Check(err, qt.IsNil)
|
||||||
result.publicDirnames = make(map[string]bool)
|
result.publicDirnames = make(map[string]bool)
|
||||||
for _, f := range pubFiles {
|
for _, f := range pubFiles {
|
||||||
result.publicDirnames[f.Name()] = true
|
result.publicDirnames[f.Name()] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
result.err = wg.Wait()
|
result.err = wg.Wait()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,21 @@ func FindAvailablePort() (*net.TCPAddr, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TCPListen starts listening on a valid TCP port.
|
||||||
|
func TCPListen() (net.Listener, *net.TCPAddr, error) {
|
||||||
|
l, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
addr := l.Addr()
|
||||||
|
if a, ok := addr.(*net.TCPAddr); ok {
|
||||||
|
return l, a, nil
|
||||||
|
}
|
||||||
|
l.Close()
|
||||||
|
return nil, nil, fmt.Errorf("unable to obtain a valid tcp port: %v", addr)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// InStringArray checks if a string is an element of a slice of strings
|
// InStringArray checks if a string is an element of a slice of strings
|
||||||
// and returns a boolean value.
|
// and returns a boolean value.
|
||||||
func InStringArray(arr []string, el string) bool {
|
func InStringArray(arr []string, el string) bool {
|
||||||
|
|
|
@ -169,7 +169,7 @@ func testGoFlags() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return "-test.short"
|
return "-timeout=1m"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run tests in 32-bit mode
|
// Run tests in 32-bit mode
|
||||||
|
|
Loading…
Reference in a new issue