1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
package history
import (
"errors"
"os"
"strings"
"testing"
)
var errBoom = errors.New("boom")
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestParseTockConfig(t *testing.T) {
cfg := []byte("backend: sqlite\nsqlite:\n path: /Users/x/.tock.db\nlanguage: eng\n")
b, p := parseTockConfig(cfg)
if b != "sqlite" || p != "/Users/x/.tock.db" {
t.Fatalf("got backend=%q path=%q", b, p)
}
b2, _ := parseTockConfig([]byte("backend: file\n"))
if b2 != "file" {
t.Errorf("backend=%q want file", b2)
}
}
func TestParseLines(t *testing.T) {
got := parseLines([]byte("ARCHER\n Development \n\nDoctor\n"))
want := []string{"ARCHER", "Development", "Doctor"}
if len(got) != 3 || got[0] != want[0] || got[1] != want[1] || got[2] != want[2] {
t.Fatalf("got %v want %v", got, want)
}
}
func TestDedupeEntries(t *testing.T) {
raw := []lastEntry{
{Project: "ARCHER", Description: "a", Tags: []string{"x", "y"}},
{Project: "ARCHER", Description: "b", Tags: []string{"x"}},
{Project: "", Description: "", Tags: nil},
}
h := dedupeEntries(raw)
if len(h.Projects) != 1 || h.Projects[0] != "ARCHER" {
t.Errorf("projects=%v", h.Projects)
}
if len(h.Descriptions) != 2 {
t.Errorf("descriptions=%v", h.Descriptions)
}
if len(h.Tags) != 2 || h.Tags[0] != "x" || h.Tags[1] != "y" {
t.Errorf("tags=%v", h.Tags)
}
}
// fakeRunner routes by the command name (sqlite3 vs tock) and a query substring.
type fakeRunner struct {
sqlite map[string][]byte
last []byte
sqlErr error
tockErr error
}
func (f fakeRunner) run(name string, args ...string) ([]byte, error) {
if name == "sqlite3" {
if f.sqlErr != nil {
return nil, f.sqlErr
}
q := args[len(args)-1]
for sub, out := range f.sqlite {
if strings.Contains(q, sub) {
return out, nil
}
}
return nil, nil
}
// tock last --json
if f.tockErr != nil {
return nil, f.tockErr
}
return f.last, nil
}
func TestLoadWithSqlite(t *testing.T) {
dir := t.TempDir()
cfgPath := dir + "/tock.yaml"
writeFile(t, cfgPath, "backend: sqlite\nsqlite:\n path: "+dir+"/x.db\n")
writeFile(t, dir+"/x.db", "") // must exist for the DB-present check
r := fakeRunner{sqlite: map[string][]byte{
"FROM activities WHERE project": []byte("ARCHER\nDoctor\n"),
"FROM activities WHERE description": []byte("standup\nemails\n"),
"json_each": []byte("admin\nmeeting\n"),
}}
h := loadWith(r, cfgPath)
if len(h.Projects) != 2 || h.Projects[0] != "ARCHER" {
t.Errorf("projects=%v", h.Projects)
}
if len(h.Descriptions) != 2 || len(h.Tags) != 2 {
t.Errorf("desc=%v tags=%v", h.Descriptions, h.Tags)
}
}
func TestLoadWithFallbackToTockLast(t *testing.T) {
dir := t.TempDir()
cfgPath := dir + "/tock.yaml"
writeFile(t, cfgPath, "backend: file\n")
r := fakeRunner{last: []byte(`[{"project":"ARCHER","description":"a","tags":["x"]},{"project":"ARCHER","description":"b"}]`)}
h := loadWith(r, cfgPath)
if len(h.Projects) != 1 || len(h.Descriptions) != 2 || len(h.Tags) != 1 {
t.Errorf("fallback wrong: %+v", h)
}
}
func TestLoadWithTotalFailureIsEmpty(t *testing.T) {
dir := t.TempDir()
cfgPath := dir + "/tock.yaml"
writeFile(t, cfgPath, "backend: file\n")
r := fakeRunner{tockErr: errBoom}
h := loadWith(r, cfgPath)
if len(h.Projects) != 0 || len(h.Descriptions) != 0 || len(h.Tags) != 0 {
t.Errorf("expected empty, got %+v", h)
}
}
|