# HG changeset patch # User Sean E. Russell # Date 1624388038 18000 # Tue Jun 22 13:53:58 2021 -0500 # Node ID a19d67a3308641b2f8eecc00a0a47467e2225362 # Parent 5ba7395b2665d24eb4e0f594876f20374891eda3 Add function to process several records at once diff --git a/db.go b/db.go --- a/db.go +++ b/db.go @@ -17,6 +17,7 @@ UpdateInvites(string, []string) error Invited(string, string) (bool, error) Error() error + ForEachRecord(func(row []string)) error } type DbStats struct { @@ -26,6 +27,7 @@ type db struct { path string err error + lockV *flock.Flock } func OpenDb(path string) Db { @@ -60,25 +62,35 @@ return d.err } +// ForEachRecord calls a supplied function for each row in the database. If the +// function returns false, the row is not persisted. +func (d db) ForEachRecord(cb func (row []string)) error { + err := d.lock() + if err != nil { + return err + } + defer d.unlock() + + rs, err := d.readDb() + if err != nil { + return err + } + for _, r := range rs { + cb(r) + } + return d.save(rs) +} + // 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 { sas := strings.Join(as, ",") // Lock the database - ln := d.path + ".lock" - lock := flock.New(ln) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - success, err := lock.TryLockContext(ctx, 100*time.Millisecond) - if !success { - msg := fmt.Sprintf("Database appears to be locked by another process. Try removing %s\n", ln) - return errors.New(msg) - } + err := d.lock() if err != nil { return err } - defer lock.Unlock() - defer os.Remove(lock.Path()) + defer d.unlock() rs, err := d.readDb() if err != nil { @@ -107,20 +119,11 @@ func (d db) Invited(id, email string) (bool, error) { // Lock the database - ln := d.path + ".lock" - lock := flock.New(ln) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - success, err := lock.TryLockContext(ctx, 100*time.Millisecond) - if !success { - msg := fmt.Sprintf("Database appears to be locked by another process. Try removing %s\n", ln) - return false, errors.New(msg) - } + err := d.lock() if err != nil { return false, err } - defer lock.Unlock() - defer os.Remove(lock.Path()) + defer d.unlock() rs, err := d.readDb() if err != nil { @@ -142,6 +145,27 @@ return false, nil } +func (d *db) lock() error { + ln := d.path + ".lock" + d.lockV = flock.New(ln) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + success, err := d.lockV.TryLockContext(ctx, 100*time.Millisecond) + if !success { + msg := fmt.Sprintf("Database appears to be locked by another process. Try removing %s\n", d.lockV.Path()) + return errors.New(msg) + } + return err +} + +func (d *db) unlock() { + d.lockV.Unlock() + os.Remove(d.lockV.Path()) + d.lockV = nil +} + +// save persists rows of data, overwriting any existing data. It expects the +// database to be already locked. func (d db) save(rs [][]string) error { // Write the DB back out foutn := d.path + ".tmp" @@ -161,6 +185,8 @@ return nil } +// readDb reads data out of a database. It expects the database to be already +// locked. func (d db) readDb() ([][]string, error) { // Read the DB fin, err := os.Open(d.path) diff --git a/db_test.go b/db_test.go --- a/db_test.go +++ b/db_test.go @@ -4,6 +4,9 @@ "io/ioutil" "os" "reflect" + "sort" + "strconv" + "strings" "testing" ) @@ -41,6 +44,31 @@ } } +var lines []string = []string{ + "0\ta@b.c", + "1\tw@v.u", + "2\tt@s.r,q@p.o", + "3\tq@r.s,a@b.c,d@e.f", + "4\tz@y.x", + "5\ta@b.c", + "6\ta@b.c", + } + +func setupTestDb(lns []string) (name string, err error) { + var rv string + for _, l := range lns { + rv += l + "\n" + } + dbName := "testdb" + fout, err := os.Create(dbName) + if err != nil { + return "", err + } + fout.WriteString(rv) + fout.Close() + return dbName, nil +} + func Test_db_UpdateInvites(t *testing.T) { type args struct { id string @@ -48,80 +76,74 @@ } tests := []struct { name string - fields []string + initDb []string args []args - want string + want []string wantErr bool }{ {"add one new to new DB", []string{}, - []args{args{"1", []string{"a@b.c"}}}, - "1\ta@b.c\n", + []args{args{"0", []string{"a@b.c"}}}, + lines[0:1], false}, - {"add several new to new DB", + {"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"}}, + args{"0", []string{"a@b.c"}}, + args{"1", []string{"w@v.u"}}, + args{"2", []string{"t@s.r,q@p.o"}}, }, - "1\ta@b.c\n2\td@e.f,g@h.i\n3\tj@k.l\n", + lines[0:3], 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", + lines[1:7], + []args{args{"0", []string{"a@b.c"}}}, + lines, false}, {"add several new to old DB", - []string{ - "A\tz@y.x", - "B\tw@v.u", - "C\tt@s.r,q@p.o", - }, + lines[3:7], []args{ - args{"1", []string{"a@b.c"}}, - args{"2", []string{"d@e.f", "g@h.i"}}, - args{"3", []string{"j@k.l"}}, + args{"0", []string{"a@b.c"}}, + args{"1", []string{"w@v.u"}}, + args{"2", []string{"t@s.r,q@p.o"}}, }, - "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", + lines, + false}, + {"add several new to old DB, no repeat", + lines[2:7], + []args{ + args{"0", []string{"a@b.c"}}, + args{"1", []string{"w@v.u"}}, + args{"2", []string{"t@s.r,q@p.o"}}, + }, + lines, false}, {"change one", + lines, + []args{args{"1", []string{"x@y.z"}}}, []string{ - "A\tz@y.x", - "1\tw@v.u", - "C\tt@s.r,q@p.o", + lines[0], "1\tx@y.z", lines[2],lines[3],lines[4],lines[5],lines[6], }, - []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", - }, + lines[:5], []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", + []string{ + lines[0], lines[1], "2\td@e.f,g@h.i", "3\tj@k.l",lines[4], + }, false}, {"change and add", - []string{ - "A\tz@y.x", - "3\tw@v.u", - "C\tt@s.r,q@p.o", + lines[:3], + []args{ + args{"1", []string{"x@y.z"}}, + args{"3", []string{"j@k.l"}}, }, - []args{ - args{"2", []string{"a@b.c"}}, - args{"3", []string{"d@e.f"}}, + []string{ + lines[0], "1\tx@y.z",lines[2],"3\tj@k.l", }, - "A\tz@y.x\n3\td@e.f\nC\tt@s.r,q@p.o\n2\ta@b.c\n", false}, } dbName := "testdb" @@ -129,35 +151,43 @@ for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Set up the test DB - fout, err := os.Create(dbName) + dbName, err := setupTestDb(tt.initDb) 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") - } - 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)) + got, err := ioutil.ReadFile(dbName) + if (err != nil) { + t.Errorf("`%s`: error = %v", tt.name, err) + } + gots := strings.Split(strings.TrimSpace(string(got)), "\n") + if len(gots) > len(tt.want) { + t.Errorf("`%s`; wants: %v, gots: %v", tt.name, tt.want, gots) + } + sort.Strings(gots) + sort.Strings(tt.want) + for i, g := range gots { + if g != tt.want[i] { + t.Errorf("db.UpdateInvites() `%s`; want: %v, got: %v\ntt: %+v", tt.name, tt.want, gots, tt) + break + } } }) } } -// TODO test locking +// TODO test locking (ensure locking works) // TODO test malformed DB func TestInvited(t *testing.T) { type args struct { - id string + id string email string } tests := []struct { @@ -166,23 +196,19 @@ want bool wantErr bool }{ - {"yes invited one", args{"2", "w@v.u"}, true, false}, + {"yes invited one", args{"4", "z@y.x"}, true, false}, {"not invited", args{"2", "a@b.c"}, false, false}, - {"yes invited many", args{"3", "q@p.o"}, true, false}, + {"yes invited many", args{"3", "d@e.f"}, true, false}, } // Set up the test DB - dbName := "testdb" - fout, err := os.Create(dbName) + dbName, err := setupTestDb(lines) if err != nil { t.Errorf("error setting up db %s", err) } - // Clean up test data defer os.Remove(dbName) - fout.WriteString("1\tz@y.x\n2\tw@v.u\n3\tt@s.r,q@p.o\n4\tq@r.s,a@b.c,d@e.f\n") - fout.Close() + d := OpenDb(dbName) - d := OpenDb(dbName) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := d.Invited(tt.args.id, tt.args.email) @@ -196,3 +222,46 @@ }) } } + + +func Test_db_ForEachRecord(t *testing.T) { + var count int + var success bool + tests := []struct { + name string + cb func([]string) + wantErr bool + wantTest func() bool + }{ + {"count", func(row []string){ count++ }, false, func() bool { return count == len(lines) }}, + {"index is id", func(row []string){ + id, err := strconv.Atoi(row[0]) + success = success && err == nil && id == count + count++ + }, false, func() bool { + return success + }}, + + } + + // Set up the test DB + dbName, err := setupTestDb(lines) + if err != nil { + t.Errorf("error setting up db %s", err) + } + defer os.Remove(dbName) + d := OpenDb(dbName) + + for _, tt := range tests { + t.Run (tt.name, func(t *testing.T) { + count = 0 + success = true + if err := d.ForEachRecord(tt.cb); (err != nil) != tt.wantErr { + t.Errorf("db.ForEachRecord() %s error = %v, wantErr %v", tt.name, err, tt.wantErr) + } + if !tt.wantTest() { + t.Errorf("%s test failed", tt.name) + } + }) + } +}