-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a0d0e0d
commit 5cf64ef
Showing
10 changed files
with
420 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package defaults | ||
|
||
import ( | ||
"encoding/json" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/lucassabreu/clockify-cli/strhlp" | ||
"github.com/pkg/errors" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// DefaultTimeEntry has the default properties for the working directory | ||
type DefaultTimeEntry struct { | ||
Workspace string `json:"workspace,omitempty" yaml:"workspace,omitempty"` | ||
ProjectID string `json:"project,omitempty" yaml:"project,omitempty"` | ||
TaskID string `json:"task,omitempty" yaml:"task,omitempty"` | ||
Description string `json:"description,omitempty" yaml:"description,omitempty"` | ||
Billable *bool `json:"billable,omitempty" yaml:"billable,omitempty"` | ||
TagIDs []string `json:"tags,omitempty" yaml:"tags,omitempty,flow"` | ||
} | ||
|
||
// ScanParam sets how ScanForDefaults should look for defaults | ||
type ScanParam struct { | ||
Dir string | ||
Filename string | ||
} | ||
|
||
// WriteDefaults persists the default values to a file | ||
func WriteDefaults(dir, filename string, d DefaultTimeEntry) error { | ||
n := filepath.Join(dir, filename) | ||
f, err := os.OpenFile(n, os.O_CREATE|os.O_RDWR, os.ModePerm) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if strings.HasSuffix(f.Name(), "json") { | ||
return json.NewEncoder(f).Encode(d) | ||
} | ||
|
||
return yaml.NewEncoder(f).Encode(d) | ||
} | ||
|
||
// ScanError wraps errors from scanning for the defaults file | ||
type ScanError struct { | ||
Err error | ||
} | ||
|
||
// Error shows error message | ||
func (s *ScanError) Error() string { | ||
return s.Unwrap().Error() | ||
} | ||
|
||
// Unwrap gives access to the error chain | ||
func (s *ScanError) Unwrap() error { | ||
return s.Err | ||
} | ||
|
||
// DefaultsFileNotFoundErr is returned when the scan can't find any files | ||
var DefaultsFileNotFoundErr = errors.New("defaults file not found") | ||
|
||
// ScanForDefaults scan the directory informed and its parents for the defaults | ||
// file | ||
func ScanForDefaults(p ScanParam) func() (DefaultTimeEntry, error) { | ||
return func() (DefaultTimeEntry, error) { | ||
if p.Filename == "" { | ||
p.Filename = ".clockify-defaults" | ||
} | ||
|
||
dir := filepath.FromSlash(p.Dir) | ||
d := DefaultTimeEntry{} | ||
for { | ||
f, err := firstMatch(dir, p.Filename) | ||
if err != nil { | ||
return d, &ScanError{ | ||
Err: errors.Wrap( | ||
err, "failed to open defaults file"), | ||
} | ||
} | ||
|
||
if f == nil { | ||
nDir := filepath.Dir(dir) | ||
if nDir == dir { | ||
return d, DefaultsFileNotFoundErr | ||
} | ||
|
||
dir = nDir | ||
continue | ||
} | ||
|
||
if strings.HasSuffix(f.Name(), "json") { | ||
err = json.NewDecoder(f).Decode(&d) | ||
} else { | ||
err = yaml.NewDecoder(f).Decode(&d) | ||
} | ||
|
||
if err != nil { | ||
return d, &ScanError{ | ||
Err: errors.Wrap( | ||
err, "failed to decode defaults file"), | ||
} | ||
} | ||
|
||
return d, nil | ||
} | ||
} | ||
} | ||
|
||
func firstMatch(dir, filename string) (*os.File, error) { | ||
ms, _ := filepath.Glob(filepath.Join(dir, filename+".*")) | ||
if len(ms) == 0 { | ||
return nil, nil | ||
} | ||
|
||
ms = strhlp.Filter( | ||
func(s string) bool { | ||
return strings.HasSuffix(s, ".json") || | ||
strings.HasSuffix(s, ".yml") || | ||
strings.HasSuffix(s, ".yaml") | ||
}, | ||
ms, | ||
) | ||
|
||
for _, m := range ms { | ||
entry, err := os.Open(m) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
s, err := entry.Stat() | ||
if err != nil || s.IsDir() { | ||
continue | ||
|
||
} | ||
return entry, nil | ||
} | ||
return nil, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
package defaults_test | ||
|
||
import ( | ||
"os" | ||
"path" | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util/defaults" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestWriteDefaults(t *testing.T) { | ||
tts := []struct { | ||
filename string | ||
d defaults.DefaultTimeEntry | ||
}{ | ||
{ | ||
filename: "y_empty.yml", | ||
d: defaults.DefaultTimeEntry{}, | ||
}, | ||
{ | ||
filename: "j_empty.json", | ||
d: defaults.DefaultTimeEntry{}, | ||
}, | ||
{ | ||
filename: "j_complete.json", | ||
d: defaults.DefaultTimeEntry{ | ||
Workspace: "w", | ||
ProjectID: "p", | ||
TaskID: "t", | ||
Description: "d", | ||
TagIDs: []string{"t1", "t2"}, | ||
}, | ||
}, | ||
{ | ||
filename: "y_complete.yaml", | ||
d: defaults.DefaultTimeEntry{ | ||
Workspace: "w", | ||
ProjectID: "p", | ||
TaskID: "t", | ||
Description: "d", | ||
TagIDs: []string{"t1", "t2"}, | ||
}, | ||
}, | ||
} | ||
|
||
dir := t.TempDir() | ||
for i := range tts { | ||
tt := &tts[i] | ||
t.Run(tt.filename, func(t *testing.T) { | ||
timeout(t, 6*time.Second, func() { | ||
err := defaults.WriteDefaults(dir, tt.filename, tt.d) | ||
if !assert.NoError(t, err) { | ||
return | ||
} | ||
|
||
f := strings.Split(tt.filename, ".")[0] | ||
|
||
r, err := defaults.ScanForDefaults(defaults.ScanParam{ | ||
Dir: dir, | ||
Filename: f, | ||
})() | ||
|
||
assert.NoError(t, err) | ||
assert.Equal(t, tt.d, r) | ||
}) | ||
}) | ||
} | ||
} | ||
|
||
func TestWriteDefaults_ShouldFail_WhenPermAreMissing(t *testing.T) { | ||
dir := t.TempDir() | ||
_ = os.Chmod(dir, 0444) | ||
timeout(t, 5*time.Second, func() { | ||
err := defaults.WriteDefaults(dir, "fail", defaults.DefaultTimeEntry{}) | ||
assert.Error(t, err) | ||
}) | ||
} | ||
|
||
func timeout(t *testing.T, d time.Duration, f func()) { | ||
done := make(chan bool) | ||
defer close(done) | ||
|
||
go func() { | ||
f() | ||
done <- true | ||
}() | ||
|
||
select { | ||
case <-done: | ||
case <-time.After(d): | ||
t.Error("timeout " + d.String()) | ||
} | ||
} | ||
|
||
func TestScanForDefaults_ShouldFail(t *testing.T) { | ||
wd, _ := os.Getwd() | ||
|
||
dir := t.TempDir() | ||
f, _ := os.OpenFile( | ||
filepath.Join(dir, "not-open.yaml"), os.O_CREATE, os.ModePerm) | ||
_ = f.Chmod(0000) | ||
_ = f.Close() | ||
|
||
tts := []struct { | ||
dir string | ||
filename string | ||
err interface{} | ||
}{ | ||
{ | ||
dir: wd, | ||
filename: "not-found", | ||
err: defaults.DefaultsFileNotFoundErr, | ||
}, | ||
{ | ||
dir: filepath.Join(wd, "test_data", "test_cur"), | ||
filename: "not-right", | ||
err: "invalid character", | ||
}, | ||
{ | ||
dir: dir, | ||
filename: "not-open", | ||
err: "permission denied", | ||
}, | ||
{ | ||
dir: filepath.Join(wd, "test_data", "test_empty", "dir.yaml"), | ||
filename: "dir", | ||
err: defaults.DefaultsFileNotFoundErr, | ||
}, | ||
} | ||
|
||
for i := range tts { | ||
tt := &tts[i] | ||
t.Run(tt.filename, func(t *testing.T) { | ||
timeout(t, 5*time.Second, func() { | ||
d, err := defaults.ScanForDefaults(defaults.ScanParam{ | ||
Dir: tt.dir, | ||
Filename: tt.filename, | ||
})() | ||
|
||
assert.Equal(t, d, defaults.DefaultTimeEntry{}) | ||
assert.Error(t, err) | ||
switch v := tt.err.(type) { | ||
case error: | ||
assert.ErrorIs(t, err, v) | ||
case string: | ||
assert.Regexp(t, v, err) | ||
} | ||
}) | ||
}) | ||
} | ||
} | ||
|
||
func TestScanForDefaults_ShouldLookUpperDirs(t *testing.T) { | ||
wd, _ := os.Getwd() | ||
tts := []struct { | ||
name string | ||
param defaults.ScanParam | ||
expected defaults.DefaultTimeEntry | ||
}{ | ||
{ | ||
name: "test_cur", | ||
param: defaults.ScanParam{ | ||
Dir: "./test_data/test_cur", | ||
}, | ||
expected: defaults.DefaultTimeEntry{ | ||
Workspace: "w", | ||
ProjectID: "p", | ||
TaskID: "t", | ||
Description: "d", | ||
TagIDs: []string{"t1", "t2"}, | ||
}, | ||
}, | ||
{ | ||
name: "test_cur, filename as defaults", | ||
param: defaults.ScanParam{ | ||
Dir: "./test_data/test_cur", | ||
Filename: "defaults", | ||
}, | ||
expected: defaults.DefaultTimeEntry{ | ||
Workspace: "W", | ||
ProjectID: "P", | ||
TaskID: "T", | ||
Description: "D", | ||
}, | ||
}, | ||
{ | ||
name: "down again", | ||
param: defaults.ScanParam{ | ||
Dir: "./test_data/test_cur/down/again", | ||
}, | ||
expected: defaults.DefaultTimeEntry{ | ||
Workspace: "w", | ||
ProjectID: "p", | ||
TaskID: "t", | ||
Description: "d", | ||
TagIDs: []string{"t1", "t2"}, | ||
}, | ||
}, | ||
{ | ||
name: "down path, filename as defaults", | ||
param: defaults.ScanParam{ | ||
Dir: "./test_data/test_cur/down/again", | ||
Filename: "defaults", | ||
}, | ||
expected: defaults.DefaultTimeEntry{ | ||
Workspace: "W", | ||
ProjectID: "P", | ||
TaskID: "T", | ||
Description: "D", | ||
}, | ||
}, | ||
{ | ||
name: "test_incompl", | ||
param: defaults.ScanParam{ | ||
Dir: "./test_data/test_incompl", | ||
}, | ||
expected: defaults.DefaultTimeEntry{ | ||
Workspace: "w", | ||
ProjectID: "p", | ||
}, | ||
}, | ||
{ | ||
name: "test_empty", | ||
param: defaults.ScanParam{ | ||
Dir: "./test_data/test_empty/down/here", | ||
}, | ||
expected: defaults.DefaultTimeEntry{}, | ||
}, | ||
} | ||
|
||
for i := range tts { | ||
tt := &tts[i] | ||
t.Run(tt.name, func(t *testing.T) { | ||
timeout(t, 1*time.Second, func() { | ||
tt.param.Dir = path.Join(wd, tt.param.Dir) | ||
d, _ := defaults.ScanForDefaults(tt.param)() | ||
assert.Equal(t, tt.expected, d) | ||
}) | ||
}) | ||
} | ||
} |
Empty file.
7 changes: 7 additions & 0 deletions
7
pkg/cmd/time-entry/util/defaults/test_data/test_cur/.clockify-defaults.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
workspace: "w" | ||
project: "p" | ||
task: "t" | ||
description: "d" | ||
tags: | ||
- "t1" | ||
- "t2" |
Oops, something went wrong.