▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
3.4 KB raw
  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)
	}
}