commands: Add CLI tests

See #4598
This commit is contained in:
Bjørn Erik Pedersen 2018-04-10 19:16:09 +02:00
parent 4d32f2fa89
commit e8d6ca9531
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
10 changed files with 199 additions and 71 deletions

View file

@ -45,8 +45,6 @@ creating a benchmark.`,
cmd.Flags().StringVar(&c.memProfileFile, "memprofile", "", "path/filename for the memory profile file") cmd.Flags().StringVar(&c.memProfileFile, "memprofile", "", "path/filename for the memory profile file")
cmd.Flags().IntVarP(&c.benchmarkTimes, "count", "n", 13, "number of times to build the site") cmd.Flags().IntVarP(&c.benchmarkTimes, "count", "n", 13, "number of times to build the site")
cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
cmd.RunE = c.benchmark cmd.RunE = c.benchmark
return c return c
@ -56,6 +54,7 @@ func (c *benchmarkCmd) benchmark(cmd *cobra.Command, args []string) error {
cfgInit := func(c *commandeer) error { cfgInit := func(c *commandeer) error {
return nil return nil
} }
comm, err := initializeConfig(false, &c.hugoBuilderCommon, c, cfgInit) comm, err := initializeConfig(false, &c.hugoBuilderCommon, c, cfgInit)
if err != nil { if err != nil {
return err return err

View file

@ -40,7 +40,7 @@ import (
type commandeer struct { type commandeer struct {
*deps.DepsCfg *deps.DepsCfg
h *hugoBuilderCommon h *hugoBuilderCommon
ftch flagsToConfigHandler ftch flagsToConfigHandler
pathSpec *helpers.PathSpec pathSpec *helpers.PathSpec
@ -109,7 +109,7 @@ func newCommandeer(running bool, h *hugoBuilderCommon, f flagsToConfigHandler, d
c := &commandeer{ c := &commandeer{
h: h, h: h,
ftch: f, ftch: f,
doWithCommandeer: doWithCommandeer, doWithCommandeer: doWithCommandeer,
visitedURLs: types.NewEvictingStringQueue(10), visitedURLs: types.NewEvictingStringQueue(10),
debounce: rebuildDebouncer, debounce: rebuildDebouncer,

133
commands/commands_test.go Normal file
View file

@ -0,0 +1,133 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestCommands(t *testing.T) {
assert := require.New(t)
dir, err := createSimpleTestSite(t)
assert.NoError(err)
dirOut, err := ioutil.TempDir("", "hugo-cli-out")
assert.NoError(err)
defer func() {
os.RemoveAll(dir)
os.RemoveAll(dirOut)
}()
sourceFlag := fmt.Sprintf("-s=%s", dir)
tests := []struct {
commands []string
flags []string
}{
{[]string{"check", "ulimit"}, nil},
{[]string{"env"}, nil},
{[]string{"version"}, nil},
// no args = hugo build
{nil, []string{sourceFlag}},
// TODO(bep) cli refactor remove the HugoSites global and enable the below
//{nil, []string{sourceFlag, "--renderToMemory"}},
{[]string{"benchmark"}, []string{sourceFlag, "-n=1"}},
{[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}},
{[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}},
{[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}},
{[]string{"gen", "autocomplete"}, []string{"--completionfile=" + filepath.Join(dirOut, "autocomplete.txt")}},
{[]string{"gen", "chromastyles"}, []string{"--style=manni"}},
{[]string{"gen", "doc"}, []string{"--dir=" + filepath.Join(dirOut, "doc")}},
{[]string{"gen", "man"}, []string{"--dir=" + filepath.Join(dirOut, "man")}},
{[]string{"list", "drafts"}, []string{sourceFlag}},
{[]string{"list", "expired"}, []string{sourceFlag}},
{[]string{"list", "future"}, []string{sourceFlag}},
{[]string{"new", "new-page.md"}, []string{sourceFlag}},
{[]string{"new", "site", filepath.Join(dirOut, "new-site")}, nil},
// TODO(bep) cli refactor fix https://github.com/gohugoio/hugo/issues/4450
//{[]string{"new", "theme", filepath.Join(dirOut, "new-theme")}, nil},
}
for _, test := range tests {
hugoCmd := newHugoCompleteCmd()
test.flags = append(test.flags, "--quiet")
hugoCmd.SetArgs(append(test.commands, test.flags...))
// TODO(bep) capture output and add some simple asserts
assert.NoError(hugoCmd.Execute(), fmt.Sprintf("%v", test.commands))
}
}
func createSimpleTestSite(t *testing.T) (string, error) {
d, e := ioutil.TempDir("", "hugo-cli")
if e != nil {
return "", e
}
// Just the basic. These are for CLI tests, not site testing.
writeFile(t, filepath.Join(d, "config.toml"), `
baseURL = "https://example.org"
title = "Hugo Commands"
`)
writeFile(t, filepath.Join(d, "content", "p1.md"), `
---
title: "P1"
weight: 1
---
Content
`)
writeFile(t, filepath.Join(d, "layouts", "_default", "single.html"), `
Single: {{ .Title }}
`)
writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
List: {{ .Title }}
`)
return d, nil
}
func writeFile(t *testing.T, filename, content string) {
must(t, os.MkdirAll(filepath.Dir(filename), os.FileMode(0755)))
must(t, ioutil.WriteFile(filename, []byte(content), os.FileMode(0755)))
}
func must(t *testing.T, err error) {
if err != nil {
t.Fatal(err)
}
}

View file

@ -32,17 +32,17 @@ var (
_ cmder = (*convertCmd)(nil) _ cmder = (*convertCmd)(nil)
) )
// TODO(bep) cli refactor
var outputDir string
var unsafe bool
type convertCmd struct { type convertCmd struct {
outputDir string
unsafe bool
*baseBuilderCmd *baseBuilderCmd
} }
func newConvertCmd() *convertCmd { func newConvertCmd() *convertCmd {
cc := &convertCmd{} cc := &convertCmd{}
// TODO(bep) cli refactor this is more than it had
cc.baseBuilderCmd = newBuilderCmd(&cobra.Command{ cc.baseBuilderCmd = newBuilderCmd(&cobra.Command{
Use: "convert", Use: "convert",
Short: "Convert your content to different formats", Short: "Convert your content to different formats",
@ -82,17 +82,16 @@ to use YAML for the front matter.`,
}, },
) )
// TODO(bep) cli refactor cc.cmd.PersistentFlags().StringVarP(&cc.outputDir, "output", "o", "", "filesystem path to write files to")
// cmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "filesystem path to write files to") cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
// cmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") cc.cmd.PersistentFlags().BoolVar(&cc.unsafe, "unsafe", false, "enable less safe operations, please backup first")
// cmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "enable less safe operations, please backup first")
cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
return cc return cc
} }
func (cc *convertCmd) convertContents(mark rune) error { func (cc *convertCmd) convertContents(mark rune) error {
if outputDir == "" && !unsafe { if cc.outputDir == "" && !cc.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path") return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
} }
@ -114,17 +113,17 @@ func (cc *convertCmd) convertContents(mark rune) error {
site.Log.FEEDBACK.Println("processing", len(site.AllPages), "content files") site.Log.FEEDBACK.Println("processing", len(site.AllPages), "content files")
for _, p := range site.AllPages { for _, p := range site.AllPages {
if err := convertAndSavePage(p, site, mark); err != nil { if err := cc.convertAndSavePage(p, site, mark); err != nil {
return err return err
} }
} }
return nil return nil
} }
func convertAndSavePage(p *hugolib.Page, site *hugolib.Site, mark rune) error { func (cc *convertCmd) convertAndSavePage(p *hugolib.Page, site *hugolib.Site, mark rune) error {
// The resources are not in .Site.AllPages. // The resources are not in .Site.AllPages.
for _, r := range p.Resources.ByType("page") { for _, r := range p.Resources.ByType("page") {
if err := convertAndSavePage(r.(*hugolib.Page), site, mark); err != nil { if err := cc.convertAndSavePage(r.(*hugolib.Page), site, mark); err != nil {
return err return err
} }
} }
@ -182,8 +181,8 @@ func convertAndSavePage(p *hugolib.Page, site *hugolib.Site, mark rune) error {
} }
newFilename := p.Filename() newFilename := p.Filename()
if outputDir != "" { if cc.outputDir != "" {
newFilename = filepath.Join(outputDir, p.Dir(), newPage.LogicalName()) newFilename = filepath.Join(cc.outputDir, p.Dir(), newPage.LogicalName())
} }
if err = newPage.SaveSourceAs(newFilename); err != nil { if err = newPage.SaveSourceAs(newFilename); err != nil {

View file

@ -141,6 +141,9 @@ Complete documentation is available at http://gohugo.io/.`,
// Set bash-completion // Set bash-completion
_ = cc.cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{}) _ = cc.cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{})
cc.cmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags)
cc.cmd.SilenceUsage = true
return cc return cc
} }
@ -191,6 +194,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files") cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files") cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
cmd.Flags().BoolP("i18n-warnings", "", false, "print missing translations") cmd.Flags().BoolP("i18n-warnings", "", false, "print missing translations")
cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)") cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
@ -214,23 +218,11 @@ func Reset() error {
return nil return nil
} }
var (
hugoCommand = newHugoCmd()
// HugoCmd is Hugo's root command.
// Every other command attached to HugoCmd is a child command to it.
HugoCmd = hugoCommand.getCommand()
)
// Execute adds all child commands to the root command HugoCmd and sets flags appropriately. // Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
func Execute() { func Execute() {
HugoCmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags) hugoCmd := newHugoCompleteCmd()
HugoCmd.SilenceUsage = true if c, err := hugoCmd.ExecuteC(); err != nil {
addAllCommands()
if c, err := HugoCmd.ExecuteC(); err != nil {
if isUserError(err) { if isUserError(err) {
c.Println("") c.Println("")
c.Println(c.UsageString()) c.Println(c.UsageString())
@ -240,9 +232,16 @@ func Execute() {
} }
} }
func newHugoCompleteCmd() *cobra.Command {
hugoCmd := newHugoCmd().getCommand()
addAllCommands(hugoCmd)
return hugoCmd
}
// addAllCommands adds child commands to the root command HugoCmd. // addAllCommands adds child commands to the root command HugoCmd.
func addAllCommands() { func addAllCommands(root *cobra.Command) {
addCommands( addCommands(
root,
newServerCmd(), newServerCmd(),
newVersionCmd(), newVersionCmd(),
newEnvCmd(), newEnvCmd(),
@ -257,9 +256,9 @@ func addAllCommands() {
) )
} }
func addCommands(commands ...cmder) { func addCommands(root *cobra.Command, commands ...cmder) {
for _, command := range commands { for _, command := range commands {
HugoCmd.AddCommand(command.getCommand()) root.AddCommand(command.getCommand())
} }
} }

View file

@ -151,7 +151,7 @@ expired.`,
) )
// TODO(bep) cli refactor // TODO(bep) cli refactor
// cc.cmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
return cc return cc

View file

@ -30,6 +30,7 @@ import (
var _ cmder = (*newCmd)(nil) var _ cmder = (*newCmd)(nil)
type newCmd struct { type newCmd struct {
hugoBuilderCommon
contentEditor string contentEditor string
contentType string contentType string
@ -37,8 +38,8 @@ type newCmd struct {
} }
func newNewCmd() *newCmd { func newNewCmd() *newCmd {
ccmd := &newCmd{baseCmd: newBaseCmd(nil)} cc := &newCmd{}
cmd := &cobra.Command{ cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "new [path]", Use: "new [path]",
Short: "Create new content for your site", Short: "Create new content for your site",
Long: `Create a new content file and automatically set the date and title. Long: `Create a new content file and automatically set the date and title.
@ -48,21 +49,19 @@ You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.`, If archetypes are provided in your theme or site, they will be used.`,
RunE: ccmd.newContent, RunE: cc.newContent,
} })
cmd.Flags().StringVarP(&ccmd.contentType, "kind", "k", "", "content type to create") cc.cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
// TODO(bep) cli refactor // TODO(bep) cli refactor
// cmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
cmd.Flags().StringVar(&ccmd.contentEditor, "editor", "", "edit new content with this editor, if provided") cc.cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
cmd.AddCommand(newNewSiteCmd().getCommand()) cc.cmd.AddCommand(newNewSiteCmd().getCommand())
cmd.AddCommand(newNewThemeCmd().getCommand()) cc.cmd.AddCommand(newNewThemeCmd().getCommand())
ccmd.cmd = cmd return cc
return ccmd
} }
func (n *newCmd) newContent(cmd *cobra.Command, args []string) error { func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
@ -73,7 +72,7 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
return nil return nil
} }
c, err := initializeConfig(false, nil, n, cfgInit) c, err := initializeConfig(false, &n.hugoBuilderCommon, n, cfgInit)
if err != nil { if err != nil {
return err return err

View file

@ -32,10 +32,11 @@ var _ cmder = (*newThemeCmd)(nil)
type newThemeCmd struct { type newThemeCmd struct {
*baseCmd *baseCmd
hugoBuilderCommon
} }
func newNewThemeCmd() *newThemeCmd { func newNewThemeCmd() *newThemeCmd {
ccmd := &newThemeCmd{newBaseCmd(nil)} ccmd := &newThemeCmd{baseCmd: newBaseCmd(nil)}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "theme [name]", Use: "theme [name]",
@ -53,7 +54,7 @@ as you see fit.`,
} }
func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error { func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
c, err := initializeConfig(false, nil, n, nil) c, err := initializeConfig(false, &n.hugoBuilderCommon, n, nil)
if err != nil { if err != nil {
return err return err

View file

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved. // Copyright 2018 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -56,24 +56,6 @@ type serverCmd struct {
*baseCmd *baseCmd
} }
func (cc *serverCmd) handleFlags(cmd *cobra.Command) {
// TODO(bep) cli refactor fields vs strings
cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen")
cc.cmd.Flags().IntVar(&cc.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
cc.cmd.Flags().StringVarP(&cc.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
cc.cmd.Flags().BoolVarP(&cc.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
cc.cmd.Flags().BoolVar(&cc.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
cc.cmd.Flags().BoolVarP(&cc.serverAppend, "appendPort", "", true, "append port to baseURL")
cc.cmd.Flags().BoolVar(&cc.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
cc.cmd.Flags().String("memstats", "", "log memory usage to this file")
cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
}
func newServerCmd() *serverCmd { func newServerCmd() *serverCmd {
cc := &serverCmd{} cc := &serverCmd{}
@ -96,6 +78,21 @@ of a second, you will be able to save and see your changes nearly instantly.`,
RunE: cc.server, RunE: cc.server,
}) })
// TODO(bep) cli refactor fields vs strings
cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen")
cc.cmd.Flags().IntVar(&cc.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
cc.cmd.Flags().StringVarP(&cc.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
cc.cmd.Flags().BoolVarP(&cc.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
cc.cmd.Flags().BoolVar(&cc.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
cc.cmd.Flags().BoolVarP(&cc.serverAppend, "appendPort", "", true, "append port to baseURL")
cc.cmd.Flags().BoolVar(&cc.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
cc.cmd.Flags().String("memstats", "", "log memory usage to this file")
cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
return cc return cc
} }

View file

@ -23,6 +23,7 @@ import (
) )
func main() { func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())
commands.Execute() commands.Execute()