commands: Add CLI tests - hugo - [fork] hugo port for 9front
 (HTM) git clone git@git.drkhsh.at/hugo.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit e8d6ca9531d19e4e898c57d77d2fd627ea38ade0
 (DIR) parent 4d32f2fa8969f368b088dc9bcedb45f2c986cb27
 (HTM) Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Tue, 10 Apr 2018 19:16:09 +0200
       
       commands: Add CLI tests
       
       See #4598
       
       Diffstat:
         M commands/benchmark.go               |       3 +--
         M commands/commandeer.go              |       4 ++--
         A commands/commands_test.go           |     133 +++++++++++++++++++++++++++++++
         M commands/convert.go                 |      27 +++++++++++++--------------
         M commands/hugo.go                    |      33 +++++++++++++++----------------
         M commands/list.go                    |       2 +-
         M commands/new.go                     |      27 +++++++++++++--------------
         M commands/new_theme.go               |       5 +++--
         M commands/server.go                  |      35 ++++++++++++++-----------------
         M main.go                             |       1 +
       
       10 files changed, 199 insertions(+), 71 deletions(-)
       ---
 (DIR) diff --git a/commands/benchmark.go b/commands/benchmark.go
       @@ -45,8 +45,6 @@ creating a benchmark.`,
                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().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
       -
                cmd.RunE = c.benchmark
        
                return c
       @@ -56,6 +54,7 @@ func (c *benchmarkCmd) benchmark(cmd *cobra.Command, args []string) error {
                cfgInit := func(c *commandeer) error {
                        return nil
                }
       +
                comm, err := initializeConfig(false, &c.hugoBuilderCommon, c, cfgInit)
                if err != nil {
                        return err
 (DIR) diff --git a/commands/commandeer.go b/commands/commandeer.go
       @@ -40,7 +40,7 @@ import (
        type commandeer struct {
                *deps.DepsCfg
        
       -        h             *hugoBuilderCommon
       +        h    *hugoBuilderCommon
                ftch flagsToConfigHandler
        
                pathSpec    *helpers.PathSpec
       @@ -109,7 +109,7 @@ func newCommandeer(running bool, h *hugoBuilderCommon, f flagsToConfigHandler, d
        
                c := &commandeer{
                        h:                h,
       -                ftch:    f,
       +                ftch:             f,
                        doWithCommandeer: doWithCommandeer,
                        visitedURLs:      types.NewEvictingStringQueue(10),
                        debounce:         rebuildDebouncer,
 (DIR) diff --git a/commands/commands_test.go b/commands/commands_test.go
       @@ -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)
       +        }
       +}
 (DIR) diff --git a/commands/convert.go b/commands/convert.go
       @@ -32,17 +32,17 @@ var (
                _ cmder = (*convertCmd)(nil)
        )
        
       -// TODO(bep) cli refactor
       -var outputDir string
       -var unsafe bool
       -
        type convertCmd struct {
       +        outputDir string
       +        unsafe    bool
       +
                *baseBuilderCmd
        }
        
        func newConvertCmd() *convertCmd {
                cc := &convertCmd{}
        
       +        // TODO(bep) cli refactor this is more than it had
                cc.baseBuilderCmd = newBuilderCmd(&cobra.Command{
                        Use:   "convert",
                        Short: "Convert your content to different formats",
       @@ -82,17 +82,16 @@ to use YAML for the front matter.`,
                        },
                )
        
       -        // TODO(bep) cli refactor
       -        //        cmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "filesystem path to write files to")
       -        //        cmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from")
       -        //        cmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "enable less safe operations, please backup first")
       +        cc.cmd.PersistentFlags().StringVarP(&cc.outputDir, "output", "o", "", "filesystem path to write files to")
       +        cc.cmd.PersistentFlags().StringVarP(&cc.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")
                cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
        
                return cc
        }
        
        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")
                }
        
       @@ -114,17 +113,17 @@ func (cc *convertCmd) convertContents(mark rune) error {
        
                site.Log.FEEDBACK.Println("processing", len(site.AllPages), "content files")
                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 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.
                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
                        }
                }
       @@ -182,8 +181,8 @@ func convertAndSavePage(p *hugolib.Page, site *hugolib.Site, mark rune) error {
                }
        
                newFilename := p.Filename()
       -        if outputDir != "" {
       -                newFilename = filepath.Join(outputDir, p.Dir(), newPage.LogicalName())
       +        if cc.outputDir != "" {
       +                newFilename = filepath.Join(cc.outputDir, p.Dir(), newPage.LogicalName())
                }
        
                if err = newPage.SaveSourceAs(newFilename); err != nil {
 (DIR) diff --git a/commands/hugo.go b/commands/hugo.go
       @@ -141,6 +141,9 @@ Complete documentation is available at http://gohugo.io/.`,
                // Set bash-completion
                _ = cc.cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{})
        
       +        cc.cmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags)
       +        cc.cmd.SilenceUsage = true
       +
                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("noChmod", "", false, "don't sync permission mode of files")
                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.)")
        
       @@ -214,23 +218,11 @@ func Reset() error {
                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.
        func Execute() {
       -        HugoCmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags)
       +        hugoCmd := newHugoCompleteCmd()
        
       -        HugoCmd.SilenceUsage = true
       -
       -        addAllCommands()
       -
       -        if c, err := HugoCmd.ExecuteC(); err != nil {
       +        if c, err := hugoCmd.ExecuteC(); err != nil {
                        if isUserError(err) {
                                c.Println("")
                                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.
       -func addAllCommands() {
       +func addAllCommands(root *cobra.Command) {
                addCommands(
       +                root,
                        newServerCmd(),
                        newVersionCmd(),
                        newEnvCmd(),
       @@ -257,9 +256,9 @@ func addAllCommands() {
                )
        }
        
       -func addCommands(commands ...cmder) {
       +func addCommands(root *cobra.Command, commands ...cmder) {
                for _, command := range commands {
       -                HugoCmd.AddCommand(command.getCommand())
       +                root.AddCommand(command.getCommand())
                }
        }
        
 (DIR) diff --git a/commands/list.go b/commands/list.go
       @@ -151,7 +151,7 @@ expired.`,
                )
        
                // 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{})
        
                return cc
 (DIR) diff --git a/commands/new.go b/commands/new.go
       @@ -30,6 +30,7 @@ import (
        var _ cmder = (*newCmd)(nil)
        
        type newCmd struct {
       +        hugoBuilderCommon
                contentEditor string
                contentType   string
        
       @@ -37,8 +38,8 @@ type newCmd struct {
        }
        
        func newNewCmd() *newCmd {
       -        ccmd := &newCmd{baseCmd: newBaseCmd(nil)}
       -        cmd := &cobra.Command{
       +        cc := &newCmd{}
       +        cc.baseCmd = newBaseCmd(&cobra.Command{
                        Use:   "new [path]",
                        Short: "Create new content for your site",
                        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.`,
        
       -                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
       -        //        cmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from")
       -        cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
       -        cmd.Flags().StringVar(&ccmd.contentEditor, "editor", "", "edit new content with this editor, if provided")
       -
       -        cmd.AddCommand(newNewSiteCmd().getCommand())
       -        cmd.AddCommand(newNewThemeCmd().getCommand())
       +        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.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
        
       -        ccmd.cmd = cmd
       +        cc.cmd.AddCommand(newNewSiteCmd().getCommand())
       +        cc.cmd.AddCommand(newNewThemeCmd().getCommand())
        
       -        return ccmd
       +        return cc
        }
        
        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
                }
        
       -        c, err := initializeConfig(false, nil, n, cfgInit)
       +        c, err := initializeConfig(false, &n.hugoBuilderCommon, n, cfgInit)
        
                if err != nil {
                        return err
 (DIR) diff --git a/commands/new_theme.go b/commands/new_theme.go
       @@ -32,10 +32,11 @@ var _ cmder = (*newThemeCmd)(nil)
        
        type newThemeCmd struct {
                *baseCmd
       +        hugoBuilderCommon
        }
        
        func newNewThemeCmd() *newThemeCmd {
       -        ccmd := &newThemeCmd{newBaseCmd(nil)}
       +        ccmd := &newThemeCmd{baseCmd: newBaseCmd(nil)}
        
                cmd := &cobra.Command{
                        Use:   "theme [name]",
       @@ -53,7 +54,7 @@ as you see fit.`,
        }
        
        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 {
                        return err
 (DIR) diff --git a/commands/server.go b/commands/server.go
       @@ -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");
        // you may not use this file except in compliance with the License.
       @@ -56,24 +56,6 @@ type serverCmd struct {
                *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 {
                cc := &serverCmd{}
        
       @@ -96,6 +78,21 @@ of a second, you will be able to save and see your changes nearly instantly.`,
                        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
        }
        
 (DIR) diff --git a/main.go b/main.go
       @@ -23,6 +23,7 @@ import (
        )
        
        func main() {
       +
                runtime.GOMAXPROCS(runtime.NumCPU())
                commands.Execute()