create: Refactor NewContent to be testable
authorCameron Moore <moorereason@gmail.com>
Thu, 17 Mar 2016 18:30:33 +0000 (13:30 -0500)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 20 Mar 2016 22:51:17 +0000 (23:51 +0100)
NewContent is refactored to use the afero.Fs interface that should allow
full testing.  This commit also pulls the metadata creation logic out of
NewContent and into a separate function to decrease the cyclomatic
complexity of NewContent.

commands/new.go
create/content.go
create/content_test.go [new file with mode: 0644]

index 170952a05ca258d1f190628338738c81ce9622bb..4fc0003191cbce42d7e817668c751ab5c9c3b7ad 100644 (file)
@@ -110,8 +110,7 @@ func NewContent(cmd *cobra.Command, args []string) error {
                kind = contentType
        }
 
-       return create.NewContent(kind, createpath)
-
+       return create.NewContent(hugofs.SourceFs, kind, createpath)
 }
 
 func doNewSite(basepath string, force bool) error {
index 6d38bbde1c7dfceaffc9e074e84f30f3ecb5fc84..b97744484e7f7af88cdaf31fea5360e42d7ec5c6 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright 2015 The Hugo Authors. All rights reserved.
+// Copyright 2016 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.
@@ -15,7 +15,6 @@ package create
 
 import (
        "bytes"
-       "io/ioutil"
        "os"
        "os/exec"
        "path"
@@ -23,24 +22,26 @@ import (
        "strings"
        "time"
 
+       "github.com/spf13/afero"
        "github.com/spf13/cast"
        "github.com/spf13/hugo/helpers"
-       "github.com/spf13/hugo/hugofs"
        "github.com/spf13/hugo/hugolib"
        "github.com/spf13/hugo/parser"
        jww "github.com/spf13/jwalterweatherman"
        "github.com/spf13/viper"
 )
 
-func NewContent(kind, name string) (err error) {
+// NewContent creates a new content file in the content directory based upon the
+// given kind, which is used to lookup an archetype.
+func NewContent(fs afero.Fs, kind, name string) (err error) {
        jww.INFO.Println("attempting to create ", name, "of", kind)
 
-       location := FindArchetype(kind)
+       location := FindArchetype(fs, kind)
 
        var by []byte
 
        if location != "" {
-               by, err = ioutil.ReadFile(location)
+               by, err = afero.ReadFile(fs, location)
                if err != nil {
                        jww.ERROR.Println(err)
                }
@@ -53,22 +54,64 @@ func NewContent(kind, name string) (err error) {
        if err != nil {
                return err
        }
-       metadata, err := psr.Metadata()
+
+       metadata, err := createMetadata(psr, name)
        if err != nil {
+               jww.ERROR.Printf("Error processing archetype file %s: %s\n", location, err)
                return err
        }
-       newmetadata, err := cast.ToStringMapE(metadata)
+
+       page, err := hugolib.NewPage(name)
        if err != nil {
-               jww.ERROR.Println("Error processing archetype file:", location)
                return err
        }
 
-       for k := range newmetadata {
+       if err = page.SetSourceMetaData(metadata, parser.FormatToLeadRune(viper.GetString("MetaDataFormat"))); err != nil {
+               return
+       }
+
+       page.SetSourceContent(psr.Content())
+
+       if err = page.SafeSaveSourceAs(filepath.Join(viper.GetString("contentDir"), name)); err != nil {
+               return
+       }
+       jww.FEEDBACK.Println(helpers.AbsPathify(filepath.Join(viper.GetString("contentDir"), name)), "created")
+
+       editor := viper.GetString("NewContentEditor")
+
+       if editor != "" {
+               jww.FEEDBACK.Printf("Editing %s with %q ...\n", name, editor)
+
+               cmd := exec.Command(editor, helpers.AbsPathify(path.Join(viper.GetString("contentDir"), name)))
+               cmd.Stdin = os.Stdin
+               cmd.Stdout = os.Stdout
+               cmd.Stderr = os.Stderr
+
+               return cmd.Run()
+       }
+
+       return nil
+}
+
+// createMetadata generates Metadata for a new page based upon the metadata
+// found in an archetype.
+func createMetadata(archetype parser.Page, name string) (map[string]interface{}, error) {
+       archMetadata, err := archetype.Metadata()
+       if err != nil {
+               return nil, err
+       }
+
+       metadata, err := cast.ToStringMapE(archMetadata)
+       if err != nil {
+               return nil, err
+       }
+
+       for k := range metadata {
                switch strings.ToLower(k) {
                case "date":
-                       newmetadata[k] = time.Now()
+                       metadata[k] = time.Now()
                case "title":
-                       newmetadata[k] = helpers.MakeTitle(helpers.Filename(name))
+                       metadata[k] = helpers.MakeTitle(helpers.Filename(name))
                }
        }
 
@@ -81,60 +124,35 @@ func NewContent(kind, name string) (err error) {
                return false
        }
 
-       if newmetadata == nil {
-               newmetadata = make(map[string]interface{})
+       if metadata == nil {
+               metadata = make(map[string]interface{})
        }
 
-       if !caseimatch(newmetadata, "date") {
-               newmetadata["date"] = time.Now()
+       if !caseimatch(metadata, "date") {
+               metadata["date"] = time.Now()
        }
 
-       if !caseimatch(newmetadata, "title") {
-               newmetadata["title"] = helpers.MakeTitle(helpers.Filename(name))
-       }
-
-       page, err := hugolib.NewPage(name)
-       if err != nil {
-               return err
+       if !caseimatch(metadata, "title") {
+               metadata["title"] = helpers.MakeTitle(helpers.Filename(name))
        }
 
        if x := parser.FormatSanitize(viper.GetString("MetaDataFormat")); x == "json" || x == "yaml" || x == "toml" {
-               newmetadata["date"] = time.Now().Format(time.RFC3339)
+               metadata["date"] = time.Now().Format(time.RFC3339)
        }
 
-       //page.Dir = viper.GetString("sourceDir")
-       page.SetSourceMetaData(newmetadata, parser.FormatToLeadRune(viper.GetString("MetaDataFormat")))
-       page.SetSourceContent(psr.Content())
-       if err = page.SafeSaveSourceAs(filepath.Join(viper.GetString("contentDir"), name)); err != nil {
-               return
-       }
-       jww.FEEDBACK.Println(helpers.AbsPathify(filepath.Join(viper.GetString("contentDir"), name)), "created")
-
-       editor := viper.GetString("NewContentEditor")
-
-       if editor != "" {
-               jww.FEEDBACK.Printf("Editing %s with %q ...\n", name, editor)
-
-               cmd := exec.Command(editor, helpers.AbsPathify(path.Join(viper.GetString("contentDir"), name)))
-               cmd.Stdin = os.Stdin
-               cmd.Stdout = os.Stdout
-               cmd.Stderr = os.Stderr
-
-               if err = cmd.Run(); err != nil {
-                       return
-               }
-       }
-
-       return nil
+       return metadata, nil
 }
 
-func FindArchetype(kind string) (outpath string) {
+// FindArchetype takes a given kind/archetype of content and returns an output
+// path for that archetype.  If no archetype is found, an empty string is
+// returned.
+func FindArchetype(fs afero.Fs, kind string) (outpath string) {
        search := []string{helpers.AbsPathify(viper.GetString("archetypeDir"))}
 
        if viper.GetString("theme") != "" {
                themeDir := filepath.Join(helpers.AbsPathify(viper.GetString("themesDir")+"/"+viper.GetString("theme")), "/archetypes/")
-               if _, err := os.Stat(themeDir); os.IsNotExist(err) {
-                       jww.ERROR.Println("Unable to find archetypes directory for theme :", viper.GetString("theme"), "in", themeDir)
+               if _, err := fs.Stat(themeDir); os.IsNotExist(err) {
+                       jww.ERROR.Printf("Unable to find archetypes directory for theme %q at %q", viper.GetString("theme"), themeDir)
                } else {
                        search = append(search, themeDir)
                }
@@ -154,7 +172,7 @@ func FindArchetype(kind string) (outpath string) {
                for _, p := range pathsToCheck {
                        curpath := filepath.Join(x, p)
                        jww.DEBUG.Println("checking", curpath, "for archetypes")
-                       if exists, _ := helpers.Exists(curpath, hugofs.SourceFs); exists {
+                       if exists, _ := helpers.Exists(curpath, fs); exists {
                                jww.INFO.Println("curpath: " + curpath)
                                return curpath
                        }
diff --git a/create/content_test.go b/create/content_test.go
new file mode 100644 (file)
index 0000000..f0b3de1
--- /dev/null
@@ -0,0 +1,125 @@
+// Copyright 2016 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 create_test
+
+import (
+       "os"
+       "path/filepath"
+       "testing"
+
+       "github.com/spf13/afero"
+       "github.com/spf13/hugo/create"
+       "github.com/spf13/hugo/hugofs"
+       "github.com/spf13/viper"
+)
+
+func TestNewContent(t *testing.T) {
+       initViper()
+
+       err := initFs()
+       if err != nil {
+               t.Fatalf("initialization error: %s", err)
+       }
+
+       cases := []struct {
+               kind          string
+               path          string
+               resultStrings []string
+       }{
+               {"post", "post/sample-1.md", []string{`title = "sample 1"`, `test = "test1"`}},
+               {"stump", "stump/sample-2.md", []string{`title = "sample 2"`}},     // no archetype file
+               {"", "sample-3.md", []string{`title = "sample 3"`}},                // no archetype
+               {"product", "product/sample-4.md", []string{`title = "sample 4"`}}, // empty archetype front matter
+       }
+
+       for i, c := range cases {
+               err = create.NewContent(hugofs.SourceFs, c.kind, c.path)
+               if err != nil {
+                       t.Errorf("[%d] NewContent: %s", i, err)
+               }
+
+               fname := filepath.Join(os.TempDir(), "content", filepath.FromSlash(c.path))
+               _, err = hugofs.SourceFs.Stat(fname)
+               if err != nil {
+                       t.Errorf("[%d] Stat: %s", i, err)
+               }
+
+               for _, v := range c.resultStrings {
+                       found, err := afero.FileContainsBytes(hugofs.SourceFs, fname, []byte(v))
+                       if err != nil {
+                               t.Errorf("[%d] FileContainsBytes: %s", i, err)
+                       }
+                       if !found {
+                               t.Errorf("content missing from output: %q", v)
+                       }
+               }
+       }
+}
+
+func initViper() {
+       viper.Reset()
+       viper.Set("MetaDataFormat", "toml")
+       viper.Set("archetypeDir", filepath.Join(os.TempDir(), "archetypes"))
+       viper.Set("contentDir", filepath.Join(os.TempDir(), "content"))
+       viper.Set("themesDir", filepath.Join(os.TempDir(), "themes"))
+       viper.Set("theme", "sample")
+}
+
+func initFs() error {
+       hugofs.SourceFs = new(afero.MemMapFs)
+       perm := os.FileMode(0755)
+       var err error
+
+       // create directories
+       dirs := []string{
+               "archetypes",
+               "content",
+               filepath.Join("themes", "sample", "archetypes"),
+       }
+       for _, dir := range dirs {
+               dir = filepath.Join(os.TempDir(), dir)
+               err = hugofs.SourceFs.Mkdir(dir, perm)
+               if err != nil {
+                       return err
+               }
+       }
+
+       // create files
+       for _, v := range []struct {
+               path    string
+               content string
+       }{
+               {
+                       path:    filepath.Join(os.TempDir(), "archetypes", "post.md"),
+                       content: "+++\ndate = \"2015-01-12T19:20:04-07:00\"\ntitle = \"post arch\"\ntest = \"test1\"\n+++\n",
+               },
+               {
+                       path:    filepath.Join(os.TempDir(), "archetypes", "product.md"),
+                       content: "+++\n+++\n",
+               },
+       } {
+               f, err := hugofs.SourceFs.Create(v.path)
+               if err != nil {
+                       return err
+               }
+               defer f.Close()
+
+               _, err = f.Write([]byte(v.content))
+               if err != nil {
+                       return err
+               }
+       }
+
+       return nil
+}