template / commit
Adding tests and monkey-patching various OS operations.
file.go | 75 ++++++----------------------- file_test.go | 44 +++++++++++++++++ main_test.go | 23 +++++++++ monkey-patch.go | 114 ++++++++++++++++++++++++++++++++++++++++++++ profile.go | 18 ++++--- profile_test.go | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test-common.go | 22 +++++++++ 7 files changed, 371 insertions(+), 70 deletions(-) diff --git a/file.go b/file.go index 90bd5d0..aa65f9b 100644 --- a/file.go +++ b/file.go @@ -2,67 +2,11 @@ package main import ( "fmt" - "io/ioutil" "os" - "os/user" - "path/filepath" "strings" "text/template" ) -func createFileFromTemplateFile( - srcPath string, - destPath string, - name string, - prePatternData PrePatternData, - usr user.User) error { - // standardize paths - srcPath = getFullPath(srcPath, usr) - destPath = getFullPath(destPath, usr) - - // parse template - srcName := filepath.Base(srcPath) // srcName is required because we are sending files to ParseFiles - template, err := template.New(srcName).ParseFiles(srcPath) - if err != nil { - return fmt.Errorf("error parsing source file '%v': %v", srcPath, err) - } - - // create output file - writer, err := os.Create(destPath) - if err != nil { - return fmt.Errorf("error creating destination file '%v': %v", destPath, err) - } - - // write template to output file - filePatternData := createPostPatternData(filepath.Base(destPath), prePatternData) - err = template.Execute(writer, filePatternData) - if err != nil { - return fmt.Errorf("error executing template: %v", err) - } - - return nil -} - -func createTemplateString( - templateText ParameterizedText, - postPatternData PostPatternData) (string, error) { - - // parse template - template, err := template.New("text-template").Parse(string(templateText)) - if err != nil { - return "", fmt.Errorf("error parsing template text: %v", err) - } - - // create output string - sb := new(strings.Builder) - err = template.Execute(sb, postPatternData) - if err != nil { - return "", fmt.Errorf("error creating templated file: %v", err) - } - - return sb.String(), nil -} - func createFile( prePatternData PrePatternData, file File, @@ -71,14 +15,25 @@ func createFile( if useTemplate { postPatternData := createPostPatternData(fullPath, prePatternData) fmt.Printf("Creating file '%v' from template string\n", fullPath) - templateString, err := createTemplateString(file.Template, postPatternData) + + // parse template + template, err := template.New("text-template").Parse(string(file.Template)) if err != nil { - return fmt.Errorf("error creating template: %v", err) + return fmt.Errorf("error parsing template text: %v", err) } - ioutil.WriteFile(fullPath, []byte(templateString), os.ModePerm) + + // create output string + sb := new(strings.Builder) + err = template.Execute(sb, postPatternData) + if err != nil { + return fmt.Errorf("error creating templated file: %v", err) + } + + templateString := sb.String() + monkey.OSWriteFile(fullPath, []byte(templateString), os.ModePerm) } else { fmt.Printf("Creating empty file: %v\n", fullPath) - os.WriteFile(fullPath, []byte{}, os.ModePerm) + monkey.OSWriteFile(fullPath, []byte{}, os.ModePerm) } return nil } diff --git a/file_test.go b/file_test.go new file mode 100644 index 0000000..afda356 --- /dev/null +++ b/file_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateFile(t *testing.T) { + // settings + path := "~/test/out/test.txt" + prePatternData := testPrePatternData() + expectedText := "test-project-2022-04-20-16-20-69-value1" + + // monkey patch + applyTestMonkeyPatch(t, monkeyPatch{ + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { + assert.Equal(t, path, filename) + assert.Equal(t, perm, os.ModePerm) + assert.Equal(t, string(data), expectedText) + return nil + }, + }) + + // test error path + err := createFile(prePatternData, File{ + Template: "{{.ProjectName", + }, path) + assert.NotNil(t, err) + + // test happy path + err = createFile(prePatternData, File{ + Template: "{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}-{{.Extras.key1}}", + }, path) + assert.Nil(t, err) + + // test empty template + expectedText = "" + err = createFile(prePatternData, File{ + Template: "", + }, path) + assert.Nil(t, err) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..2a1ab5a --- /dev/null +++ b/main_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + setup() + code := m.Run() + shutdown() + os.Exit(code) +} + +func setup() { + // setup code +} +func shutdown() { + // shutdown code + + // not really necessary, but why not? + undoMonkeyPatch() +} diff --git a/monkey-patch.go b/monkey-patch.go new file mode 100644 index 0000000..bc67233 --- /dev/null +++ b/monkey-patch.go @@ -0,0 +1,114 @@ +package main + +import ( + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" +) + +type monkeyPatch struct { + ExecCommand func(name string, arg ...string) *exec.Cmd + OSWriteFile func(filename string, data []byte, perm os.FileMode) error + OSGetwd func() (string, error) + OSChdir func(dir string) error + OSMkdir func(path string, perm os.FileMode) error + OSReadDir func(dirname string) ([]os.DirEntry, error) +} +type monkeyPatchCalls struct { + ExecCommandCalls []struct { + name string + arg []string + } + OSWriteFileCalls []struct { + filename string + data []byte + perm os.FileMode + } + OSGetwdCalls []struct{} + OSChdirCalls []struct{ dir string } + OSMkdirCalls []struct { + path string + perm os.FileMode + } + OSReadDirCalls []struct{ dirname string } +} + +var monkey = monkeyPatch{} + +func applyTestMonkeyPatch(t *testing.T, monkeyImpl monkeyPatch) *monkeyPatchCalls { + t.Cleanup(undoMonkeyPatch) + results := monkeyPatchCalls{ + ExecCommandCalls: []struct { + name string + arg []string + }{}, + OSWriteFileCalls: []struct { + filename string + data []byte + perm os.FileMode + }{}, + OSGetwdCalls: []struct{}{}, + OSChdirCalls: []struct{ dir string }{}, + OSMkdirCalls: []struct { + path string + perm os.FileMode + }{}, + OSReadDirCalls: []struct{ dirname string }{}, + } + monkey = monkeyPatch{ + ExecCommand: func(name string, arg ...string) *exec.Cmd { + assert.NotNil(t, monkeyImpl.ExecCommand, "ExecCommand is not implemented") + results.ExecCommandCalls = append(results.ExecCommandCalls, struct { + name string + arg []string + }{name, arg}) + return monkeyImpl.ExecCommand(name, arg...) + }, + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { + assert.NotNil(t, monkeyImpl.OSWriteFile, "OSWriteFile is not implemented") + results.OSWriteFileCalls = append(results.OSWriteFileCalls, struct { + filename string + data []byte + perm os.FileMode + }{filename, data, perm}) + return monkeyImpl.OSWriteFile(filename, data, perm) + }, + OSGetwd: func() (string, error) { + assert.NotNil(t, monkeyImpl.OSGetwd, "OSGetwd is not implemented") + results.OSGetwdCalls = append(results.OSGetwdCalls, struct{}{}) + return monkeyImpl.OSGetwd() + }, + OSChdir: func(dir string) error { + assert.NotNil(t, monkeyImpl.OSChdir, "OSChdir is not implemented") + results.OSChdirCalls = append(results.OSChdirCalls, struct{ dir string }{dir}) + return monkeyImpl.OSChdir(dir) + }, + OSMkdir: func(path string, perm os.FileMode) error { + assert.NotNil(t, monkeyImpl.OSMkdir, "OSMkdir is not implemented") + results.OSMkdirCalls = append(results.OSMkdirCalls, struct { + path string + perm os.FileMode + }{path, perm}) + return monkeyImpl.OSMkdir(path, perm) + }, + OSReadDir: func(dirname string) ([]os.DirEntry, error) { + assert.NotNil(t, monkeyImpl.OSReadDir, "OSReadDir is not implemented") + results.OSReadDirCalls = append(results.OSReadDirCalls, struct{ dirname string }{dirname}) + return monkeyImpl.OSReadDir(dirname) + }, + } + return &results +} + +func undoMonkeyPatch() { + monkey = monkeyPatch{ + ExecCommand: exec.Command, + OSWriteFile: os.WriteFile, + OSGetwd: os.Getwd, + OSChdir: os.Chdir, + OSMkdir: os.Mkdir, + OSReadDir: os.ReadDir, + } +} diff --git a/profile.go b/profile.go index 6dd1a56..00dc2c3 100644 --- a/profile.go +++ b/profile.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "os" - "os/exec" "os/user" "path/filepath" "runtime" @@ -93,14 +92,14 @@ func processProfile(profile Profile, projectName string, extras map[string]strin return nil } - cwd, err := os.Getwd() + cwd, err := monkey.OSGetwd() if err != nil { return []error{fmt.Errorf("unable to get current working directory: %v", err)} } if profile.Path != "" { // change cwd to the profile's path, if one is specified path := getFullPath(profile.Path, usr) - err = os.Chdir(path) + err = monkey.OSChdir(path) if err != nil { return []error{fmt.Errorf("unable to change directory (1) to %v: %v", path, err)} } @@ -108,7 +107,7 @@ func processProfile(profile Profile, projectName string, extras map[string]strin } // get all entities in current directory - files, err := os.ReadDir(cwd) + files, err := monkey.OSReadDir(cwd) if err != nil { return []error{err} } @@ -147,7 +146,7 @@ func processProfile(profile Profile, projectName string, extras map[string]strin fullPath := "" if err == nil && newName != "" { - fullPath, err = createEntity(now, profile, profileType, cwd, newName, projectName, extras, prePatternData) + fullPath, err = createEntity(now, profile, cwd, newName, projectName, extras, prePatternData) if err != nil { errors = append(errors, fmt.Errorf("unable to create entity: %v", err)) if !profile.RunPostscriptOnFailure { @@ -167,7 +166,7 @@ func processProfile(profile Profile, projectName string, extras map[string]strin allArgs := []string{"-c", postScript} shell := profile.ScriptShell fmt.Printf("Executing post script: %v %v\n", shell, strings.Join(allArgs, " ")) - cmd := exec.Command(shell, allArgs...) + cmd := monkey.ExecCommand(shell, allArgs...) if err := cmd.Run(); err != nil { // post scripts tend to fail (TODO: investigate why), so for now we'll just log the error @@ -182,22 +181,21 @@ func processProfile(profile Profile, projectName string, extras map[string]strin func createEntity( now time.Time, profile Profile, - profileType ProfileType, cwd string, newName string, projectName string, extras map[string]string, prePatternData PrePatternData) (string, error) { - // create new entity + profileType := profile.Type fullPath := filepath.Join(cwd, newName) if profileType == FolderType { fmt.Printf("Creating directory: %v\n", fullPath) - err := os.Mkdir(fullPath, os.ModePerm) + err := monkey.OSMkdir(fullPath, os.ModePerm) if err != nil { return "", fmt.Errorf("error creating directory: %v", err) } - err = os.Chdir(fullPath) + err = monkey.OSChdir(fullPath) if err != nil { return "", fmt.Errorf("unable to change directory (2) to %v: %v", fullPath, err) } diff --git a/profile_test.go b/profile_test.go index 17f726a..a68d627 100644 --- a/profile_test.go +++ b/profile_test.go @@ -1,7 +1,14 @@ package main import ( + "os" + "os/exec" + "os/user" + "path/filepath" + "runtime" "testing" + + "github.com/stretchr/testify/assert" ) func TestValidateProfile(t *testing.T) { @@ -126,3 +133,141 @@ func TestFindProfile(t *testing.T) { testFailure("") testFailure("non-default") } + +func TestCreateEntity(t *testing.T) { + // setup + now := testNow() + cwd := "~/test/dir" + projectName := "project-name" + extras := map[string]string{ + "foo": "bar", + } + patternData := testPrePatternData() + + testFolder := func() { + monkeyPatchCalls := applyTestMonkeyPatch(t, monkeyPatch{ + OSMkdir: func(path string, perm os.FileMode) error { return nil }, + OSChdir: func(dir string) error { return nil }, + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { return nil }, + }) + + profile := Profile{ + Type: FolderType, + Files: []FileInFolder{ + { + Name: "readme.md", + File: File{ + Template: "blah", + }, + }, + { + Name: "test.txt", + File: File{ + Template: "foo", + }, + }, + }, + } + + newName := "new-name" + expectedFullPath := filepath.Clean("~/test/dir/" + newName) + fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData) + assert.Nil(t, err) + assert.Equal(t, expectedFullPath, fullPath) + assert.Equal(t, 1, len(monkeyPatchCalls.OSChdirCalls)) + assert.Equal(t, expectedFullPath, monkeyPatchCalls.OSChdirCalls[0].dir) + assert.Equal(t, 1, len(monkeyPatchCalls.OSMkdirCalls)) + assert.Equal(t, expectedFullPath, monkeyPatchCalls.OSMkdirCalls[0].path) + assert.Equal(t, 2, len(monkeyPatchCalls.OSWriteFileCalls)) + assert.Equal(t, filepath.Clean(expectedFullPath+"/readme.md"), monkeyPatchCalls.OSWriteFileCalls[0].filename) + assert.Equal(t, []byte("blah"), monkeyPatchCalls.OSWriteFileCalls[0].data) + assert.Equal(t, filepath.Clean(expectedFullPath+"/test.txt"), monkeyPatchCalls.OSWriteFileCalls[1].filename) + assert.Equal(t, []byte("foo"), monkeyPatchCalls.OSWriteFileCalls[1].data) + } + + testFile := func() { + monkeyPatchCalls := applyTestMonkeyPatch(t, monkeyPatch{ + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { return nil }, + }) + + profile := Profile{ + Type: FileType, + File: File{ + Template: "blah", + }, + } + newName := "new-name.md" + expectedFullPath := filepath.Clean("~/test/dir/" + newName) + fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData) + assert.Nil(t, err) + assert.Equal(t, expectedFullPath, fullPath) + assert.Equal(t, 1, len(monkeyPatchCalls.OSWriteFileCalls)) + assert.Equal(t, expectedFullPath, monkeyPatchCalls.OSWriteFileCalls[0].filename) + assert.Equal(t, []byte("blah"), monkeyPatchCalls.OSWriteFileCalls[0].data) + } + + testScript := func() { + profile := Profile{ + Type: ScriptType, + PostScripts: []ParameterizedText{ + "{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}-{{.Extras.key1}}", + }, + } + newName := "new-name.md" + expectedFullPath := filepath.Clean("~/test/dir/" + newName) + fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData) + assert.Nil(t, err) + assert.Equal(t, expectedFullPath, fullPath) + } + + testFolder() + + testFile() + + testScript() +} + +func TestProcessProfile(t *testing.T) { + cwd := filepath.Clean("~/test/dir") + monkeyPatchCalls := applyTestMonkeyPatch(t, monkeyPatch{ + ExecCommand: func(command string, args ...string) *exec.Cmd { + return exec.Command("echo", "foo") + }, + OSChdir: func(dir string) error { return nil }, + OSGetwd: func() (dir string, err error) { return cwd, nil }, + OSReadDir: func(dirname string) ([]os.DirEntry, error) { + return []os.DirEntry{}, nil + }, + }) + + projectName := "project-name" + var shell string + if runtime.GOOS == "windows" { + shell = "cmd" + } else { + shell = "bash" + } + profile := Profile{ + Path: cwd, + NamePatterns: []ParameterizedText{ + "{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}", + }, + RunPostscriptOnFailure: true, + PostScripts: []ParameterizedText{ + "echo 'hi'", + }, + ScriptShell: shell, + } + extras := map[string]string{ + "foo": "bar", + } + usr, err := user.Current() + assert.Nil(t, err) + assert.NotNil(t, usr) + errors := processProfile(profile, projectName, extras, *usr) + assert.Equal(t, 0, len(errors)) + assert.Equal(t, 1, len(monkeyPatchCalls.ExecCommandCalls)) + assert.Equal(t, 1, len(monkeyPatchCalls.OSChdirCalls)) + assert.Equal(t, 1, len(monkeyPatchCalls.OSGetwdCalls)) + assert.Equal(t, 1, len(monkeyPatchCalls.OSReadDirCalls)) +} diff --git a/test-common.go b/test-common.go new file mode 100644 index 0000000..dc7b324 --- /dev/null +++ b/test-common.go @@ -0,0 +1,22 @@ +package main + +import "time" + +func testNow() time.Time { + return time.Date(2022, 04, 20, 16, 20, 69, 0, time.UTC) +} + +func testPrePatternData() PrePatternData { + return PrePatternData{ + ProjectName: "test-project", + Year: "2022", + Month: "04", + Day: "20", + Hour: "16", + Minute: "20", + Second: "69", + Extras: map[string]string{ + "key1": "value1", + }, + } +}