internal/store/faultydb_test.go
Rama main · réplica de lectura del repositorio original.
package store
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"io"
"testing"
"time"
)
var errQueryFailed = errors.New("the database went away mid-read")
type faults struct {
columns int
values []driver.Value
execsPass int
beginFails bool
commitErr error
closeErr error
readErr error
}
type faultyConnector struct {
arm faults
execs *int
closes *int
}
func (c faultyConnector) Connect(context.Context) (driver.Conn, error) {
return faultyConn(c), nil
}
func (c faultyConnector) Driver() driver.Driver {
return faultyDriver{}
}
type faultyDriver struct{}
func (faultyDriver) Open(string) (driver.Conn, error) {
return faultyConn{}, nil
}
type faultyConn struct {
arm faults
execs *int
closes *int
}
func (c faultyConn) Prepare(string) (driver.Stmt, error) {
return faultyStmt{arm: c.arm, execs: c.execs}, nil
}
func (c faultyConn) Close() error {
if c.closes != nil {
*c.closes++
}
return c.arm.closeErr
}
func (c faultyConn) Begin() (driver.Tx, error) {
if c.arm.beginFails {
return nil, errQueryFailed
}
return faultyTx{arm: c.arm}, nil
}
type faultyTx struct {
arm faults
}
func (t faultyTx) Commit() error {
return t.arm.commitErr
}
func (faultyTx) Rollback() error {
return nil
}
type faultyStmt struct {
arm faults
execs *int
}
func (faultyStmt) Close() error {
return nil
}
func (faultyStmt) NumInput() int {
return -1
}
func (s faultyStmt) Exec([]driver.Value) (driver.Result, error) {
*s.execs++
if *s.execs <= s.arm.execsPass {
return driver.RowsAffected(1), nil
}
return nil, errQueryFailed
}
func (s faultyStmt) Query([]driver.Value) (driver.Rows, error) {
return &faultyRows{columns: s.arm.columns, values: s.arm.values, readErr: s.arm.readErr}, nil
}
type faultyRows struct {
columns int
values []driver.Value
served bool
readErr error
}
func (r *faultyRows) Columns() []string {
names := make([]string, r.columns)
for at := range names {
names[at] = "c"
}
return names
}
func (*faultyRows) Close() error {
return nil
}
func (r *faultyRows) Next(dest []driver.Value) error {
if r.values == nil {
return errQueryFailed
}
if r.served {
if r.readErr != nil {
return r.readErr
}
return io.EOF
}
r.served = true
copy(dest, r.values)
return nil
}
func faultyDB(t *testing.T, arm faults) *sql.DB {
t.Helper()
execs := 0
db := sql.OpenDB(faultyConnector{arm: arm, execs: &execs})
t.Cleanup(func() { _ = db.Close() })
return db
}
func faultyStore(t *testing.T, columns int, values []driver.Value) *Store {
t.Helper()
db := faultyDB(t, faults{columns: columns, values: values})
return &Store{writes: db, reads: db}
}
func TestAWriteThatFailsIsPassedUp(t *testing.T) {
store := faultyStore(t, 0, nil)
tabs := store.Tabs()
ctx := context.Background()
cases := map[string]func() error{
"remember": func() error { return tabs.Remember(ctx, Tab{ID: "one"}) },
"forget": func() error { return tabs.Forget(ctx, "one") },
"recolor": func() error { return tabs.Recolor(ctx, "one", 3) },
"rename": func() error { return tabs.Rename(ctx, "one", "label", "group") },
"reopening": func() error { return tabs.Reopening(ctx, "one", true) },
"recording": func() error { return tabs.Recording(ctx, "one", "pods") },
"record": func() error { return store.record(ctx, Entry{Name: "web"}) },
"forgetAll": func() error { return store.Forget(ctx, "one") },
"prune": func() error { return store.Prune(ctx, Retention{Days: 7}, time.Unix(1700000000, 0)) },
}
for name, call := range cases {
t.Run(name, func(t *testing.T) {
err := call()
if !errors.Is(err, errQueryFailed) {
t.Fatalf("err = %v, want the driver failure passed up", err)
}
})
}
}
func TestAReadThatBreaksPartWayThroughIsReported(t *testing.T) {
ctx := context.Background()
cases := map[string]struct {
columns int
read func(*Store) error
}{
"tabs": {columns: 9, read: func(s *Store) error {
_, err := s.Tabs().All(ctx)
return err
}},
"entries": {columns: 13, read: func(s *Store) error {
_, err := s.Recent(ctx, Query{})
return err
}},
"changes": {columns: 12, read: func(s *Store) error {
_, err := s.Changed(ctx, Query{Cluster: "one"})
return err
}},
}
for name, one := range cases {
t.Run(name, func(t *testing.T) {
err := one.read(faultyStore(t, one.columns, nil))
if !errors.Is(err, errQueryFailed) {
t.Fatalf("err = %v, want the read failure reported", err)
}
})
}
}
func TestARowThatWillNotScanIsReported(t *testing.T) {
ctx := context.Background()
cases := map[string]struct {
columns int
values []driver.Value
read func(*Store) error
}{
"tabs": {
columns: 9, values: []driver.Value{"id", "ctx", "path", "not a number", int64(0), "", "", false, ""},
read: func(s *Store) error {
_, err := s.Tabs().All(ctx)
return err
},
},
"entries": {
columns: 13, values: []driver.Value{int64(1), "one", "not a number", "get", "", "", "", "", "", "web", "", "done", ""},
read: func(s *Store) error {
_, err := s.Recent(ctx, Query{})
return err
},
},
"changes": {
columns: 12, values: []driver.Value{int64(1), "one", "not a number", "get", "", "", "", "", "", "web", "uid", ""},
read: func(s *Store) error {
_, err := s.Changed(ctx, Query{Cluster: "one"})
return err
},
},
}
for name, one := range cases {
t.Run(name, func(t *testing.T) {
err := one.read(faultyStore(t, one.columns, one.values))
if err == nil {
t.Fatal("a row that would not scan was reported as read")
}
})
}
}
func TestForgettingAClusterReportsTheSecondWriteFailing(t *testing.T) {
store := &Store{writes: faultyDB(t, faults{execsPass: 1}), reads: nil}
err := store.Forget(context.Background(), "one")
if !errors.Is(err, errQueryFailed) {
t.Fatalf("err = %v, want the second delete's failure", err)
}
}
func TestForgettingAClusterReportsTheCommitFailing(t *testing.T) {
store := &Store{writes: faultyDB(t, faults{execsPass: 2, commitErr: errQueryFailed})}
err := store.Forget(t.Context(), "one")
if !errors.Is(err, errQueryFailed) {
t.Fatalf("err = %v, want the commit failure", err)
}
}
func TestCappingRowsReportsTheDeleteFailing(t *testing.T) {
db := faultyDB(t, faults{columns: 1, values: []driver.Value{int64(42)}})
store := &Store{writes: db, reads: db}
err := store.Prune(context.Background(), Retention{Rows: 10}, time.Unix(1700000000, 0))
if !errors.Is(err, errQueryFailed) {
t.Fatalf("err = %v, want the cut-off delete's failure", err)
}
}
func TestNotingChangesReportsWhatTheTransactionDid(t *testing.T) {
change := Change{Cluster: "one", Verb: "update", Name: "web", Cells: []string{"web", "1/1"}}
ctx := context.Background()
cases := map[string]faults{
"the transaction would not start": {beginFails: true},
"a row would not insert": {},
"the commit failed": {execsPass: 1, commitErr: errQueryFailed},
}
for name, arm := range cases {
t.Run(name, func(t *testing.T) {
store := &Store{writes: faultyDB(t, arm)}
err := store.note(ctx, []Change{change})
if !errors.Is(err, errQueryFailed) {
t.Fatalf("err = %v, want %s reported", err, name)
}
})
}
}
func TestApplyingAMigrationReportsWhatTheTransactionDid(t *testing.T) {
ctx := context.Background()
cases := map[string]faults{
"the transaction would not start": {beginFails: true},
"the statements would not run": {},
"the version would not stick": {execsPass: 1},
"the commit failed": {execsPass: 2, commitErr: errQueryFailed},
}
for name, arm := range cases {
t.Run(name, func(t *testing.T) {
err := apply(ctx, faultyDB(t, arm), "CREATE TABLE t (a)", 1)
if !errors.Is(err, errQueryFailed) {
t.Fatalf("err = %v, want %s reported", err, name)
}
})
}
}
func TestClosingReportsADatabaseThatWouldNotClose(t *testing.T) {
readCloses := 0
writeCloses := 0
reads := sql.OpenDB(faultyConnector{
arm: faults{closeErr: errQueryFailed}, execs: new(int), closes: &readCloses,
})
writes := sql.OpenDB(faultyConnector{execs: new(int), closes: &writeCloses})
if err := reads.Ping(); err != nil {
t.Fatalf("open reads: %v", err)
}
if err := writes.Ping(); err != nil {
t.Fatalf("open writes: %v", err)
}
store := &Store{writes: writes, reads: reads}
err := store.Close()
if !errors.Is(err, errQueryFailed) {
t.Fatalf("err = %v, want the close failure passed up", err)
}
if readCloses != 1 || writeCloses != 1 {
t.Fatalf("close calls = reads %d, writes %d, want both pools closed", readCloses, writeCloses)
}
}