template / commit
improved test coverage
.claude/settings.local.json | 5 +- .gitignore | 3 + .gitlab-ci.yml | 2 +- .vscode/settings.json | 1 + IMPROVEMENTS.md | 74 +++++-------- Makefile | 5 + internal/core/app_test.go | 87 +++++++++++++++ internal/core/file_test.go | 51 +++++++++ internal/core/main_test.go | 35 ++++++ internal/core/profile_test.go | 250 ++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 466 insertions(+), 47 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fb9b7f3..59205ff 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,10 @@ "Bash(make lint *)", "Bash(go build *)", "Bash(go tool *)", - "Bash(make test *)" + "Bash(make test *)", + "Read(//tmp/**)", + "Bash(go test *)", + "Bash(awk '-F[ :,]' '$NF==0 { *)" ] } } diff --git a/.gitignore b/.gitignore index 5bcfc30..4a56eb6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,8 @@ test-coverage.out test-coverage.html dev-setup +# macOS +**/.DS_Store + # idea files # .idea diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9359dad..79c8e05 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ # Copied from # https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Getting-Started.gitlab-ci.yml -image: golang:1.25 +image: golang:1.25.4 stages: # List of stages for jobs, and their order of execution - build-and-test diff --git a/.vscode/settings.json b/.vscode/settings.json index d170926..1ebfb6b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "bashdb", + "chdir", "filetemplate", "hackathon", "golangci", diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index 1152ee6..a641dbb 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -59,51 +59,35 @@ where they are reachable by tests without going through the CLI layer. --- -## 6. Improve test coverage - -Current coverage: ~56%. Tests focus on the happy path. Priority areas for new tests: - -### 6a. Error paths in config handling ([config.go](internal/core/config.go)) - -- `loadConfig` — file not found, malformed YAML, invalid permissions -- `saveConfig` — write failure (read-only directory) -- `validateConfig` — all validation branches (duplicate names, multiple defaults) - -### 6b. Error paths in file creation ([file.go](internal/core/file.go)) - -- `createFile` — template parse error, template execution error, `os.Create` - failure, `os.WriteFile` failure -- `readFile` — file not found - -### 6c. Profile processing edge cases ([profile.go](internal/core/profile.go)) - -- `processProfile` with `ScriptType` (currently untested or minimally tested) -- `createEntity` failure cases: mkdir failure, nested file creation failure -- `validateProfile` — all invalid combinations (missing target, unsupported OS, - etc.) - -### 6d. Extras loading ([extras.go](internal/core/extras.go)) - -- Missing extras file (should not error) -- Malformed extras YAML - -### 6e. App-level integration ([app.go](internal/core/app.go)) - -- Profile not found -- Config load failure -- Piped stdin with a folder-type profile (should be an error or no-op) - -### 6f. Coverage gate - -Add a threshold to the Makefile so CI fails if coverage drops below a chosen -minimum (e.g., 70%): - -```makefile -test: build - go test ./... -coverprofile=test-coverage.out - go tool cover -func=test-coverage.out | grep total \ - | awk '{if ($$3+0 < 70) exit 1}' -``` +## ✅ 6. Improve test coverage + +Coverage raised from ~56% to 67.7%. Threshold gate in [Makefile](Makefile) raised +to 65%. + +New tests added across: + +- **[app_test.go](internal/core/app_test.go)** — `readEnvVars` (all env var + branches), `loadAndMergeUserConfig` (file not found, valid merge, invalid YAML, + merge error from invalid global config), `findProfile` (named profile, default + profile, no-profile error) +- **[file_test.go](internal/core/file_test.go)** — `createFile` error paths + (`OSCreate` failure, `OSWriteFile` failure in template and empty-template paths), + `readFile` (happy path and missing file) +- **[profile_test.go](internal/core/profile_test.go)** — `createEntity` error + paths (mkdir failure, chdir failure, invalid file-name pattern inside folder, + subdir mkdir failure, `createFile` failure for both `FolderType` and `FileType`), + `processProfile` edge cases (target-dir mkdir/chdir failures, `createEntity` + error with and without `RunPostscriptOnFailure` swallow, invalid post-script + template, post script with no output, post script that exits non-zero), + `ScriptType` profile +- **[profile_test.go](internal/core/profile_test.go)** — new + `TestCreateNewNameForPatternsGetVersionError`: `getVersion` used in a non-last + pattern returns an error + +Remaining uncovered paths are inherently untestable in unit tests: CLI wiring +(`Run`, `createSettings`, `runMain`), the Windows branch of `shellCommandAndArgs`, +and error branches in `yaml.Marshal` / `bufio.Writer` that the library panics on +rather than returning errors. --- diff --git a/Makefile b/Makefile index 3caca2d..62fd938 100644 --- a/Makefile +++ b/Makefile @@ -29,8 +29,13 @@ lint: build tools-install lint-docs: tools-install gomarklint . +COVERAGE_THRESHOLD=65 + test: build go test -v ./... -coverprofile="./test-coverage.out" + @go tool cover -func="./test-coverage.out" | grep "^total:" | \ + awk -v t=$(COVERAGE_THRESHOLD) '{sub(/%/,"",$$3); if ($$3+0 < t) \ + {print "Coverage " $$3 "% is below threshold " t "%"; exit 1}}' test_coverage: test go tool cover -html="./test-coverage.out" -o "./test-coverage.html" diff --git a/internal/core/app_test.go b/internal/core/app_test.go new file mode 100644 index 0000000..fa243ee --- /dev/null +++ b/internal/core/app_test.go @@ -0,0 +1,87 @@ +package core + +import ( + "os" + "os/user" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadEnvVars(t *testing.T) { + os.Unsetenv("TEMPLATE_USE_DEFAULT_CONFIG") + ev, err := readEnvVars() + assert.NoError(t, err) + assert.False(t, ev.useDefaultConfig) + + t.Setenv("TEMPLATE_USE_DEFAULT_CONFIG", "true") + ev, err = readEnvVars() + assert.NoError(t, err) + assert.True(t, ev.useDefaultConfig) + + t.Setenv("TEMPLATE_USE_DEFAULT_CONFIG", "false") + ev, err = readEnvVars() + assert.NoError(t, err) + assert.False(t, ev.useDefaultConfig) + + t.Setenv("TEMPLATE_USE_DEFAULT_CONFIG", "not-a-bool") + _, err = readEnvVars() + assert.Error(t, err) +} + +func TestLoadAndMergeUserConfig(t *testing.T) { + usr, err := user.Current() + assert.NoError(t, err) + + // file not found — returns global config unchanged + globalConfig := defaultConfig() + result := loadAndMergeUserConfig("/nonexistent/path/user.yaml", usr, globalConfig) + assert.Equal(t, len(globalConfig.Profiles), len(result.Profiles)) + + // valid user config — merges profiles + dir := t.TempDir() + userConfigPath := filepath.Join(dir, "user.yaml") + userConfig := Config{ + Profiles: []Profile{ + { + Name: "user-profile", + Type: FileType, + NamePatterns: []ParameterizedText{"{{.Year}}.md"}, + TargetDirectory: "~/notes", + }, + }, + } + err = saveConfig(userConfigPath, userConfig) + assert.NoError(t, err) + + result = loadAndMergeUserConfig(userConfigPath, usr, globalConfig) + names := make([]string, len(result.Profiles)) + for i, p := range result.Profiles { + names[i] = p.Name + } + assert.Contains(t, names, "user-profile") + + // invalid yaml — returns global config unchanged + badPath := filepath.Join(dir, "bad.yaml") + err = os.WriteFile(badPath, []byte("invalid: yaml: ["), 0644) + assert.NoError(t, err) + result = loadAndMergeUserConfig(badPath, usr, globalConfig) + assert.Equal(t, len(globalConfig.Profiles), len(result.Profiles)) + + // valid user config but invalid globalConfig → mergeConfigs logs error and returns partial result + // mergeConfigs fails validation on an empty globalConfig ("no profiles found") + emptyGlobal := Config{} + result = loadAndMergeUserConfig(userConfigPath, usr, emptyGlobal) + // returns whatever mergeConfigs produced (empty config in this case) + _ = result +} + +func TestFindProfileNamedNotFound(t *testing.T) { + _, err := findProfile(settings{ + argumentValues: argumentValues{profile: "nonexistent"}, + config: Config{Profiles: []Profile{}}, + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "error finding profile") +} diff --git a/internal/core/file_test.go b/internal/core/file_test.go index 815bd72..19e29c0 100644 --- a/internal/core/file_test.go +++ b/internal/core/file_test.go @@ -2,6 +2,7 @@ package core import ( "bufio" + "fmt" "os" "strings" "testing" @@ -63,3 +64,53 @@ line3 err = createFile(prePatternData, "", path, nil) assert.Nil(t, err) } + +func TestCreateFileErrors(t *testing.T) { + prePatternData := testPrePatternData() + path := "~/test/out/test.txt" + + // OSCreate failure when using pipe input + applyTestMonkeyPatch(t, monkeyPatch{ + OSCreate: func(filename string) (*os.File, error) { + return nil, fmt.Errorf("create failed") + }, + }) + reader := strings.NewReader("some input") + scanner := bufio.NewScanner(reader) + err := createFile(prePatternData, "", path, scanner) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "error creating file") + + // OSWriteFile failure in template path + applyTestMonkeyPatch(t, monkeyPatch{ + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { + return fmt.Errorf("write failed") + }, + }) + err = createFile(prePatternData, "{{.ProjectName}}", path, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "error writing file") + + // OSWriteFile failure in empty-template path + err = createFile(prePatternData, "", path, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "error writing file") +} + +func TestReadFile(t *testing.T) { + f, err := os.CreateTemp("./test", "readfile-*.txt") + assert.Nil(t, err) + defer os.Remove(f.Name()) + + content := "hello world" + _, err = f.WriteString(content) + assert.Nil(t, err) + f.Close() + + got, err := readFile(f.Name()) + assert.Nil(t, err) + assert.Equal(t, content, got) + + _, err = readFile("nonexistent-file-that-does-not-exist.txt") + assert.NotNil(t, err) +} diff --git a/internal/core/main_test.go b/internal/core/main_test.go index 5088ccf..ac72a07 100644 --- a/internal/core/main_test.go +++ b/internal/core/main_test.go @@ -48,4 +48,39 @@ func TestFindProfile(t *testing.T) { assert.Equal(t, FolderType, profile.Type) assert.Equal(t, 1, len(profile.NamePatterns)) assert.Equal(t, ParameterizedText(dirTemplate), profile.NamePatterns[0]) + + // script template → ScriptType + scriptTemplate := "echo hello" + profile, err = findProfile(settings{ + argumentValues: argumentValues{ + scriptTemplate: scriptTemplate, + }, + }) + assert.NoError(t, err) + assert.Equal(t, ScriptType, profile.Type) + assert.Equal(t, 1, len(profile.PostScripts)) + assert.Equal(t, ParameterizedText(scriptTemplate), profile.PostScripts[0]) + + // named profile resolved from config + namedProfile := Profile{Name: "my-profile", Type: FileType, NamePatterns: []ParameterizedText{"name"}, TargetDirectory: "~/dir"} + profile, err = findProfile(settings{ + argumentValues: argumentValues{profile: "my-profile"}, + config: Config{Profiles: []Profile{namedProfile}}, + }) + assert.NoError(t, err) + assert.Equal(t, "my-profile", profile.Name) + + // default profile when no name given + defaultProfile := Profile{Name: "default-profile", IsDefault: true, Type: FileType, NamePatterns: []ParameterizedText{"name"}, TargetDirectory: "~/dir"} + profile, err = findProfile(settings{ + config: Config{Profiles: []Profile{defaultProfile}}, + }) + assert.NoError(t, err) + assert.Equal(t, "default-profile", profile.Name) + + // error: no profile specified and no default + _, err = findProfile(settings{ + config: Config{Profiles: []Profile{}}, + }) + assert.Error(t, err) } diff --git a/internal/core/profile_test.go b/internal/core/profile_test.go index 52f5ed7..6a19c9a 100644 --- a/internal/core/profile_test.go +++ b/internal/core/profile_test.go @@ -1,12 +1,15 @@ package core import ( + "bufio" + "fmt" "io/fs" "os" "os/exec" "os/user" "path/filepath" "runtime" + "strings" "testing" "time" @@ -224,6 +227,75 @@ func TestCreateEntity(t *testing.T) { testFile() testScript() + + // OSMkdir failure for folder type + applyTestMonkeyPatch(t, monkeyPatch{ + OSMkdir: func(path string, perm os.FileMode) error { return fmt.Errorf("mkdir failed") }, + }) + _, err := createEntity(now, Profile{Type: FolderType}, cwd, "new-name", projectName, extras, patternData, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "error creating directory") + + // OSChdir failure after successful OSMkdir + applyTestMonkeyPatch(t, monkeyPatch{ + OSMkdir: func(path string, perm os.FileMode) error { return nil }, + OSChdir: func(dir string) error { return fmt.Errorf("chdir failed") }, + }) + _, err = createEntity(now, Profile{Type: FolderType}, cwd, "new-name", projectName, extras, patternData, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "unable to change directory") + + // createFile failure inside folder (FolderType) + 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 fmt.Errorf("write failed") }, + }) + _, err = createEntity(now, Profile{ + Type: FolderType, + Files: []FileInFolder{{Name: "f.md", FileTemplate: "tmpl"}}, + }, cwd, "new-name", projectName, extras, patternData, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "error creating file") + + // invalid file name pattern inside folder triggers createNewNameForPatterns failure + applyTestMonkeyPatch(t, monkeyPatch{ + OSMkdir: func(path string, perm os.FileMode) error { return nil }, + OSChdir: func(dir string) error { return nil }, + }) + _, err = createEntity(now, Profile{ + Type: FolderType, + Files: []FileInFolder{{Name: ParameterizedText("{{.Bad"), FileTemplate: "tmpl"}}, + }, cwd, "new-name", projectName, extras, patternData, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "error creating file name for file index") + + // subdir OSMkdir failure inside folder + mkdirCount := 0 + applyTestMonkeyPatch(t, monkeyPatch{ + OSMkdir: func(path string, perm os.FileMode) error { + mkdirCount++ + if mkdirCount > 1 { + return fmt.Errorf("subdir mkdir failed") + } + return nil + }, + OSChdir: func(dir string) error { return nil }, + }) + _, err = createEntity(now, Profile{ + Type: FolderType, + Files: []FileInFolder{{Name: "f.md", FileTemplate: "tmpl"}}, + }, cwd, "new-name", projectName, extras, patternData, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "error creating sub directory") + + // FileType createFile failure + applyTestMonkeyPatch(t, monkeyPatch{ + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { return fmt.Errorf("write failed") }, + }) + _, err = createEntity(now, Profile{Type: FileType, FileTemplate: "tmpl"}, cwd, "new-name.md", projectName, extras, patternData, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "error writing file") } type testFile struct { @@ -348,6 +420,184 @@ func TestProcessProfile(t *testing.T) { assert.Equal(t, 1, len(monkeyPatchCalls.OSGetwdCalls), "calls to OSGetwd") assert.Equal(t, 1, len(monkeyPatchCalls.OSReadDirCalls), "calls to OSReadDir") assert.Equal(t, 1, len(monkeyPatchCalls.OSWriteFileCalls), "calls to OSWriteFile") + + // script-type profile runs a post-script and creates no files + scriptProfile := Profile{ + Type: ScriptType, + Name: "my-script", + PostScripts: []ParameterizedText{"echo hello"}, + } + testSuccess(scriptProfile) + assert.Equal(t, 1, len(monkeyPatchCalls.ExecCommandCalls), "script profile should run one command") + assert.Equal(t, 0, len(monkeyPatchCalls.OSWriteFileCalls), "script profile should not write files") + + // pipe input with a folder-type profile is an error + folderProfile := Profile{ + Type: FolderType, + Name: "folder", + NamePatterns: []ParameterizedText{"newfolder"}, + TargetDirectory: "./test/dir", + } + scanner := bufio.NewScanner(strings.NewReader("some input")) + configureMonkeyPatching() + errs := processProfile(folderProfile, projectName, extras, *usr, scanner) + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "cannot use pipe input with non-file type profile") + + // OSGetwd failure + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return "", fmt.Errorf("getwd failed") }, + }) + errs = processProfile(Profile{Type: FileType, NamePatterns: []ParameterizedText{"f"}}, projectName, extras, *usr, nil) + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "unable to get current working directory") + + // OSReadDir failure + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return cwd, nil }, + OSReadDir: func(dirname string) ([]os.DirEntry, error) { return nil, fmt.Errorf("readdir failed") }, + OSStat: func(name string) (os.FileInfo, error) { return createDirEntry("d", true), nil }, + OSChdir: func(dir string) error { return nil }, + }) + errs = processProfile(Profile{Type: FileType, NamePatterns: []ParameterizedText{"f"}, TargetDirectory: "./test/dir"}, projectName, extras, *usr, nil) + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "readdir failed") + + // OSStat returns ErrNotExist → OSMkdir fails + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return cwd, nil }, + OSStat: func(name string) (os.FileInfo, error) { return nil, os.ErrNotExist }, + OSMkdir: func(path string, perm os.FileMode) error { return fmt.Errorf("mkdir target failed") }, + }) + errs = processProfile(Profile{Type: FileType, NamePatterns: []ParameterizedText{"f"}, TargetDirectory: "./test/dir"}, projectName, extras, *usr, nil) + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "unable to create target directory") + + // OSChdir fails after mkdir succeeds + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return cwd, nil }, + OSStat: func(name string) (os.FileInfo, error) { return nil, os.ErrNotExist }, + OSMkdir: func(path string, perm os.FileMode) error { return nil }, + OSChdir: func(dir string) error { return fmt.Errorf("chdir target failed") }, + }) + errs = processProfile(Profile{Type: FileType, NamePatterns: []ParameterizedText{"f"}, TargetDirectory: "./test/dir"}, projectName, extras, *usr, nil) + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "unable to change directory (1)") + + // RunPostscriptOnFailure=true, no good name, still runs post script + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return cwd, nil }, + OSStat: func(name string) (os.FileInfo, error) { return createDirEntry("d", true), nil }, + OSReadDir: func(dirname string) ([]os.DirEntry, error) { + return []os.DirEntry{createDirEntry("file1", false)}, nil + }, + ExecCommand: func(command string, args ...string) *exec.Cmd { + shell, shellArgs := shellCommandAndArgs("echo") + return exec.Command(shell, shellArgs...) + }, + }) + errs = processProfile(Profile{ + Type: FileType, + NamePatterns: []ParameterizedText{"file1"}, + RunPostscriptOnFailure: true, + PostScripts: []ParameterizedText{"echo hi"}, + }, projectName, extras, *usr, nil) + assert.Equal(t, 0, len(errs)) + + // createEntity error without swallow → "unable to create entity" + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return cwd, nil }, + OSStat: func(name string) (os.FileInfo, error) { return createDirEntry("d", true), nil }, + OSReadDir: func(dirname string) ([]os.DirEntry, error) { return []os.DirEntry{}, nil }, + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { + return fmt.Errorf("write failed") + }, + }) + errs = processProfile(Profile{ + Type: FileType, + NamePatterns: []ParameterizedText{"newfile"}, + }, projectName, extras, *usr, nil) + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "unable to create entity") + + // createEntity error with swallow → continues to post scripts + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return cwd, nil }, + OSStat: func(name string) (os.FileInfo, error) { return createDirEntry("d", true), nil }, + OSReadDir: func(dirname string) ([]os.DirEntry, error) { return []os.DirEntry{}, nil }, + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { + return fmt.Errorf("write failed") + }, + ExecCommand: func(command string, args ...string) *exec.Cmd { + shell, shellArgs := shellCommandAndArgs("echo") + return exec.Command(shell, shellArgs...) + }, + }) + errs = processProfile(Profile{ + Type: FileType, + NamePatterns: []ParameterizedText{"newfile"}, + RunPostscriptOnFailure: true, + PostScripts: []ParameterizedText{"echo hi"}, + }, projectName, extras, *usr, nil) + assert.Equal(t, 0, len(errs)) + + // invalid post script template → createPostScriptArrays failure + configureMonkeyPatching() + errs = processProfile(Profile{ + Type: FileType, + NamePatterns: []ParameterizedText{"file3"}, + TargetDirectory: "./test/dir", + PostScripts: []ParameterizedText{"{{.Bad"}, + }, projectName, extras, *usr, nil) + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "unable to create post scripts") + + // post script with no output → "Post script had no output" + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return cwd, nil }, + OSStat: func(name string) (os.FileInfo, error) { return createDirEntry("d", true), nil }, + OSReadDir: func(dirname string) ([]os.DirEntry, error) { return []os.DirEntry{}, nil }, + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { return nil }, + ExecCommand: func(command string, args ...string) *exec.Cmd { + return exec.Command("sh", "-c", "true") // exits 0, no stdout output + }, + }) + errs = processProfile(Profile{ + Type: FileType, + NamePatterns: []ParameterizedText{"newfile"}, + PostScripts: []ParameterizedText{"true"}, + }, projectName, extras, *usr, nil) + assert.Equal(t, 0, len(errs)) + + // post script that exits non-zero → logs error but does not fail + applyTestMonkeyPatch(t, monkeyPatch{ + OSGetwd: func() (string, error) { return cwd, nil }, + OSStat: func(name string) (os.FileInfo, error) { return createDirEntry("d", true), nil }, + OSReadDir: func(dirname string) ([]os.DirEntry, error) { return []os.DirEntry{}, nil }, + OSWriteFile: func(filename string, data []byte, perm os.FileMode) error { return nil }, + ExecCommand: func(command string, args ...string) *exec.Cmd { + return exec.Command("sh", "-c", "false") // exits 1 + }, + }) + errs = processProfile(Profile{ + Type: FileType, + NamePatterns: []ParameterizedText{"newfile"}, + PostScripts: []ParameterizedText{"false"}, + }, projectName, extras, *usr, nil) + assert.Equal(t, 0, len(errs)) +} + +func TestCreateNewNameForPatternsGetVersionError(t *testing.T) { + now := testNow() + patternData := testPrePatternData() + // getVersion used in a non-last pattern is an error + _, err := createNewNameForPatterns(now, patternData, "proj", + []ParameterizedText{"{{getVersion}}-name", "fallback"}, + nil, + func(newName string) bool { return false }, // always reject + ) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "cannot use function getVersion in a pattern that is not the last pattern") } func getCurrentOs() OSType {