M cal.go +5 -4
@@ 11,14 11,14 @@ import (
type Calendar interface {
Stats() CalStats
- ForEachIcs(func(io.Reader) error)
+ ForEachIcs(func(io.Reader) error) error
}
type CalStats struct {
Entries int
}
-func NewCal(path string) Calendar {
+func OpenCal(path string) Calendar {
return calendar{path}
}
@@ 36,9 36,9 @@ func (c calendar) Stats() CalStats {
}
}
-func (c calendar) ForEachIcs(f func(io.Reader) error) {
+func (c calendar) ForEachIcs(f func(io.Reader) error) error {
// drop through to operate on calendar
- filepath.WalkDir(c.path, func(p string, d fs.DirEntry, err error) error {
+ err := filepath.WalkDir(c.path, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
@@ 56,4 56,5 @@ func (c calendar) ForEachIcs(f func(io.R
}
return nil
})
+ return err
}
M cal_test.go +159 -43
@@ 1,74 1,190 @@
package main
import (
+ "bufio"
"io"
+ "io/ioutil"
+ "os"
"reflect"
+ "strings"
"testing"
+ "text/template"
+ "time"
)
-func TestNewCal(t *testing.T) {
- type args struct {
- path string
- }
- tests := []struct {
- name string
- args args
- want Calendar
- }{
- // TODO: Add test cases.
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := NewCal(tt.args.path); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("NewCal() %s = %v, want %v", tt.name, got, tt.want)
- }
- })
- }
-}
-
func Test_calendar_Stats(t *testing.T) {
- type fields struct {
- path string
- }
tests := []struct {
name string
- fields fields
+ count int
want CalStats
}{
- // TODO: Add test cases.
+ {"is empty", 0, CalStats{0}},
+ {"not empty", 5, CalStats{5}},
}
+ cn := "testcal"
+ os.RemoveAll(cn)
for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- c := calendar{
- path: tt.fields.path,
- }
+ t.Run (tt.name, func(t *testing.T) {
+ setupTestDir(cn, tt.count)
+ defer os.RemoveAll(cn)
+ c := OpenCal(cn)
if got := c.Stats(); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("calendar.Stats() %s = %v, want %v", tt.name, got, tt.want)
+ t.Errorf("%s = %v, want %v", tt.name, got, tt.want)
}
})
}
}
func Test_calendar_ForEachIcs(t *testing.T) {
- type fields struct {
- path string
- }
- type args struct {
- f func(io.Reader) error
- }
+ var ctr int
+ var testN int
tests := []struct {
name string
- fields fields
- args args
+ count int
+ wants int
+ args func (io.Reader) error
}{
- // TODO: Add test cases.
+ { "count", 10, 10, func(f io.Reader) error {
+ ctr++
+ return nil
+ }},
+ { "lines", 1, 34, func(f io.Reader) error {
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ ctr++
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+ return nil
+ }},
+ { "UID", 3, 3, func(f io.Reader) error {
+ s := makeId(testN)
+ bs, err := ioutil.ReadAll(f)
+ if err != nil {
+ return err
+ }
+ strings.Contains(string(bs), s)
+ return nil
+ }},
}
+ cn := "testcal"
+ os.RemoveAll(cn)
for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- c := calendar{
- path: tt.fields.path,
+ t.Run (tt.name, func(t *testing.T) {
+ if err := setupTestDir(cn, tt.count); err != nil {
+ t.Errorf("%s = setting up test cal directory: %s", cn, err)
}
- c.ForEachIcs(tt.args.f)
+ defer os.RemoveAll(cn)
+ c := OpenCal(cn)
+ ctr = 0
+ if err := c.ForEachIcs(tt.args); err != nil {
+ t.Errorf("%s = error running test %v", tt.name, err)
+ }
})
}
}
+
+func setupTestDir(cn string, sz int) error {
+ err := os.Mkdir(cn, 0700)
+ if err != nil {
+ return err
+ }
+ // Make some entries
+ for ctr := 0; ctr < sz; ctr++ {
+ f, err := os.CreateTemp(cn, "test.*.ics")
+ if err != nil {
+ return err
+ }
+ makeData(f, makeId(ctr), "summary", "location", time.Now(), time.Now())
+ f.Close()
+
+ }
+ return nil
+}
+
+func makeId(k int) string {
+ rv := make([]byte, 37)
+ for i := 0; i < 37; i++ {
+ rv[i] = byte(k)
+ }
+ return string(rv)
+}
+func makeSummary(k int) string {
+ l := byte(('a' + k) % 26)
+ rv := make([]byte, 37)
+ for i := 0; i < 37; i++ {
+ rv[i] = l
+ }
+ return string(rv)
+}
+func makeLocation(k int) string {
+ l := byte(('A' + k) % 26)
+ rv := make([]byte, 11)
+ for i := 0; i < 11; i++ {
+ rv[i] = l
+ }
+ return string(rv)
+}
+func makeStart(k int) time.Time {
+ return time.Unix(int64(k), int64(k))
+}
+func makeEnd(k int) time.Time {
+ return time.Unix(int64(k+1), int64(k+1))
+}
+
+func makeData(fout io.Writer, uid, summary, location string, start, end time.Time) error {
+ t, e := template.New("data").Parse(DATA)
+ if e != nil {
+ return e
+ }
+ return t.Execute(fout, struct {
+ Uid string
+ Summary string
+ Location string
+ Start time.Time
+ End time.Time
+ } {
+ Uid: uid,
+ Summary: summary,
+ Location: location,
+ Start: start,
+ End: end,
+ },
+ )
+}
+
+var DATA string = `BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN
+BEGIN:VTIMEZONE
+TZID:America/Chicago
+BEGIN:DAYLIGHT
+DTSTART;VALUE=DATE-TIME:20190310T030000
+TZNAME:CDT
+TZOFFSETFROM:-0600
+TZOFFSETTO:-0500
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART;VALUE=DATE-TIME:20191103T010000
+TZNAME:CST
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0600
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+SUMMARY:{{.Summary}}
+DTSTART;TZID=America/Chicago;VALUE=DATE-TIME:{{.Start}}
+DTEND;TZID=America/Chicago;VALUE=DATE-TIME:{{.End}}
+DTSTAMP;VALUE=DATE-TIME:20190816T143301Z
+UID:{{.Uid}}
+SEQUENCE:0
+CATEGORIES:
+LOCATION:{{.Location}}
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:
+TRIGGER:-PT1H
+END:VALARM
+END:VEVENT
+END:VCALENDAR`
M db.go +26 -4
@@ 14,22 14,35 @@ import (
type Db interface {
Stats() DbStats
UpdateInvites(string, []string) error
+ Error() error
}
-// TODO fill in DbStats
type DbStats struct {
Entries int
}
type db struct {
path string
+ err error
}
-func NewDb(path string) Db {
- return db{path}
+func OpenDb(path string) Db {
+ fout, err := os.Open(path)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ // Set up the test DB
+ fout, err = os.Create(path)
+ if err != nil {
+ return db{err: err}
+ }
+ } else {
+ return db{err: err}
+ }
+ }
+ fout.Close()
+ return db{path: path}
}
-// TODO implement dbStats()
func (d db) Stats() DbStats {
rs, err := d.readDb()
if err != nil {
@@ 41,6 54,10 @@ func (d db) Stats() DbStats {
}
}
+func (d db) Error() error {
+ return d.err
+}
+
// persistInvites saves the invite list of the ICS UUID to the database.
// It replaces any existing record for that UUID.
func (d db) UpdateInvites(id string, as []string) error {
@@ 59,6 76,7 @@ func (d db) UpdateInvites(id string, as
return err
}
defer lock.Unlock()
+ defer os.Remove(lock.Path())
rs, err := d.readDb()
if err != nil {
@@ 69,6 87,8 @@ func (d db) UpdateInvites(id string, as
for _, r := range rs {
if r[0] == id {
r[1] = sas
+ found = true
+ break
}
}
if !found {
@@ 92,11 112,13 @@ func (d db) save(rs [][]string) error {
}
defer fout.Close()
csvw := csv.NewWriter(fout)
+ csvw.Comma = '\t'
err = csvw.WriteAll(rs)
if err != nil {
return err
}
csvw.Flush()
+ os.Rename(foutn, d.path)
return nil
}
M db_test.go +111 -88
@@ 1,46 1,39 @@
package main
import (
+ "io/ioutil"
+ "os"
"reflect"
"testing"
)
-func TestNewDb(t *testing.T) {
- type args struct {
- path string
- }
- tests := []struct {
- name string
- args args
- want Db
- }{
- // TODO: Add test cases.
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := NewDb(tt.args.path); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("NewDb() %s = %v, want %v", tt.name, got, tt.want)
- }
- })
- }
-}
-
func Test_db_Stats(t *testing.T) {
type fields struct {
path string
+ size int
}
tests := []struct {
name string
fields fields
want DbStats
}{
- // TODO: Add test cases.
+ {"is empty", fields{"testdb", 0}, DbStats{0}},
+ {"not empty", fields{"testdb", 5}, DbStats{5}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- d := db{
- path: tt.fields.path,
+ td, err := ioutil.TempFile("", tt.fields.path)
+ if err != nil {
+ t.Errorf("db.Stats() %s = setting up DB: %s", tt.name, err)
+ return
}
+ defer os.Remove(td.Name())
+ // Make some entries
+ for ctr := 0; ctr < tt.fields.size; ctr++ {
+ td.WriteString("dummy\n")
+ }
+ td.Close()
+ d := OpenDb(td.Name())
if got := d.Stats(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("db.Stats() %s = %v, want %v", tt.name, got, tt.want)
}
@@ 49,85 42,115 @@ func Test_db_Stats(t *testing.T) {
}
func Test_db_UpdateInvites(t *testing.T) {
- type fields struct {
- path string
- }
type args struct {
id string
as []string
}
tests := []struct {
name string
- fields fields
- args args
+ fields []string
+ args []args
+ want string
wantErr bool
}{
- // TODO: Add test cases.
+ {"add one new to new DB",
+ []string{},
+ []args{args{"1", []string{"a@b.c"}}},
+ "1\ta@b.c\n",
+ false},
+ {"add several new to new DB",
+ []string{},
+ []args{
+ args{"1", []string{"a@b.c"}},
+ args{"2", []string{"d@e.f", "g@h.i"}},
+ args{"3", []string{"j@k.l"}},
+ },
+ "1\ta@b.c\n2\td@e.f,g@h.i\n3\tj@k.l\n",
+ false},
+ {"add one new to old DB",
+ []string{
+ "A\tz@y.x",
+ "B\tw@v.u",
+ "C\tt@s.r,q@p.o",
+ },
+ []args{args{"1", []string{"a@b.c"}}},
+ "A\tz@y.x\nB\tw@v.u\nC\tt@s.r,q@p.o\n1\ta@b.c\n",
+ false},
+ {"add several new to old DB",
+ []string{
+ "A\tz@y.x",
+ "B\tw@v.u",
+ "C\tt@s.r,q@p.o",
+ },
+ []args{
+ args{"1", []string{"a@b.c"}},
+ args{"2", []string{"d@e.f", "g@h.i"}},
+ args{"3", []string{"j@k.l"}},
+ },
+ "A\tz@y.x\nB\tw@v.u\nC\tt@s.r,q@p.o\n1\ta@b.c\n2\td@e.f,g@h.i\n3\tj@k.l\n",
+ false},
+ {"change one",
+ []string{
+ "A\tz@y.x",
+ "1\tw@v.u",
+ "C\tt@s.r,q@p.o",
+ },
+ []args{args{"1", []string{"a@b.c"}}},
+ "A\tz@y.x\n1\ta@b.c\nC\tt@s.r,q@p.o\n",
+ false},
+ {"change several",
+ []string{
+ "A\tz@y.x",
+ "3\tw@v.u",
+ "C\tt@s.r,q@p.o",
+ "2\tw@v.u",
+ },
+ []args{
+ args{"2", []string{"d@e.f", "g@h.i"}},
+ args{"3", []string{"j@k.l"}},
+ },
+ "A\tz@y.x\n3\tj@k.l\nC\tt@s.r,q@p.o\n2\td@e.f,g@h.i\n",
+ false},
+ {"change and add",
+ []string{
+ "A\tz@y.x",
+ "3\tw@v.u",
+ "C\tt@s.r,q@p.o",
+ },
+ []args{
+ args{"2", []string{"a@b.c"}},
+ args{"3", []string{"d@e.f"}},
+ },
+ "A\tz@y.x\n3\td@e.f\nC\tt@s.r,q@p.o\n2\ta@b.c\n",
+ false},
}
+ dbName := "testdb"
+ os.Remove(dbName)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- d := db{
- path: tt.fields.path,
+ // Set up the test DB
+ fout, err := os.Create(dbName)
+ if err != nil {
+ t.Errorf("error setting up db %s", err)
+ }
+ // Clean up test data
+ defer os.Remove(dbName)
+ for _, row := range tt.fields {
+ fout.WriteString(row + "\n")
}
- if err := d.UpdateInvites(tt.args.id, tt.args.as); (err != nil) != tt.wantErr {
- t.Errorf("db.UpdateInvites() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
+ fout.Close()
+ d := OpenDb(dbName)
+ for _, inv := range tt.args {
+ if err := d.UpdateInvites(inv.id, inv.as); (err != nil) != tt.wantErr {
+ t.Errorf("db.UpdateInvites() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
+ }
+ }
+ if got, err := ioutil.ReadFile(dbName); (err != nil) || (string(got) != tt.want) {
+ t.Errorf("db.UpdateInvites() `%s`; error = %v\nwant:\n`%v`\ngot:\n`%v`", tt.name, err, tt.want, string(got))
}
})
}
}
-func Test_db_save(t *testing.T) {
- type fields struct {
- path string
- }
- type args struct {
- rs [][]string
- }
- tests := []struct {
- name string
- fields fields
- args args
- wantErr bool
- }{
- // TODO: Add test cases.
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- d := db{
- path: tt.fields.path,
- }
- if err := d.save(tt.args.rs); (err != nil) != tt.wantErr {
- t.Errorf("db.save() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
- }
- })
- }
-}
-
-func Test_db_readDb(t *testing.T) {
- type fields struct {
- path string
- }
- tests := []struct {
- name string
- fields fields
- want [][]string
- wantErr bool
- }{
- // TODO: Add test cases.
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- d := db{
- path: tt.fields.path,
- }
- got, err := d.readDb()
- if (err != nil) != tt.wantErr {
- t.Errorf("db.readDb() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("db.readDb() %s = %v, want %v", tt.name, got, tt.want)
- }
- })
- }
-}
+// TODO test locking
+// TODO test malformed DB
M main.go +3 -2
@@ 16,6 16,7 @@ var VERSION string = "dev"
// TODO -C -- send only to changed attendees
// TODO -e -- input is an email
+// TODO repair db
func main() {
var send Send
@@ 26,8 27,8 @@ func main() {
return
}
- cal := NewCal(send.CalPath)
- dtb := NewDb(send.DbPath)
+ cal := OpenCal(send.CalPath)
+ dtb := OpenDb(send.DbPath)
if send.Query {
dbs := dtb.Stats()
M main_test.go +6 -14
@@ 9,16 9,8 @@ import (
)
func Test_main(t *testing.T) {
- tests := []struct {
- name string
- }{
- // TODO: Add test cases.
- }
- for range tests {
- t.Run(tt.name, func(t *testing.T) {
- main()
- })
- }
+ // EMPTY TEST
+ // Never test this, and never generate it (testmd)
}
func Test_sendInvites(t *testing.T) {
@@ 32,7 24,7 @@ func Test_sendInvites(t *testing.T) {
args args
wantErr bool
}{
- // TODO: Add test cases.
+ // TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ 61,11 53,11 @@ func TestSend_Metadata(t *testing.T) {
fields fields
want map[string]flag.Flag
}{
- // TODO: Add test cases.
+ // TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- t := &Send{
+ ts := &Send{
One: tt.fields.One,
Version: tt.fields.Version,
Query: tt.fields.Query,
@@ 77,7 69,7 @@ func TestSend_Metadata(t *testing.T) {
CalPath: tt.fields.CalPath,
MailInput: tt.fields.MailInput,
}
- if got := t.Metadata(); !reflect.DeepEqual(got, tt.want) {
+ if got := ts.Metadata(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Send.Metadata() %s = %v, want %v", tt.name, got, tt.want)
}
})