# HG changeset patch # User Sean E. Russell # Date 1624311743 18000 # Mon Jun 21 16:42:23 2021 -0500 # Node ID 15192bcf2f9486eba6ccef5406ae7d4a4bb5da85 # Parent 77e280226726cd8471ca13cdff7eb2f2b137ac45 Unit tests and some consequent fixes diff --git a/cal.go b/cal.go --- a/cal.go +++ b/cal.go @@ -11,14 +11,14 @@ 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) 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 @@ } return nil }) + return err } diff --git a/cal_test.go b/cal_test.go --- a/cal_test.go +++ b/cal_test.go @@ -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` diff --git a/db.go b/db.go --- a/db.go +++ b/db.go @@ -14,22 +14,35 @@ 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) 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 @@ return err } defer lock.Unlock() + defer os.Remove(lock.Path()) rs, err := d.readDb() if err != nil { @@ -69,6 +87,8 @@ for _, r := range rs { if r[0] == id { r[1] = sas + found = true + break } } if !found { @@ -92,11 +112,13 @@ } 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 } diff --git a/db_test.go b/db_test.go --- a/db_test.go +++ b/db_test.go @@ -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_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 diff --git a/main.go b/main.go --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ // 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 @@ return } - cal := NewCal(send.CalPath) - dtb := NewDb(send.DbPath) + cal := OpenCal(send.CalPath) + dtb := OpenDb(send.DbPath) if send.Query { dbs := dtb.Stats() diff --git a/main_test.go b/main_test.go --- a/main_test.go +++ b/main_test.go @@ -9,16 +9,8 @@ ) 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 @@ 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 @@ 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 @@ 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) } })