template / commit
Adding a makefile and more tests.
.gitignore | 1 + Makefile | 26 +++++++++++++++ config.go | 69 +++----------------------------------- config_test.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ postscript.go | 3 +- postscript_test.go | 45 +++++++++++++++++++++++++ profile.go | 67 +++++++++++++++++++++++++++++++++++++ profile_test.go | 59 ++++++++++++++++++++++++++++++++ utils_test.go | 37 +++++++++++++++++++++ 9 files changed, 339 insertions(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index 984b600..4c85f83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .git template.exe template +test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..280617a --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +SHELL := pwsh.exe +.SHELLFLAGS := -Command + +mkfile_path := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) +BINARY_NAME := template + +.PHONY: test + +build: + go build . + +install: + ./scripts/clean-install.ps1 + +test: + go test -v ./... -coverprofile="./test/coverage.out" + +test_coverage: + go test -v ./... -coverprofile="./test/coverage.out" + go tool cover -html="./test/coverage.out" -o "./test/coverage.html" + +test_coverage_html: test_coverage + start "./test/coverage.html" + +clean: + go clean diff --git a/config.go b/config.go index 7bc8769..c8a6526 100644 --- a/config.go +++ b/config.go @@ -3,49 +3,10 @@ package main import ( "fmt" "os" - "strings" - "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) -type ProfileType string - -const ( - FileType ProfileType = "file" - FolderType ProfileType = "folder" - ScriptType ProfileType = "script" -) - -type OSType string - -const ( - Windows OSType = "Windows" - Mac OSType = "Mac" - All OSType = "All" -) - -type Profile struct { - IsDefault bool - Name string - Description string - Path string - Type ProfileType - NamePatterns []string - PostScripts []string - ScriptShell string - OS OSType - FileTemplatePath string - FileTemplate string - - // FileTemplatePath string -} - -var forbiddenProfileNames = []string{ - "list", - "get", -} - type Config struct { Profiles []Profile } @@ -141,6 +102,7 @@ Today's notes Description: "Creates a folder in the current folder in the format YYYY-MM-DD[-ProjectName][-vVersion]", Type: FolderType, ScriptShell: "pwsh", + PostScripts: []string{}, NamePatterns: []string{ "{{$version := getVersion}}" + "{{.Year}}-{{.Month}}-{{.Day}}" + @@ -188,9 +150,6 @@ Today's notes Type: ScriptType, NamePatterns: []string{ "{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", - "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", - "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", - "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", }, ScriptShell: "pwsh", PostScripts: []string{ @@ -211,35 +170,15 @@ func validateConfig(config Config) error { foundDefault := false for _, profile := range config.Profiles { - if profile.Name == "" { - return fmt.Errorf("Profile name is required") - } - if slices.Contains(forbiddenProfileNames, profile.Name) { - return fmt.Errorf("forbidden profile name '%s'", profile.Name) - } if profile.IsDefault { if foundDefault { return fmt.Errorf("multiple default profiles found") } foundDefault = true } - if profile.NamePatterns == nil || len(profile.NamePatterns) == 0 { - return fmt.Errorf("no name patterns found for profile %s", profile.Name) - } - if profile.Path != "" && strings.Contains(profile.Path, "\\") { - return fmt.Errorf("invalid path for profile %s: %s", profile.Name, profile.Path) - } - if profile.Type == "" || (profile.Type != FileType && profile.Type != FolderType && profile.Type != ScriptType) { - return fmt.Errorf("invalid profile type %s for profile %s", profile.Type, profile.Name) - } - if profile.Type != FileType && profile.FileTemplatePath != "" { - return fmt.Errorf("file template path is not supported for profile %s, which has Type=%s", profile.Name, profile.Type) - } - if profile.Type != ScriptType && profile.NamePatterns != nil && len(profile.NamePatterns) > 1 { - return fmt.Errorf("multiple name patterns are not supported for profile %s, which has Type=%s", profile.Name, profile.Type) - } - if profile.ScriptShell == "" && profile.PostScripts != nil && len(profile.PostScripts) > 0 { - return fmt.Errorf("script shell is required for profile %s", profile.Name) + error := validateProfile(profile) + if error != nil { + return error } } return nil diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..9b9a208 --- /dev/null +++ b/config_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "errors" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSaveLoadConfig(t *testing.T) { + config := defaultConfig() + configPath := "./test/test-config.yaml" + + // save config + err := saveConfig(configPath, config) + assert.Nil(t, err, "Error saving config") + + // load config + loadedConfig, err := loadConfig(configPath) + assert.Nil(t, err, "Error loading config") + + // test config + assert.Equal(t, loadedConfig, config) + + // test load non-existent config + _, err = loadConfig("non-existent-file.yaml") + assert.NotNil(t, err, "Expected to fail to load config, but succeeded") + + // test load invalid yaml + os.WriteFile("./test/invalid-yaml.yaml", []byte("invalid yaml"), 0644) + _, err = loadConfig("./test/invalid-yaml.yaml") + assert.NotNil(t, err, "Expected to fail to parse config, but succeeded") + + // test saving config to invalid path + err = saveConfig("invalid/path/invalid-config.yaml", config) + assert.NotNil(t, err, "Expected to fail to save config, but succeeded") + + // remove config + os.Remove(configPath) + + // test default config creation + _, err = os.Stat(configPath) + assert.True(t, errors.Is(err, os.ErrNotExist), "Expected config file to not exist, but it does") + + // get and create config + loadedConfig, err = getOrCreateConfig(configPath) + assert.Nil(t, err, "Error loading config") + + // test config + assert.Equal(t, loadedConfig, config) + + // verify config file exists + stat1, err := os.Stat(configPath) + assert.Nil(t, err, "Expected config file to exist, but it does not") + + // get and create config + loadedConfig, err = getOrCreateConfig(configPath) + assert.Nil(t, err, "Error loading config") + + // test config + assert.Equal(t, loadedConfig, config) + + // verify config file wasn't updated + stat2, err := os.Stat(configPath) + assert.Nil(t, err, "Expected config file to exist, but it does not") + assert.Equal(t, stat1.ModTime(), stat2.ModTime(), "Expected config file to not be modified, but it was") +} + +func TestValidateDefaultConfig(t *testing.T) { + config := defaultConfig() + err := validateConfig(config) + if err != nil { + t.Errorf("Expected to validate config, but failed: %v", err) + } +} + +func TestValidateConfig(t *testing.T) { + testFailure := func(config Config) { + err := validateConfig(config) + if err == nil { + t.Errorf("Expected to fail to validate config, but succeeded") + } + } + + testFailure(Config{}) + testFailure(Config{Profiles: []Profile{}}) + testFailure(Config{Profiles: []Profile{ + { + Name: "daily", + IsDefault: true, + }, + { + Name: "daily2", + IsDefault: true, + }, + }}) +} diff --git a/postscript.go b/postscript.go index dfc2024..e8cfcce 100644 --- a/postscript.go +++ b/postscript.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "strings" "text/template" ) @@ -15,7 +16,7 @@ func createPostScriptArrays(profile Profile, newName string, fullPath string, ex array := make([]string, 0) if profile.PostScripts == nil || len(profile.PostScripts) == 0 { - return nil, nil + return nil, fmt.Errorf("no post scripts specified") } patternData := PostScriptPatternData{ diff --git a/postscript_test.go b/postscript_test.go new file mode 100644 index 0000000..8ae03f6 --- /dev/null +++ b/postscript_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreatePostScriptArrays(t *testing.T) { + testFailure := func(profile Profile) { + _, err := createPostScriptArrays(profile, "test", "test", nil) + assert.NotNil(t, err, "Expected to fail to create post script arrays, but succeeded") + } + testSuccess := func(profile Profile) { + extras := map[string]string{ + "foo": "bar", + } + + postscripts, err := createPostScriptArrays(profile, "test", "full-path", extras) + assert.Nil(t, err, "Error creating postscript array") + assert.Equal(t, postscripts, []string{"echo \"test\""}) + } + + testSuccess(Profile{ + PostScripts: []string{ + "echo \"{{.Name}}\"", + }, + }) + testFailure(Profile{ + PostScripts: nil, + }) + testFailure(Profile{ + PostScripts: []string{}, + }) + testFailure(Profile{ + PostScripts: []string{ + "echo \"{{.Name\"", + }, + }) + testFailure(Profile{ + PostScripts: []string{ + "echo \"{{.NameUnknown}}\"", + }, + }) +} diff --git a/profile.go b/profile.go index 6389435..fd19ac1 100644 --- a/profile.go +++ b/profile.go @@ -10,8 +10,47 @@ import ( "runtime" "strings" "time" + + "golang.org/x/exp/slices" +) + +type ProfileType string + +const ( + FileType ProfileType = "file" + FolderType ProfileType = "folder" + ScriptType ProfileType = "script" +) + +type OSType string + +const ( + Windows OSType = "Windows" + Mac OSType = "Mac" + All OSType = "All" ) +type Profile struct { + IsDefault bool + Name string + Description string + Path string + Type ProfileType + NamePatterns []string + PostScripts []string + ScriptShell string + OS OSType + FileTemplatePath string + FileTemplate string + + // FileTemplatePath string +} + +var forbiddenProfileNames = []string{ + "list", + "get", +} + func findProfile(config Config, profileName string) (Profile, error) { if profileName == "" { // find default profile @@ -153,3 +192,31 @@ func processProfile(profile Profile, projectName string, extras map[string]strin return nil } + +func validateProfile(profile Profile) error { + if profile.Name == "" { + return fmt.Errorf("Profile name is required") + } + if slices.Contains(forbiddenProfileNames, profile.Name) { + return fmt.Errorf("forbidden profile name '%s'", profile.Name) + } + if profile.NamePatterns == nil || len(profile.NamePatterns) == 0 { + return fmt.Errorf("no name patterns found for profile %s", profile.Name) + } + if profile.Path != "" && strings.Contains(profile.Path, "\\") { + return fmt.Errorf("invalid path for profile %s: %s", profile.Name, profile.Path) + } + if profile.Type == "" || (profile.Type != FileType && profile.Type != FolderType && profile.Type != ScriptType) { + return fmt.Errorf("invalid profile type %s for profile %s", profile.Type, profile.Name) + } + if profile.Type != FileType && profile.FileTemplatePath != "" { + return fmt.Errorf("file template path is not supported for profile %s, which has Type=%s", profile.Name, profile.Type) + } + if profile.Type == ScriptType && profile.NamePatterns != nil && len(profile.NamePatterns) > 1 { + return fmt.Errorf("multiple name patterns are not supported for profile %s, which has Type=%s", profile.Name, profile.Type) + } + if profile.ScriptShell == "" && profile.PostScripts != nil && len(profile.PostScripts) > 0 { + return fmt.Errorf("script shell is required for profile %s", profile.Name) + } + return nil +} diff --git a/profile_test.go b/profile_test.go index b79cf7a..2eb60aa 100644 --- a/profile_test.go +++ b/profile_test.go @@ -4,6 +4,65 @@ import ( "testing" ) +func TestValidateProfile(t *testing.T) { + testSuccess := func(profile Profile) { + err := validateProfile(profile) + if err != nil { + t.Errorf("Expected to validate profile '%v', but failed: %v", profile.Name, err) + } + } + testFailure := func(profile Profile) { + err := validateProfile(profile) + if err == nil { + t.Errorf("Expected to fail to validate profile '%v', but succeeded", profile.Name) + } + } + + testSuccess(Profile{ + Type: FileType, + Name: "daily", + NamePatterns: []string{"{{.Date}}"}, + }) + + // empty name + testFailure(Profile{ + Name: "", + }) + // illegal name + testFailure(Profile{ + Name: "list", + }) + testFailure(Profile{ + Name: "get", + }) + // file type with no path + testFailure(Profile{ + Type: FileType, + Name: "daily", + }) + testFailure(Profile{ + Type: FileType, + Name: "daily", + Path: "\\", + }) + // no name patterns + testFailure(Profile{ + Type: FileType, + Name: "daily", + NamePatterns: []string{}, + }) + // testFailure(Profile{ + // Type: FileType, + // Name: "daily", + // NamePatterns: []string{"test"}, + // FileTemplatePath: "", + // }) + + for _, profile := range defaultConfig().Profiles { + testSuccess(profile) + } +} + func TestFindProfile(t *testing.T) { config := Config{ Profiles: []Profile{ diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..1cd946f --- /dev/null +++ b/utils_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "os/user" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetFullPath(t *testing.T) { + testSuccess := func(path string, expected string) { + fullPath, err := getFullPath(path) + if err != nil { + t.Errorf("Expected to get full path for '%v', but failed: %v", path, err) + } + if fullPath != expected { + t.Errorf("Expected to get full path '%v' for '%v', but got '%v'", expected, path, fullPath) + } + } + // testFailure := func(path string) { + // _, err := getFullPath(path) + // if err == nil { + // t.Errorf("Expected to fail to get full path for '%v', but succeeded", path) + // } + // } + + testSuccess("C:\\", "C:\\") + testSuccess("C:\\test", "C:\\test") + testSuccess("C:\\test\\", "C:\\test\\") + testSuccess("C:\\test\\..", "C:\\test\\..") // WRONG + + usr, err := user.Current() + assert.NoError(t, err, "Failed to get current user") + + testSuccess("~/", usr.HomeDir) + testSuccess("~", usr.HomeDir) +}