diff --git a/db_test.go b/db_test.go new file mode 100644 index 0000000..a6b6731 --- /dev/null +++ b/db_test.go @@ -0,0 +1,107 @@ +package litestream_test + +import ( + "database/sql" + "os" + "path/filepath" + "testing" + "time" + + "github.com/benbjohnson/litestream" +) + +// Ensure we can check the last modified time of the real database and its WAL. +func TestDB_UpdatedAt(t *testing.T) { + t.Run("ErrNotExist", func(t *testing.T) { + db := MustOpenDB(t) + defer MustCloseDB(t, db) + if _, err := db.UpdatedAt(); !os.IsNotExist(err) { + t.Fatalf("unexpected error: %#v", err) + } + }) + + t.Run("DB", func(t *testing.T) { + db, sqldb := MustOpenDBs(t) + defer MustCloseDBs(t, db, sqldb) + + if t0, err := db.UpdatedAt(); err != nil { + t.Fatal(err) + } else if time.Since(t0) > 10*time.Second { + t.Fatalf("unexpected updated at time: %s", t0) + } + }) + + t.Run("WAL", func(t *testing.T) { + db, sqldb := MustOpenDBs(t) + defer MustCloseDBs(t, db, sqldb) + + t0, err := db.UpdatedAt() + if err != nil { + t.Fatal(err) + } + + if _, err := sqldb.Exec(`CREATE TABLE t (id INT);`); err != nil { + t.Fatal(err) + } + + if t1, err := db.UpdatedAt(); err != nil { + t.Fatal(err) + } else if !t1.After(t0) { + t.Fatalf("expected newer updated at time: %s > %s", t1, t0) + } + }) +} + +// MustOpenDBs returns a new instance of a DB & associated SQL DB. +func MustOpenDBs(tb testing.TB) (*litestream.DB, *sql.DB) { + db := MustOpenDB(tb) + return db, MustOpenSQLDB(tb, db.Path()) +} + +// MustCloseDBs closes db & sqldb and removes the parent directory. +func MustCloseDBs(tb testing.TB, db *litestream.DB, sqldb *sql.DB) { + MustCloseDB(tb, db) + MustCloseSQLDB(tb, sqldb) +} + +// MustOpenDB returns a new instance of a DB. +func MustOpenDB(tb testing.TB) *litestream.DB { + tb.Helper() + + dir := tb.TempDir() + db := litestream.NewDB(filepath.Join(dir, "db")) + if err := db.Open(); err != nil { + tb.Fatal(err) + } + return db +} + +// MustCloseDB closes db and removes its parent directory. +func MustCloseDB(tb testing.TB, db *litestream.DB) { + tb.Helper() + if err := db.Close(); err != nil { + tb.Fatal(err) + } else if err := os.RemoveAll(filepath.Dir(db.Path())); err != nil { + tb.Fatal(err) + } +} + +// MustOpenSQLDB returns a database/sql DB. +func MustOpenSQLDB(tb testing.TB, path string) *sql.DB { + tb.Helper() + d, err := sql.Open("sqlite3", path) + if err != nil { + tb.Fatal(err) + } else if _, err := d.Exec(`PRAGMA journal_mode = wal;`); err != nil { + tb.Fatal(err) + } + return d +} + +// MustCloseSQLDB closes a database/sql DB. +func MustCloseSQLDB(tb testing.TB, d *sql.DB) { + tb.Helper() + if err := d.Close(); err != nil { + tb.Fatal(err) + } +} diff --git a/litestream.go b/litestream.go index 8b4d8a4..828b94f 100644 --- a/litestream.go +++ b/litestream.go @@ -4,7 +4,6 @@ import ( "compress/gzip" "database/sql" "encoding/binary" - "encoding/hex" "errors" "fmt" "io" @@ -256,41 +255,6 @@ func (r *gzipReadCloser) Close() error { return r.closer.Close() } -// HexDump returns hexdump output but with duplicate lines removed. -func HexDump(b []byte) string { - const prefixN = len("00000000") - - var output []string - var prev string - var ellipsis bool - - lines := strings.Split(strings.TrimSpace(hex.Dump(b)), "\n") - for i, line := range lines { - // Add line to output if it is not repeating or the last line. - if i == 0 || i == len(lines)-1 || trimPrefixN(line, prefixN) != trimPrefixN(prev, prefixN) { - output = append(output, line) - prev, ellipsis = line, false - continue - } - - // Add an ellipsis for the first duplicate line. - if !ellipsis { - output = append(output, "...") - ellipsis = true - continue - } - } - - return strings.Join(output, "\n") -} - -func trimPrefixN(s string, n int) string { - if len(s) < n { - return "" - } - return s[n:] -} - func assert(condition bool, message string) { if !condition { panic("assertion failed: " + message)