diff --git a/README.md b/README.md index 289c89e..aa1aed6 100644 --- a/README.md +++ b/README.md @@ -70,15 +70,15 @@ Run tmplnator like so: `t2 -template-dir /templates` And that's it! -*NOTE*: Templates without a described `dir` will use `default-dir` as their output directory. +**NOTE**: Templates without a described `dir` will use `default-dir` as their output directory. ## Template Functions Access environment variables in the template with `.Env` like so `.Env.VARIABLE` -Access etcd values with `.Var ` if key not found will look in ENV +Access etcd values with `.Get ` if key not found will look in ENV -`dir "/path/to/destination/dir" `: Describe destination directory. Accepts printf style formatting in path string. *NOTE*: Templates without a described `dir` will use `default-dir` as their output directory. +`dir "/path/to/destination/dir" `: Describe destination directory. Accepts printf style formatting in path string. **NOTE**: Templates without a described `dir` will use `default-dir` as their output directory. `name "name" `: Describe name of generated file. Accepts printf style formatting of name string. @@ -90,6 +90,8 @@ Access etcd values with `.Var ` if key not found will look in ENV `group `: Describe gid for generated file +`file_info`: Returns a file.Info object for the current file + `to_json `: Marshal JSON string `from_json `: Unmarshal JSON string diff --git a/config/version.go b/config/version.go index 2f10f4e..246a536 100644 --- a/config/version.go +++ b/config/version.go @@ -1,5 +1,5 @@ package config const ( - CodeVersion = "v0.1.1" + CodeVersion = "v1.0.0" ) diff --git a/file/file.go b/file/file.go new file mode 100644 index 0000000..e853d7b --- /dev/null +++ b/file/file.go @@ -0,0 +1,52 @@ +package file + +import ( + "bytes" + "os" + "text/template" +) + +// Testing is set to true for running file tests +var Testing bool + +// File describes a tmplnator template file +type File interface { + Write(*bytes.Buffer, interface{}) error + Read() ([]byte, error) + Template(*template.Template) + Destination() string + Info() Info + Output() string + DeleteTemplate() error + setDir(string, ...interface{}) string + setName(string, ...interface{}) string + setUser(int) string + setGroup(int) string + setMode(os.FileMode) string + setDirMode(os.FileMode) string + setSkip() string +} + +// Info objects have all the info for objects that implement File. +type Info struct { + Src string + Name string + Dir string + User int + Group int + Mode os.FileMode + Dirmode os.FileMode +} + +// NewFile returns a File object. If Testing is true underlying struct is +// a mockFile, otherwise it is a templateFile +func NewFile(path string, defaultDir string) File { + if Testing { + return newMockFile(path, defaultDir) + } + return newTemplateFile(path, defaultDir) +} + +func init() { + Testing = false +} diff --git a/file/file_test.go b/file/file_test.go new file mode 100644 index 0000000..a688b60 --- /dev/null +++ b/file/file_test.go @@ -0,0 +1,96 @@ +package file + +import ( + // "io/ioutil" + "bytes" + "os" + "testing" +) + +var filetest = []struct { + name string + template string + expectedOutput string + expectedInfo Info + expectError bool + stackSize int +}{ + { + name: "bad", + template: `{{ dir "/some/other/path" }{{ mode 0755 "one too many" }}Body Text {{ env "BAD" Something }}`, + expectedOutput: "", + expectedInfo: Info{}, + expectError: true, + stackSize: 0, + }, + { + name: "change_everything", + template: `{{ dir "/some/path" }}{{ name "name_changed" }}{{ mode 0777 }}{{ user 10000 }}Body Text`, + expectedOutput: "Body Text", + expectedInfo: Info{ + Name: "name_changed", + Dir: "/some/path", + Mode: os.FileMode(0777), + User: 10000, + }, + expectError: false, + stackSize: 1, + }, +} + +func TestParseFile(t *testing.T) { + Testing = true + for _, ft := range filetest { + fq := NewFileQueue() + mf := NewFile(ft.template, ft.name) + err := ParseFile(mf, fq) + fq.PopulateQueue() + + if !ft.expectError && err != nil { + t.Errorf("ParseFile(%q): Expected no error while parsing, got: %v", ft.name, err) + } + if ft.expectError && err == nil { + t.Errorf("ParseFile(%q): Expected an error while parsing", ft.name) + } + if fq.Len() != ft.stackSize { + t.Errorf("ParseFile(%q): Expected stack size to be %d, got %d", ft.name, ft.stackSize, fq.Len()) + } + } +} + +func TestWriteFile(t *testing.T) { + Testing = true + for _, ft := range filetest { + fq := NewFileQueue() + mf := NewFile(ft.template, ft.name) + err := ParseFile(mf, fq) + if err != nil { + if !ft.expectError { + t.Errorf("WriteFile(%q): Parsing failed, please fix it.", ft.name) + } + } else { + err = mf.Write(new(bytes.Buffer), nil) + if err != nil { + t.Errorf("WriteFile(%q): Did not expect error in write: %v", ft.name, err) + } + + out, info := mf.Output(), mf.Info() + if out != ft.expectedOutput { + t.Errorf("WriteFile(%q): Expected output=%q, got output=%q", ft.name, ft.expectedOutput, out) + } + + if info.Name != ft.expectedInfo.Name { + t.Errorf("WriteFile(%q): Expected filename=%q, got filename=%q", ft.name, ft.expectedInfo.Name, info.Name) + } + if info.Dir != ft.expectedInfo.Dir { + t.Errorf("WriteFile(%q): Expected dir=%q, got dir=%q", ft.name, ft.expectedInfo.Dir, info.Dir) + } + if info.Mode != ft.expectedInfo.Mode { + t.Errorf("WriteFile(%q): Expected mode=%q, got mode=%q", ft.name, ft.expectedInfo.Mode, info.Mode) + } + if info.User != ft.expectedInfo.User { + t.Errorf("WriteFile(%q): Expected user=%d, got user=%d", ft.name, ft.expectedInfo.User, info.User) + } + } + } +} diff --git a/file/mock_file.go b/file/mock_file.go new file mode 100644 index 0000000..2b49b65 --- /dev/null +++ b/file/mock_file.go @@ -0,0 +1,109 @@ +package file + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + tmpl "text/template" +) + +type mockFile struct { + dir string + dirmode os.FileMode + example string + group int + mode os.FileMode + name string + output string + template *tmpl.Template + user int +} + +func (mf *mockFile) Write(b *bytes.Buffer, data interface{}) (err error) { + err = mf.template.Execute(b, data) + mf.output = b.String() + return +} + +func (mf *mockFile) Read() ([]byte, error) { + return []byte(mf.example), nil +} + +func (mf *mockFile) Template(t *tmpl.Template) { + mf.template = t +} + +func (mf *mockFile) Destination() string { + return filepath.Join(mf.dir, mf.name) +} + +func (mf *mockFile) Info() Info { + return Info{ + Name: mf.name, + Dir: mf.dir, + User: mf.user, + Group: mf.group, + Mode: mf.mode, + Dirmode: mf.dirmode, + } +} + +func (mf *mockFile) Output() string { + return mf.output +} + +func (mf *mockFile) DeleteTemplate() error { + return nil +} + +func (mf *mockFile) setDir(d string, args ...interface{}) string { + for i, a := range args { + if a == nil { + args[i] = "" + } + } + mf.dir = fmt.Sprintf(d, args...) + return "" +} + +func (mf *mockFile) setName(n string, args ...interface{}) string { + for i, a := range args { + if a == nil { + args[i] = "" + } + } + mf.name = fmt.Sprintf(n, args...) + return "" +} + +func (mf *mockFile) setUser(uid int) string { + mf.user = uid + return "" +} + +func (mf *mockFile) setGroup(gid int) string { + mf.group = gid + return "" +} + +func (mf *mockFile) setMode(fm os.FileMode) string { + mf.mode = fm + return "" +} + +func (mf *mockFile) setDirMode(dm os.FileMode) string { + mf.dirmode = dm + return "" +} + +func (mf *mockFile) setSkip() string { + return "" +} + +func newMockFile(e string, n string) File { + return &mockFile{ + example: e, + name: n, + } +} diff --git a/file/parse.go b/file/parse.go new file mode 100644 index 0000000..2483860 --- /dev/null +++ b/file/parse.go @@ -0,0 +1,53 @@ +package file + +import ( + l "github.com/Sirupsen/logrus" + "os" + "path/filepath" + "text/template" +) + +// ParseFiles will recursively parse all the files under dir, returning +// a Queue object with all the files loaded in. +func ParseFiles(dir string, def string) (fq *Queue, err error) { + l.WithField("directory", dir).Info("Parsing files") + fq = NewFileQueue() + err = filepath.Walk(dir, walkfunc(def, fq)) + fq.PopulateQueue() + return +} + +func walkfunc(def string, fq *Queue) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + ext := filepath.Ext(path) + if info.Mode().IsRegular() && ext != ".skip" && ext != ".ignore" { + f := NewFile(path, def) + return ParseFile(f, fq) + } + l.WithField("path", path).Debug("Skipping") + return nil + } +} + +// ParseFile will parse an individual file and put it in the +// Queue +func ParseFile(f File, fq *Queue) (err error) { + l.WithField("path", f.Info().Src).Debug("Parsing file") + + contents, err := f.Read() + if err != nil { + return + } + + t, err := newTemplate(f).Parse(string(contents)) + if err != nil { + return + } + f.Template(t) + fq.add(f) + return +} + +func newTemplate(f File) *template.Template { + return template.New(f.Info().Src).Funcs(newFuncMap(f)) +} diff --git a/file/queue.go b/file/queue.go new file mode 100644 index 0000000..1b7c32b --- /dev/null +++ b/file/queue.go @@ -0,0 +1,44 @@ +package file + +import l "github.com/Sirupsen/logrus" + +// Queue describes a queue of files ofr the generator workers +type Queue struct { + files []File + queue chan File +} + +// NewFileQueue returns an initialized file.Queue +func NewFileQueue() *Queue { + return &Queue{files: []File{}} +} + +func (fq *Queue) add(f File) { + l.WithField("file", f).Debug("Adding file to queue") + fq.files = append(fq.files, f) + l.WithField("file", f).Debug("File added") +} + +// PopulateQueue feeds parsed files into the underlying channel +func (fq *Queue) PopulateQueue() { + fq.queue = make(chan File, len(fq.files)) + for _, f := range fq.files { + fq.queue <- f + } + close(fq.queue) +} + +// Queue returns the File channel +func (fq *Queue) Queue() chan File { + return fq.queue +} + +// Len returns the length of the queue +func (fq *Queue) Len() int { + return len(fq.queue) +} + +// Files returns the file slice +func (f *Queue) Files() []File { + return f.files +} diff --git a/file/template_file.go b/file/template_file.go new file mode 100644 index 0000000..6435885 --- /dev/null +++ b/file/template_file.go @@ -0,0 +1,165 @@ +package file + +import ( + "bytes" + "fmt" + l "github.com/Sirupsen/logrus" + "io/ioutil" + "os" + "path/filepath" + tmpl "text/template" +) + +type templateFile struct { + template *tmpl.Template + src string + name string + dir string + user int + group int + mode os.FileMode + dirmode os.FileMode + skip bool + bytesWritten int +} + +func (tf *templateFile) Write(b *bytes.Buffer, data interface{}) (err error) { + l.WithFields(l.Fields{ + "template": tf.src, + "data": data, + }).Debug("Executing template") + err = tf.template.Execute(b, data) + if err != nil { + return err + } + + if tf.skip { + return nil + } + + l.WithField("path", tf.dir).Debug("Creating directory") + if _, err := os.Stat(tf.dir); err != nil { + if err = os.MkdirAll(tf.dir, tf.dirmode); err != nil { + return err + } + } + + l.WithField("path", tf.Destination()).Debug("Creating file") + fh, err := os.OpenFile(tf.Destination(), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, tf.mode) + if err != nil { + return err + } + defer fh.Close() + defer os.Chown(tf.Destination(), tf.user, tf.group) + + l.WithFields(l.Fields{ + "template": tf.src, + "file": tf.Destination(), + }).Info("Generating file") + n, err := fh.Write(b.Bytes()) + tf.bytesWritten = n + if err != nil { + return err + } + return nil +} + +func (tf *templateFile) Read() (b []byte, err error) { + b, err = ioutil.ReadFile(tf.src) + return +} + +func (tf *templateFile) Info() Info { + return Info{ + Src: tf.src, + Name: tf.name, + Dir: tf.dir, + User: tf.user, + Group: tf.group, + Mode: tf.mode, + Dirmode: tf.dirmode, + } +} + +func (tf *templateFile) Src() string { + return tf.src +} + +func (tf *templateFile) Name() string { + return tf.name +} + +func (tf *templateFile) Output() string { + return fmt.Sprintf("%d", tf.bytesWritten) +} + +func (tf *templateFile) Destination() string { + return filepath.Join(tf.dir, tf.name) +} + +func (tf *templateFile) DeleteTemplate() (err error) { + err = os.Remove(tf.src) + return +} + +func (tf *templateFile) Template(t *tmpl.Template) { + tf.template = t +} + +func (tf *templateFile) setDir(dir string, args ...interface{}) string { + for i, a := range args { + if a == nil { + args[i] = "" + } + } + tf.dir = fmt.Sprintf(dir, args...) + return "" +} + +func (tf *templateFile) setName(name string, args ...interface{}) string { + for i, a := range args { + if a == nil { + args[i] = "" + } + } + tf.name = fmt.Sprintf(name, args...) + return "" +} + +func (tf *templateFile) setUser(uid int) string { + tf.user = uid + return "" +} + +func (tf *templateFile) setGroup(gid int) string { + tf.group = gid + return "" +} + +func (tf *templateFile) setMode(m os.FileMode) string { + tf.mode = m + return "" +} + +func (tf *templateFile) setDirMode(dm os.FileMode) string { + tf.dirmode = dm + return "" +} + +func (tf *templateFile) setSkip() string { + tf.skip = true + return "" +} + +func newTemplateFile(path string, defualtDir string) File { + return &templateFile{ + src: path, + name: filepath.Base(path), + dir: defualtDir, + mode: os.FileMode(0644), + dirmode: os.FileMode(0755), + user: os.Geteuid(), + group: os.Getegid(), + skip: false, + } +} diff --git a/generator/template_func_test.go b/file/template_func_test.go similarity index 99% rename from generator/template_func_test.go rename to file/template_func_test.go index d49f4f8..e084abc 100644 --- a/generator/template_func_test.go +++ b/file/template_func_test.go @@ -1,4 +1,4 @@ -package generator +package file import ( "os" diff --git a/generator/template_funcs.go b/file/template_funcs.go similarity index 96% rename from generator/template_funcs.go rename to file/template_funcs.go index 0b5601a..b97b197 100644 --- a/generator/template_funcs.go +++ b/file/template_funcs.go @@ -1,4 +1,4 @@ -package generator +package file import ( "bytes" @@ -13,7 +13,7 @@ import ( "time" ) -func newFuncMap(f *file) map[string]interface{} { +func newFuncMap(f File) map[string]interface{} { return map[string]interface{}{ "dir": f.setDir, "name": f.setName, @@ -23,7 +23,7 @@ func newFuncMap(f *file) map[string]interface{} { "group": f.setGroup, "skip": f.setSkip, "env": os.Getenv, - "source": f.Src, + "file_info": f.Info, "timestamp": timestamp, "to_json": marshalJSON, "from_json": UnmarshalJSON, diff --git a/generator/context.go b/generator/context.go index aac2b0e..c411459 100644 --- a/generator/context.go +++ b/generator/context.go @@ -9,11 +9,12 @@ import ( // Context type objects are passed into the template during template.Execute(). type Context struct { + Env map[string]string store backend.Backend } func newContext(be backend.Backend) *Context { - return &Context{be} + return &Context{envMap(), be} } // Get performs a lookup of the given key in the backend. Failing that, @@ -35,3 +36,12 @@ func (c *Context) Get(key string) string { l.WithField("key", key).Debug("Not in backend, looking in ENV") return os.Getenv(strings.ToUpper(strings.Replace(key, "/", "_", -1))) } + +func envMap() map[string]string { + env := make(map[string]string, len(os.Environ())) + for _, val := range os.Environ() { + index := strings.Index(val, "=") + env[val[:index]] = val[index+1:] + } + return env +} diff --git a/generator/file.go b/generator/file.go deleted file mode 100644 index 2b9fa0c..0000000 --- a/generator/file.go +++ /dev/null @@ -1,130 +0,0 @@ -package generator - -import ( - "fmt" - l "github.com/Sirupsen/logrus" - "github.com/albertrdixon/tmplnator/stack" - "io/ioutil" - "os" - "path/filepath" - "text/template" -) - -type file struct { - body *template.Template - src string - name string - dir string - user int - group int - mode os.FileMode - dirmode os.FileMode - skip bool -} - -func (f *file) setDir(dir string, args ...interface{}) string { - for i, a := range args { - if a == nil { - args[i] = "" - } - } - f.dir = fmt.Sprintf(dir, args...) - return "" -} - -func (f *file) setName(name string, args ...interface{}) string { - for i, a := range args { - if a == nil { - args[i] = "" - } - } - f.name = fmt.Sprintf(name, args...) - return "" -} - -func (f *file) setUser(uid int) string { - f.user = uid - return "" -} - -func (f *file) setGroup(gid int) string { - f.group = gid - return "" -} - -func (f *file) setMode(m os.FileMode) string { - f.mode = m - return "" -} - -func (f *file) setDirMode(dm os.FileMode) string { - f.dirmode = dm - return "" -} - -func (f *file) setSkip() string { - f.skip = true - return "" -} - -func (f *file) Src() string { - return f.src -} - -func (f *file) destination() string { - return filepath.Join(f.dir, f.name) -} - -func parseFiles(dir string, def string) (st *stack.Stack, err error) { - l.WithField("directory", dir).Info("Parsing files") - st = stack.NewStack() - err = filepath.Walk(dir, walkfunc(def, st)) - return -} - -func walkfunc(def string, st *stack.Stack) filepath.WalkFunc { - return func(path string, info os.FileInfo, err error) error { - ext := filepath.Ext(path) - if info.Mode().IsRegular() && ext != ".skip" && ext != ".ignore" { - return parseFile(path, def, st) - } else { - l.WithField("path", path).Debug("Skipping") - } - return nil - } -} - -func parseFile(path string, def string, st *stack.Stack) (err error) { - l.WithField("path", path).Debug("Parsing file") - - f := newFile(path, def, filepath.Base(path)) - contents, err := ioutil.ReadFile(path) - if err != nil { - return - } - - t, err := newTemplate(path, f).Parse(string(contents)) - if err != nil { - return - } - f.body = t - st.Push(f) - return -} - -func newFile(path string, def string, name string) *file { - return &file{ - src: path, - name: name, - dir: def, - mode: os.FileMode(0644), - dirmode: os.FileMode(0755), - user: os.Geteuid(), - group: os.Getegid(), - skip: false, - } -} - -func newTemplate(path string, f *file) *template.Template { - return template.New(path).Funcs(newFuncMap(f)) -} diff --git a/generator/file_test.go b/generator/file_test.go deleted file mode 100644 index 91a9a5b..0000000 --- a/generator/file_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package generator - -import ( - "github.com/albertrdixon/tmplnator/stack" - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -func TestParseFile(t *testing.T) { - var filetest = []struct { - name string - body string - expectError bool - stackSize int - }{ - { - name: "good", - body: `{{ dir "/some/path" }}{{ mode 0777 }} Body Text {{ .Env.VAR }}`, - expectError: false, - stackSize: 1, - }, - { - name: "bad", - body: `{{ dir "/some/other/path" }{{ mode 0755 "one too many" }}Body Text {{ .Env.BAD Something }}`, - expectError: true, - stackSize: 0, - }, - } - - dir, err := ioutil.TempDir("", "tmpltest") - defer os.RemoveAll(dir) - if err != nil { - t.Errorf("ParseFile(): Could not create tmp dir: %v", err) - } - - for _, ft := range filetest { - fp := filepath.Join(dir, ft.name) - fh, err := os.OpenFile(fp, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) - if err != nil { - t.Errorf("ParseFile(): Could not create testFile: %v", err) - } - fh.WriteString(ft.body) - fh.Close() - - st := stack.NewStack() - err = parseFile(fp, "", st) - if !ft.expectError && err != nil { - t.Errorf("ParseFile(%q): Expected no error while parsing, got: %v", ft.name, err) - } - if ft.expectError && err == nil { - t.Errorf("ParseFile(%q): Expected an error while parsing", ft.name) - } - if st.Len() != ft.stackSize { - t.Errorf("ParseFile(%q): Expected stack size to be %d, got %d", ft.name, ft.stackSize, st.Len()) - } - } -} diff --git a/generator/generator.go b/generator/generator.go index 467027c..1ef16fa 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -5,31 +5,31 @@ import ( l "github.com/Sirupsen/logrus" "github.com/albertrdixon/tmplnator/backend" "github.com/albertrdixon/tmplnator/config" - "github.com/albertrdixon/tmplnator/stack" + "github.com/albertrdixon/tmplnator/file" "github.com/oxtoacart/bpool" - "os" "sync" ) type generator struct { - files *stack.Stack + files *file.Queue defaultDir string context *Context bpool *bpool.BufferPool wg *sync.WaitGroup threads int del bool + errors chan error } // NewGenerator returns a generator with a parsed file stack func NewGenerator(c *config.Config) (*generator, error) { - fs, err := parseFiles(c.TmplDir, c.DefaultDir) + fq, err := file.ParseFiles(c.TmplDir, c.DefaultDir) if err != nil { return nil, err } return &generator{ - files: fs, + files: fq, defaultDir: c.DefaultDir, context: newContext(backend.New(c.Namespace, c.EtcdPeers)), bpool: bpool.NewBufferPool(c.BpoolSize), @@ -50,7 +50,7 @@ func (g *generator) Generate() (err error) { } g.wg.Wait() - if l.GetLevel() == l.ErrorLevel { + if l.GetLevel() <= l.ErrorLevel { fmt.Println(g.defaultDir) } return nil @@ -58,72 +58,43 @@ func (g *generator) Generate() (err error) { func (g *generator) process(id int) { l.WithFields(l.Fields{ - "id": id, + "thread_id": id, "file_stack_size": g.files.Len(), }).Debug("Starting processing thread") defer g.wg.Done() + defer g.catch(id) - for g.files.Len() > 0 { - if f, ok := g.files.Pop().(*file); ok { - l.WithFields(l.Fields{ - "id": id, - "template": f.src, - }).Debug("Processing a template") - if err := g.write(f); err == nil { - os.Chown(f.destination(), f.user, f.group) - if g.del { - l.WithField("path", f.src).Info("Removing file") - if err := os.Remove(f.src); err != nil { - l.WithField("error", err).Info("Failed to remove file") - } + for f := range g.files.Queue() { + l.WithFields(l.Fields{ + "thread_id": id, + "template": f.Info().Src, + }).Debug("Processing template") + if err := g.write(f); err == nil { + if g.del { + l.WithField("template", f.Info().Src).Info("Removing template") + if err := f.DeleteTemplate(); err != nil { + l.WithField("error", err).Error("Failed to remove file") } - } else { - l.WithField("error", err).Info("Failed to write file") } } else { - l.WithField("item", f).Panic("Internal Error: Could not cast stack item as *file") + l.WithField("error", err).Fatal("Failed to write file") } } } -func (g *generator) write(f *file) error { +func (g *generator) write(f file.File) (err error) { b := g.bpool.Get() defer g.bpool.Put(b) - l.WithFields(l.Fields{ - "template": f.src, - "context": g.context, - }).Debug("Executing template") - err := f.body.Execute(b, g.context) - if err != nil { - return err - } - - if f.skip { - return nil - } - - l.WithField("path", f.dir).Debug("Creating directory") - if _, err := os.Stat(f.dir); err != nil { - if err = os.MkdirAll(f.dir, f.dirmode); err != nil { - return err - } - } + err = f.Write(b, g.context) + return +} - l.WithField("path", f.destination()).Debug("Creating file") - fh, err := os.OpenFile(f.destination(), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, f.mode) - if err != nil { - return err +func (g *generator) catch(tid int) { + if r := recover(); r != nil { + l.WithFields(l.Fields{ + "thread_id": tid, + "message": r, + }).Fatal("Recovered from panic!") } - defer fh.Close() - - l.WithFields(l.Fields{ - "template": f.src, - "file": f.destination(), - }).Info("Generating file") - _, err = fh.Write(b.Bytes()) - if err != nil { - return err - } - return nil } diff --git a/generator/generator_test.go b/generator/generator_test.go new file mode 100644 index 0000000..edaa490 --- /dev/null +++ b/generator/generator_test.go @@ -0,0 +1,98 @@ +package generator + +import ( + "github.com/albertrdixon/tmplnator/backend" + "github.com/albertrdixon/tmplnator/file" + "github.com/oxtoacart/bpool" + "os" + "sync" + "testing" +) + +var filetest = []struct { + names []string + templates []string + expectedOutput map[string]string + expectError bool + stackSize int +}{ + { + names: []string{"one"}, + templates: []string{`{{ dir "/some/path" }}{{ mode 0777 }}Body Text {{ .Env.TEST_VAR }}`}, + expectedOutput: map[string]string{"one": "Body Text VALUE"}, + expectError: false, + stackSize: 1, + }, + { + names: []string{"first", "second"}, + templates: []string{ + `{{ dir "/some/other/path" }}{{ mode 0755 }}Body Text One {{ .Env.TEST_VAR }}`, + `{{ dir "some/other/path" }}{{ name "2nd" }}{{ mode 0644 }}Body Text Two {{ .Get "foo/bar" }}`, + }, + expectedOutput: map[string]string{ + "first": "Body Text One VALUE", + "2nd": "Body Text Two baz", + }, + expectError: true, + stackSize: 0, + }, +} + +func newTestGenerator(fq *file.Queue, be backend.Backend) *generator { + return &generator{ + files: fq, + defaultDir: "/var/tmp/testing", + context: newContext(be), + bpool: bpool.NewBufferPool(2), + threads: 2, + wg: new(sync.WaitGroup), + del: false, + } +} + +func TestProcess(t *testing.T) { + mb := backend.NewMock( + map[string]string{ + "foo": "bar", + "foo/bar": "baz", + "one": "two", + }, + map[string][]string{ + "foo": []string{"bar", "baz"}, + "foo/baz": []string{"bim", "biff"}, + }, + ) + + for _, ft := range filetest { + file.Testing = true + fq := file.NewFileQueue() + for idx, tm := range ft.templates { + mf := file.NewFile(tm, ft.names[idx]) + err := file.ParseFile(mf, fq) + if err != nil { + t.Errorf("Parsing failed, please fix it! %v", err) + t.FailNow() + } + } + + if !t.Failed() { + g := newTestGenerator(fq, mb) + fq.PopulateQueue() + err := g.Generate() + if err != nil { + t.Errorf("Generate(%q): Should not have produced an error: %v", ft.names, err) + } + + for _, f := range fq.Files() { + fi := f.Info() + if f.Output() != ft.expectedOutput[fi.Name] { + t.Errorf("Generate(%q): Output not expected, Got file=%q out=%q", ft.names, fi.Name, f.Output()) + } + } + } + } +} + +func init() { + os.Setenv("TEST_VAR", "VALUE") +} diff --git a/stack/stack.go b/stack/stack.go deleted file mode 100644 index 8a25805..0000000 --- a/stack/stack.go +++ /dev/null @@ -1,52 +0,0 @@ -package stack - -import ( - "sync" -) - -type Stack struct { - top *element - size int - mutex *sync.Mutex -} - -type element struct { - value interface{} - next *element -} - -// Return the stack's length -func (s *Stack) Len() int { - s.mutex.Lock() - defer s.mutex.Unlock() - return s.size -} - -// Push a new element onto the stack -func (s *Stack) Push(value interface{}) { - s.mutex.Lock() - defer s.mutex.Unlock() - s.top = &element{value, s.top} - s.size++ -} - -// Remove the top element from the stack and return it's value -// If the stack is empty, return nil -func (s *Stack) Pop() (value interface{}) { - s.mutex.Lock() - defer s.mutex.Unlock() - if s.size > 0 { - value, s.top = s.top.value, s.top.next - s.size-- - return - } - return nil -} - -func NewStack() *Stack { - s := new(Stack) - s.mutex = new(sync.Mutex) - s.size = 0 - s.top = nil - return s -} diff --git a/stack/stack_test.go b/stack/stack_test.go deleted file mode 100644 index 4a409ad..0000000 --- a/stack/stack_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package stack - -import ( - "testing" -) - -var stacktests = []struct { - list []interface{} -}{ - {[]interface{}{"one", "two", "three"}}, - {[]interface{}{1, 2, 3}}, -} - -func TestStack(t *testing.T) { - for _, st := range stacktests { - i, s := 0, NewStack() - for _, in := range st.list { - if s.Len() != i { - t.Errorf("stack.Len(): %d, want %d", s.Len(), i) - } - s.Push(in) - i++ - } - for j := 2; j >= 0; j-- { - item := s.Pop() - if st.list[j] != item { - t.Errorf("stack.Pop(): %v, want %v", item, st.list[j]) - } - } - } -}