fuzzyworld

template / commit

Merge branch 'testing' into 'main'

6437e549da4d9cc6939de792326e29170fa2bf6d · Pavel S · 2022-10-30 17:58:09 +0000 · parents 11e8e4b22de8 a5ad914b538b

Merge branch 'testing' into 'main'

Testing

See merge request fuzzy-rp/template!5
 README.md       |   5 ++
 file.go         |  75 +++++-------------------
 file_test.go    |  44 ++++++++++++++
 main_test.go    |  23 ++++++++
 monkey-patch.go | 114 +++++++++++++++++++++++++++++++++++++
 profile.go      |  18 +++---
 profile_test.go | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test-common.go  |  22 +++++++
 8 files changed, 404 insertions(+), 70 deletions(-)

diff --git a/README.md b/README.md
index 43d482e..61c8968 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,11 @@ For anything, open an issue in this repo.
 
 I'm going to use this tool daily to figure out how it can be improved.
 
+While I am the only user, this app will stay at version `0.x.y`.
+At this point, breaking changes may happen.
+
+Once there are enough other users and enough new feature requirements are added, we'll transition to version `1.x.y`.
+
 ### Basic features
 
 Here are the basic features of the CLI that I want to keep:
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..683aac4 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,169 @@ 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"
+	}
+	extras := map[string]string{
+		"foo": "bar",
+	}
+
+	testSuccess := func() {
+		profile := Profile{
+			Path: cwd,
+			NamePatterns: []ParameterizedText{
+				"{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}",
+			},
+			RunPostscriptOnFailure: true,
+			PostScripts: []ParameterizedText{
+				"echo 'hi'",
+			},
+			ScriptShell: shell,
+		}
+
+		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))
+	}
+	testFailure := func() {
+		profile := Profile{
+			Path: cwd,
+			NamePatterns: []ParameterizedText{
+				"{{.ProjectName",
+			},
+			// RunPostscriptOnFailure: true,
+			PostScripts: []ParameterizedText{
+				"echo 'hi'",
+			},
+			ScriptShell: shell,
+		}
+
+		usr, err := user.Current()
+		assert.Nil(t, err)
+		assert.NotNil(t, usr)
+
+		errors := processProfile(profile, projectName, extras, *usr)
+		assert.Equal(t, 1, len(errors))
+	}
+
+	testSuccess()
+
+	testFailure()
+}
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",
+		},
+	}
+}