| package bolt_test |
| |
| import ( |
| "bytes" |
| "encoding/binary" |
| "errors" |
| "flag" |
| "fmt" |
| "hash/fnv" |
| "io/ioutil" |
| "log" |
| "os" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "sort" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| "unsafe" |
| |
| "github.com/boltdb/bolt" |
| ) |
| |
| var statsFlag = flag.Bool("stats", false, "show performance stats") |
| |
| // version is the data file format version. |
| const version = 2 |
| |
| // magic is the marker value to indicate that a file is a Bolt DB. |
| const magic uint32 = 0xED0CDAED |
| |
| // pageSize is the size of one page in the data file. |
| const pageSize = 4096 |
| |
| // pageHeaderSize is the size of a page header. |
| const pageHeaderSize = 16 |
| |
| // meta represents a simplified version of a database meta page for testing. |
| type meta struct { |
| magic uint32 |
| version uint32 |
| _ uint32 |
| _ uint32 |
| _ [16]byte |
| _ uint64 |
| _ uint64 |
| _ uint64 |
| checksum uint64 |
| } |
| |
| // Ensure that a database can be opened without error. |
| func TestOpen(t *testing.T) { |
| path := tempfile() |
| db, err := bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } else if db == nil { |
| t.Fatal("expected db") |
| } |
| |
| if s := db.Path(); s != path { |
| t.Fatalf("unexpected path: %s", s) |
| } |
| |
| if err := db.Close(); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure that opening a database with a blank path returns an error. |
| func TestOpen_ErrPathRequired(t *testing.T) { |
| _, err := bolt.Open("", 0666, nil) |
| if err == nil { |
| t.Fatalf("expected error") |
| } |
| } |
| |
| // Ensure that opening a database with a bad path returns an error. |
| func TestOpen_ErrNotExists(t *testing.T) { |
| _, err := bolt.Open(filepath.Join(tempfile(), "bad-path"), 0666, nil) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| } |
| |
| // Ensure that opening a file with wrong checksum returns ErrChecksum. |
| func TestOpen_ErrChecksum(t *testing.T) { |
| buf := make([]byte, pageSize) |
| meta := (*meta)(unsafe.Pointer(&buf[0])) |
| meta.magic = magic |
| meta.version = version |
| meta.checksum = 123 |
| |
| path := tempfile() |
| f, err := os.Create(path) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if _, err := f.WriteAt(buf, pageHeaderSize); err != nil { |
| t.Fatal(err) |
| } |
| if err := f.Close(); err != nil { |
| t.Fatal(err) |
| } |
| defer os.Remove(path) |
| |
| if _, err := bolt.Open(path, 0666, nil); err != bolt.ErrChecksum { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| } |
| |
| // Ensure that opening a file that is not a Bolt database returns ErrInvalid. |
| func TestOpen_ErrInvalid(t *testing.T) { |
| path := tempfile() |
| |
| f, err := os.Create(path) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if _, err := fmt.Fprintln(f, "this is not a bolt database"); err != nil { |
| t.Fatal(err) |
| } |
| if err := f.Close(); err != nil { |
| t.Fatal(err) |
| } |
| defer os.Remove(path) |
| |
| if _, err := bolt.Open(path, 0666, nil); err != bolt.ErrInvalid { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| } |
| |
| // Ensure that opening a file created with a different version of Bolt returns |
| // ErrVersionMismatch. |
| func TestOpen_ErrVersionMismatch(t *testing.T) { |
| buf := make([]byte, pageSize) |
| meta := (*meta)(unsafe.Pointer(&buf[0])) |
| meta.magic = magic |
| meta.version = version + 100 |
| |
| path := tempfile() |
| f, err := os.Create(path) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if _, err := f.WriteAt(buf, pageHeaderSize); err != nil { |
| t.Fatal(err) |
| } |
| if err := f.Close(); err != nil { |
| t.Fatal(err) |
| } |
| defer os.Remove(path) |
| |
| if _, err := bolt.Open(path, 0666, nil); err != bolt.ErrVersionMismatch { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| } |
| |
| // Ensure that opening an already open database file will timeout. |
| func TestOpen_Timeout(t *testing.T) { |
| if runtime.GOOS == "solaris" { |
| t.Skip("solaris fcntl locks don't support intra-process locking") |
| } |
| |
| path := tempfile() |
| |
| // Open a data file. |
| db0, err := bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } else if db0 == nil { |
| t.Fatal("expected database") |
| } |
| |
| // Attempt to open the database again. |
| start := time.Now() |
| db1, err := bolt.Open(path, 0666, &bolt.Options{Timeout: 100 * time.Millisecond}) |
| if err != bolt.ErrTimeout { |
| t.Fatalf("unexpected timeout: %s", err) |
| } else if db1 != nil { |
| t.Fatal("unexpected database") |
| } else if time.Since(start) <= 100*time.Millisecond { |
| t.Fatal("expected to wait at least timeout duration") |
| } |
| |
| if err := db0.Close(); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure that opening an already open database file will wait until its closed. |
| func TestOpen_Wait(t *testing.T) { |
| if runtime.GOOS == "solaris" { |
| t.Skip("solaris fcntl locks don't support intra-process locking") |
| } |
| |
| path := tempfile() |
| |
| // Open a data file. |
| db0, err := bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Close it in just a bit. |
| time.AfterFunc(100*time.Millisecond, func() { _ = db0.Close() }) |
| |
| // Attempt to open the database again. |
| start := time.Now() |
| db1, err := bolt.Open(path, 0666, &bolt.Options{Timeout: 200 * time.Millisecond}) |
| if err != nil { |
| t.Fatal(err) |
| } else if time.Since(start) <= 100*time.Millisecond { |
| t.Fatal("expected to wait at least timeout duration") |
| } |
| |
| if err := db1.Close(); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure that opening a database does not increase its size. |
| // https://github.com/boltdb/bolt/issues/291 |
| func TestOpen_Size(t *testing.T) { |
| // Open a data file. |
| db := MustOpenDB() |
| path := db.Path() |
| defer db.MustClose() |
| |
| pagesize := db.Info().PageSize |
| |
| // Insert until we get above the minimum 4MB size. |
| if err := db.Update(func(tx *bolt.Tx) error { |
| b, _ := tx.CreateBucketIfNotExists([]byte("data")) |
| for i := 0; i < 10000; i++ { |
| if err := b.Put([]byte(fmt.Sprintf("%04d", i)), make([]byte, 1000)); err != nil { |
| t.Fatal(err) |
| } |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Close database and grab the size. |
| if err := db.DB.Close(); err != nil { |
| t.Fatal(err) |
| } |
| sz := fileSize(path) |
| if sz == 0 { |
| t.Fatalf("unexpected new file size: %d", sz) |
| } |
| |
| // Reopen database, update, and check size again. |
| db0, err := bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := db0.Update(func(tx *bolt.Tx) error { |
| if err := tx.Bucket([]byte("data")).Put([]byte{0}, []byte{0}); err != nil { |
| t.Fatal(err) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| if err := db0.Close(); err != nil { |
| t.Fatal(err) |
| } |
| newSz := fileSize(path) |
| if newSz == 0 { |
| t.Fatalf("unexpected new file size: %d", newSz) |
| } |
| |
| // Compare the original size with the new size. |
| // db size might increase by a few page sizes due to the new small update. |
| if sz < newSz-5*int64(pagesize) { |
| t.Fatalf("unexpected file growth: %d => %d", sz, newSz) |
| } |
| } |
| |
| // Ensure that opening a database beyond the max step size does not increase its size. |
| // https://github.com/boltdb/bolt/issues/303 |
| func TestOpen_Size_Large(t *testing.T) { |
| if testing.Short() { |
| t.Skip("short mode") |
| } |
| |
| // Open a data file. |
| db := MustOpenDB() |
| path := db.Path() |
| defer db.MustClose() |
| |
| pagesize := db.Info().PageSize |
| |
| // Insert until we get above the minimum 4MB size. |
| var index uint64 |
| for i := 0; i < 10000; i++ { |
| if err := db.Update(func(tx *bolt.Tx) error { |
| b, _ := tx.CreateBucketIfNotExists([]byte("data")) |
| for j := 0; j < 1000; j++ { |
| if err := b.Put(u64tob(index), make([]byte, 50)); err != nil { |
| t.Fatal(err) |
| } |
| index++ |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Close database and grab the size. |
| if err := db.DB.Close(); err != nil { |
| t.Fatal(err) |
| } |
| sz := fileSize(path) |
| if sz == 0 { |
| t.Fatalf("unexpected new file size: %d", sz) |
| } else if sz < (1 << 30) { |
| t.Fatalf("expected larger initial size: %d", sz) |
| } |
| |
| // Reopen database, update, and check size again. |
| db0, err := bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := db0.Update(func(tx *bolt.Tx) error { |
| return tx.Bucket([]byte("data")).Put([]byte{0}, []byte{0}) |
| }); err != nil { |
| t.Fatal(err) |
| } |
| if err := db0.Close(); err != nil { |
| t.Fatal(err) |
| } |
| |
| newSz := fileSize(path) |
| if newSz == 0 { |
| t.Fatalf("unexpected new file size: %d", newSz) |
| } |
| |
| // Compare the original size with the new size. |
| // db size might increase by a few page sizes due to the new small update. |
| if sz < newSz-5*int64(pagesize) { |
| t.Fatalf("unexpected file growth: %d => %d", sz, newSz) |
| } |
| } |
| |
| // Ensure that a re-opened database is consistent. |
| func TestOpen_Check(t *testing.T) { |
| path := tempfile() |
| |
| db, err := bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := db.View(func(tx *bolt.Tx) error { return <-tx.Check() }); err != nil { |
| t.Fatal(err) |
| } |
| if err := db.Close(); err != nil { |
| t.Fatal(err) |
| } |
| |
| db, err = bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := db.View(func(tx *bolt.Tx) error { return <-tx.Check() }); err != nil { |
| t.Fatal(err) |
| } |
| if err := db.Close(); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure that write errors to the meta file handler during initialization are returned. |
| func TestOpen_MetaInitWriteError(t *testing.T) { |
| t.Skip("pending") |
| } |
| |
| // Ensure that a database that is too small returns an error. |
| func TestOpen_FileTooSmall(t *testing.T) { |
| path := tempfile() |
| |
| db, err := bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := db.Close(); err != nil { |
| t.Fatal(err) |
| } |
| |
| // corrupt the database |
| if err := os.Truncate(path, int64(os.Getpagesize())); err != nil { |
| t.Fatal(err) |
| } |
| |
| db, err = bolt.Open(path, 0666, nil) |
| if err == nil || err.Error() != "file size too small" { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| } |
| |
| // Ensure that a database can be opened in read-only mode by multiple processes |
| // and that a database can not be opened in read-write mode and in read-only |
| // mode at the same time. |
| func TestOpen_ReadOnly(t *testing.T) { |
| if runtime.GOOS == "solaris" { |
| t.Skip("solaris fcntl locks don't support intra-process locking") |
| } |
| |
| bucket, key, value := []byte(`bucket`), []byte(`key`), []byte(`value`) |
| |
| path := tempfile() |
| |
| // Open in read-write mode. |
| db, err := bolt.Open(path, 0666, nil) |
| if err != nil { |
| t.Fatal(err) |
| } else if db.IsReadOnly() { |
| t.Fatal("db should not be in read only mode") |
| } |
| if err := db.Update(func(tx *bolt.Tx) error { |
| b, err := tx.CreateBucket(bucket) |
| if err != nil { |
| return err |
| } |
| if err := b.Put(key, value); err != nil { |
| t.Fatal(err) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| if err := db.Close(); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Open in read-only mode. |
| db0, err := bolt.Open(path, 0666, &bolt.Options{ReadOnly: true}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Opening in read-write mode should return an error. |
| if _, err = bolt.Open(path, 0666, &bolt.Options{Timeout: time.Millisecond * 100}); err == nil { |
| t.Fatal("expected error") |
| } |
| |
| // And again (in read-only mode). |
| db1, err := bolt.Open(path, 0666, &bolt.Options{ReadOnly: true}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Verify both read-only databases are accessible. |
| for _, db := range []*bolt.DB{db0, db1} { |
| // Verify is is in read only mode indeed. |
| if !db.IsReadOnly() { |
| t.Fatal("expected read only mode") |
| } |
| |
| // Read-only databases should not allow updates. |
| if err := db.Update(func(*bolt.Tx) error { |
| panic(`should never get here`) |
| }); err != bolt.ErrDatabaseReadOnly { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| |
| // Read-only databases should not allow beginning writable txns. |
| if _, err := db.Begin(true); err != bolt.ErrDatabaseReadOnly { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| |
| // Verify the data. |
| if err := db.View(func(tx *bolt.Tx) error { |
| b := tx.Bucket(bucket) |
| if b == nil { |
| return fmt.Errorf("expected bucket `%s`", string(bucket)) |
| } |
| |
| got := string(b.Get(key)) |
| expected := string(value) |
| if got != expected { |
| return fmt.Errorf("expected `%s`, got `%s`", expected, got) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| if err := db0.Close(); err != nil { |
| t.Fatal(err) |
| } |
| if err := db1.Close(); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // TestDB_Open_InitialMmapSize tests if having InitialMmapSize large enough |
| // to hold data from concurrent write transaction resolves the issue that |
| // read transaction blocks the write transaction and causes deadlock. |
| // This is a very hacky test since the mmap size is not exposed. |
| func TestDB_Open_InitialMmapSize(t *testing.T) { |
| path := tempfile() |
| defer os.Remove(path) |
| |
| initMmapSize := 1 << 31 // 2GB |
| testWriteSize := 1 << 27 // 134MB |
| |
| db, err := bolt.Open(path, 0666, &bolt.Options{InitialMmapSize: initMmapSize}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // create a long-running read transaction |
| // that never gets closed while writing |
| rtx, err := db.Begin(false) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // create a write transaction |
| wtx, err := db.Begin(true) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| b, err := wtx.CreateBucket([]byte("test")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // and commit a large write |
| err = b.Put([]byte("foo"), make([]byte, testWriteSize)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| done := make(chan struct{}) |
| |
| go func() { |
| if err := wtx.Commit(); err != nil { |
| t.Fatal(err) |
| } |
| done <- struct{}{} |
| }() |
| |
| select { |
| case <-time.After(5 * time.Second): |
| t.Errorf("unexpected that the reader blocks writer") |
| case <-done: |
| } |
| |
| if err := rtx.Rollback(); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure that a database cannot open a transaction when it's not open. |
| func TestDB_Begin_ErrDatabaseNotOpen(t *testing.T) { |
| var db bolt.DB |
| if _, err := db.Begin(false); err != bolt.ErrDatabaseNotOpen { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| } |
| |
| // Ensure that a read-write transaction can be retrieved. |
| func TestDB_BeginRW(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| tx, err := db.Begin(true) |
| if err != nil { |
| t.Fatal(err) |
| } else if tx == nil { |
| t.Fatal("expected tx") |
| } |
| |
| if tx.DB() != db.DB { |
| t.Fatal("unexpected tx database") |
| } else if !tx.Writable() { |
| t.Fatal("expected writable tx") |
| } |
| |
| if err := tx.Commit(); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure that opening a transaction while the DB is closed returns an error. |
| func TestDB_BeginRW_Closed(t *testing.T) { |
| var db bolt.DB |
| if _, err := db.Begin(true); err != bolt.ErrDatabaseNotOpen { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| } |
| |
| func TestDB_Close_PendingTx_RW(t *testing.T) { testDB_Close_PendingTx(t, true) } |
| func TestDB_Close_PendingTx_RO(t *testing.T) { testDB_Close_PendingTx(t, false) } |
| |
| // Ensure that a database cannot close while transactions are open. |
| func testDB_Close_PendingTx(t *testing.T, writable bool) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| // Start transaction. |
| tx, err := db.Begin(true) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Open update in separate goroutine. |
| done := make(chan struct{}) |
| go func() { |
| if err := db.Close(); err != nil { |
| t.Fatal(err) |
| } |
| close(done) |
| }() |
| |
| // Ensure database hasn't closed. |
| time.Sleep(100 * time.Millisecond) |
| select { |
| case <-done: |
| t.Fatal("database closed too early") |
| default: |
| } |
| |
| // Commit transaction. |
| if err := tx.Commit(); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Ensure database closed now. |
| time.Sleep(100 * time.Millisecond) |
| select { |
| case <-done: |
| default: |
| t.Fatal("database did not close") |
| } |
| } |
| |
| // Ensure a database can provide a transactional block. |
| func TestDB_Update(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| if err := db.Update(func(tx *bolt.Tx) error { |
| b, err := tx.CreateBucket([]byte("widgets")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := b.Put([]byte("foo"), []byte("bar")); err != nil { |
| t.Fatal(err) |
| } |
| if err := b.Put([]byte("baz"), []byte("bat")); err != nil { |
| t.Fatal(err) |
| } |
| if err := b.Delete([]byte("foo")); err != nil { |
| t.Fatal(err) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| if err := db.View(func(tx *bolt.Tx) error { |
| b := tx.Bucket([]byte("widgets")) |
| if v := b.Get([]byte("foo")); v != nil { |
| t.Fatalf("expected nil value, got: %v", v) |
| } |
| if v := b.Get([]byte("baz")); !bytes.Equal(v, []byte("bat")) { |
| t.Fatalf("unexpected value: %v", v) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure a closed database returns an error while running a transaction block |
| func TestDB_Update_Closed(t *testing.T) { |
| var db bolt.DB |
| if err := db.Update(func(tx *bolt.Tx) error { |
| if _, err := tx.CreateBucket([]byte("widgets")); err != nil { |
| t.Fatal(err) |
| } |
| return nil |
| }); err != bolt.ErrDatabaseNotOpen { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| } |
| |
| // Ensure a panic occurs while trying to commit a managed transaction. |
| func TestDB_Update_ManualCommit(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| var panicked bool |
| if err := db.Update(func(tx *bolt.Tx) error { |
| func() { |
| defer func() { |
| if r := recover(); r != nil { |
| panicked = true |
| } |
| }() |
| |
| if err := tx.Commit(); err != nil { |
| t.Fatal(err) |
| } |
| }() |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } else if !panicked { |
| t.Fatal("expected panic") |
| } |
| } |
| |
| // Ensure a panic occurs while trying to rollback a managed transaction. |
| func TestDB_Update_ManualRollback(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| var panicked bool |
| if err := db.Update(func(tx *bolt.Tx) error { |
| func() { |
| defer func() { |
| if r := recover(); r != nil { |
| panicked = true |
| } |
| }() |
| |
| if err := tx.Rollback(); err != nil { |
| t.Fatal(err) |
| } |
| }() |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } else if !panicked { |
| t.Fatal("expected panic") |
| } |
| } |
| |
| // Ensure a panic occurs while trying to commit a managed transaction. |
| func TestDB_View_ManualCommit(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| var panicked bool |
| if err := db.View(func(tx *bolt.Tx) error { |
| func() { |
| defer func() { |
| if r := recover(); r != nil { |
| panicked = true |
| } |
| }() |
| |
| if err := tx.Commit(); err != nil { |
| t.Fatal(err) |
| } |
| }() |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } else if !panicked { |
| t.Fatal("expected panic") |
| } |
| } |
| |
| // Ensure a panic occurs while trying to rollback a managed transaction. |
| func TestDB_View_ManualRollback(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| var panicked bool |
| if err := db.View(func(tx *bolt.Tx) error { |
| func() { |
| defer func() { |
| if r := recover(); r != nil { |
| panicked = true |
| } |
| }() |
| |
| if err := tx.Rollback(); err != nil { |
| t.Fatal(err) |
| } |
| }() |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } else if !panicked { |
| t.Fatal("expected panic") |
| } |
| } |
| |
| // Ensure a write transaction that panics does not hold open locks. |
| func TestDB_Update_Panic(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| // Panic during update but recover. |
| func() { |
| defer func() { |
| if r := recover(); r != nil { |
| t.Log("recover: update", r) |
| } |
| }() |
| |
| if err := db.Update(func(tx *bolt.Tx) error { |
| if _, err := tx.CreateBucket([]byte("widgets")); err != nil { |
| t.Fatal(err) |
| } |
| panic("omg") |
| }); err != nil { |
| t.Fatal(err) |
| } |
| }() |
| |
| // Verify we can update again. |
| if err := db.Update(func(tx *bolt.Tx) error { |
| if _, err := tx.CreateBucket([]byte("widgets")); err != nil { |
| t.Fatal(err) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Verify that our change persisted. |
| if err := db.Update(func(tx *bolt.Tx) error { |
| if tx.Bucket([]byte("widgets")) == nil { |
| t.Fatal("expected bucket") |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure a database can return an error through a read-only transactional block. |
| func TestDB_View_Error(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| if err := db.View(func(tx *bolt.Tx) error { |
| return errors.New("xxx") |
| }); err == nil || err.Error() != "xxx" { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| } |
| |
| // Ensure a read transaction that panics does not hold open locks. |
| func TestDB_View_Panic(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| if err := db.Update(func(tx *bolt.Tx) error { |
| if _, err := tx.CreateBucket([]byte("widgets")); err != nil { |
| t.Fatal(err) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Panic during view transaction but recover. |
| func() { |
| defer func() { |
| if r := recover(); r != nil { |
| t.Log("recover: view", r) |
| } |
| }() |
| |
| if err := db.View(func(tx *bolt.Tx) error { |
| if tx.Bucket([]byte("widgets")) == nil { |
| t.Fatal("expected bucket") |
| } |
| panic("omg") |
| }); err != nil { |
| t.Fatal(err) |
| } |
| }() |
| |
| // Verify that we can still use read transactions. |
| if err := db.View(func(tx *bolt.Tx) error { |
| if tx.Bucket([]byte("widgets")) == nil { |
| t.Fatal("expected bucket") |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure that DB stats can be returned. |
| func TestDB_Stats(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| if err := db.Update(func(tx *bolt.Tx) error { |
| _, err := tx.CreateBucket([]byte("widgets")) |
| return err |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| stats := db.Stats() |
| if stats.TxStats.PageCount != 2 { |
| t.Fatalf("unexpected TxStats.PageCount: %d", stats.TxStats.PageCount) |
| } else if stats.FreePageN != 0 { |
| t.Fatalf("unexpected FreePageN != 0: %d", stats.FreePageN) |
| } else if stats.PendingPageN != 2 { |
| t.Fatalf("unexpected PendingPageN != 2: %d", stats.PendingPageN) |
| } |
| } |
| |
| // Ensure that database pages are in expected order and type. |
| func TestDB_Consistency(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| if err := db.Update(func(tx *bolt.Tx) error { |
| _, err := tx.CreateBucket([]byte("widgets")) |
| return err |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| for i := 0; i < 10; i++ { |
| if err := db.Update(func(tx *bolt.Tx) error { |
| if err := tx.Bucket([]byte("widgets")).Put([]byte("foo"), []byte("bar")); err != nil { |
| t.Fatal(err) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| if err := db.Update(func(tx *bolt.Tx) error { |
| if p, _ := tx.Page(0); p == nil { |
| t.Fatal("expected page") |
| } else if p.Type != "meta" { |
| t.Fatalf("unexpected page type: %s", p.Type) |
| } |
| |
| if p, _ := tx.Page(1); p == nil { |
| t.Fatal("expected page") |
| } else if p.Type != "meta" { |
| t.Fatalf("unexpected page type: %s", p.Type) |
| } |
| |
| if p, _ := tx.Page(2); p == nil { |
| t.Fatal("expected page") |
| } else if p.Type != "free" { |
| t.Fatalf("unexpected page type: %s", p.Type) |
| } |
| |
| if p, _ := tx.Page(3); p == nil { |
| t.Fatal("expected page") |
| } else if p.Type != "free" { |
| t.Fatalf("unexpected page type: %s", p.Type) |
| } |
| |
| if p, _ := tx.Page(4); p == nil { |
| t.Fatal("expected page") |
| } else if p.Type != "leaf" { |
| t.Fatalf("unexpected page type: %s", p.Type) |
| } |
| |
| if p, _ := tx.Page(5); p == nil { |
| t.Fatal("expected page") |
| } else if p.Type != "freelist" { |
| t.Fatalf("unexpected page type: %s", p.Type) |
| } |
| |
| if p, _ := tx.Page(6); p != nil { |
| t.Fatal("unexpected page") |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure that DB stats can be subtracted from one another. |
| func TestDBStats_Sub(t *testing.T) { |
| var a, b bolt.Stats |
| a.TxStats.PageCount = 3 |
| a.FreePageN = 4 |
| b.TxStats.PageCount = 10 |
| b.FreePageN = 14 |
| diff := b.Sub(&a) |
| if diff.TxStats.PageCount != 7 { |
| t.Fatalf("unexpected TxStats.PageCount: %d", diff.TxStats.PageCount) |
| } |
| |
| // free page stats are copied from the receiver and not subtracted |
| if diff.FreePageN != 14 { |
| t.Fatalf("unexpected FreePageN: %d", diff.FreePageN) |
| } |
| } |
| |
| // Ensure two functions can perform updates in a single batch. |
| func TestDB_Batch(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| if err := db.Update(func(tx *bolt.Tx) error { |
| if _, err := tx.CreateBucket([]byte("widgets")); err != nil { |
| t.Fatal(err) |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Iterate over multiple updates in separate goroutines. |
| n := 2 |
| ch := make(chan error) |
| for i := 0; i < n; i++ { |
| go func(i int) { |
| ch <- db.Batch(func(tx *bolt.Tx) error { |
| return tx.Bucket([]byte("widgets")).Put(u64tob(uint64(i)), []byte{}) |
| }) |
| }(i) |
| } |
| |
| // Check all responses to make sure there's no error. |
| for i := 0; i < n; i++ { |
| if err := <-ch; err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure data is correct. |
| if err := db.View(func(tx *bolt.Tx) error { |
| b := tx.Bucket([]byte("widgets")) |
| for i := 0; i < n; i++ { |
| if v := b.Get(u64tob(uint64(i))); v == nil { |
| t.Errorf("key not found: %d", i) |
| } |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func TestDB_Batch_Panic(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| |
| var sentinel int |
| var bork = &sentinel |
| var problem interface{} |
| var err error |
| |
| // Execute a function inside a batch that panics. |
| func() { |
| defer func() { |
| if p := recover(); p != nil { |
| problem = p |
| } |
| }() |
| err = db.Batch(func(tx *bolt.Tx) error { |
| panic(bork) |
| }) |
| }() |
| |
| // Verify there is no error. |
| if g, e := err, error(nil); g != e { |
| t.Fatalf("wrong error: %v != %v", g, e) |
| } |
| // Verify the panic was captured. |
| if g, e := problem, bork; g != e { |
| t.Fatalf("wrong error: %v != %v", g, e) |
| } |
| } |
| |
| func TestDB_BatchFull(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| if err := db.Update(func(tx *bolt.Tx) error { |
| _, err := tx.CreateBucket([]byte("widgets")) |
| return err |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| const size = 3 |
| // buffered so we never leak goroutines |
| ch := make(chan error, size) |
| put := func(i int) { |
| ch <- db.Batch(func(tx *bolt.Tx) error { |
| return tx.Bucket([]byte("widgets")).Put(u64tob(uint64(i)), []byte{}) |
| }) |
| } |
| |
| db.MaxBatchSize = size |
| // high enough to never trigger here |
| db.MaxBatchDelay = 1 * time.Hour |
| |
| go put(1) |
| go put(2) |
| |
| // Give the batch a chance to exhibit bugs. |
| time.Sleep(10 * time.Millisecond) |
| |
| // not triggered yet |
| select { |
| case <-ch: |
| t.Fatalf("batch triggered too early") |
| default: |
| } |
| |
| go put(3) |
| |
| // Check all responses to make sure there's no error. |
| for i := 0; i < size; i++ { |
| if err := <-ch; err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure data is correct. |
| if err := db.View(func(tx *bolt.Tx) error { |
| b := tx.Bucket([]byte("widgets")) |
| for i := 1; i <= size; i++ { |
| if v := b.Get(u64tob(uint64(i))); v == nil { |
| t.Errorf("key not found: %d", i) |
| } |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func TestDB_BatchTime(t *testing.T) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| if err := db.Update(func(tx *bolt.Tx) error { |
| _, err := tx.CreateBucket([]byte("widgets")) |
| return err |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| const size = 1 |
| // buffered so we never leak goroutines |
| ch := make(chan error, size) |
| put := func(i int) { |
| ch <- db.Batch(func(tx *bolt.Tx) error { |
| return tx.Bucket([]byte("widgets")).Put(u64tob(uint64(i)), []byte{}) |
| }) |
| } |
| |
| db.MaxBatchSize = 1000 |
| db.MaxBatchDelay = 0 |
| |
| go put(1) |
| |
| // Batch must trigger by time alone. |
| |
| // Check all responses to make sure there's no error. |
| for i := 0; i < size; i++ { |
| if err := <-ch; err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Ensure data is correct. |
| if err := db.View(func(tx *bolt.Tx) error { |
| b := tx.Bucket([]byte("widgets")) |
| for i := 1; i <= size; i++ { |
| if v := b.Get(u64tob(uint64(i))); v == nil { |
| t.Errorf("key not found: %d", i) |
| } |
| } |
| return nil |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func ExampleDB_Update() { |
| // Open the database. |
| db, err := bolt.Open(tempfile(), 0666, nil) |
| if err != nil { |
| log.Fatal(err) |
| } |
| defer os.Remove(db.Path()) |
| |
| // Execute several commands within a read-write transaction. |
| if err := db.Update(func(tx *bolt.Tx) error { |
| b, err := tx.CreateBucket([]byte("widgets")) |
| if err != nil { |
| return err |
| } |
| if err := b.Put([]byte("foo"), []byte("bar")); err != nil { |
| return err |
| } |
| return nil |
| }); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Read the value back from a separate read-only transaction. |
| if err := db.View(func(tx *bolt.Tx) error { |
| value := tx.Bucket([]byte("widgets")).Get([]byte("foo")) |
| fmt.Printf("The value of 'foo' is: %s\n", value) |
| return nil |
| }); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Close database to release the file lock. |
| if err := db.Close(); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Output: |
| // The value of 'foo' is: bar |
| } |
| |
| func ExampleDB_View() { |
| // Open the database. |
| db, err := bolt.Open(tempfile(), 0666, nil) |
| if err != nil { |
| log.Fatal(err) |
| } |
| defer os.Remove(db.Path()) |
| |
| // Insert data into a bucket. |
| if err := db.Update(func(tx *bolt.Tx) error { |
| b, err := tx.CreateBucket([]byte("people")) |
| if err != nil { |
| return err |
| } |
| if err := b.Put([]byte("john"), []byte("doe")); err != nil { |
| return err |
| } |
| if err := b.Put([]byte("susy"), []byte("que")); err != nil { |
| return err |
| } |
| return nil |
| }); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Access data from within a read-only transactional block. |
| if err := db.View(func(tx *bolt.Tx) error { |
| v := tx.Bucket([]byte("people")).Get([]byte("john")) |
| fmt.Printf("John's last name is %s.\n", v) |
| return nil |
| }); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Close database to release the file lock. |
| if err := db.Close(); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Output: |
| // John's last name is doe. |
| } |
| |
| func ExampleDB_Begin_ReadOnly() { |
| // Open the database. |
| db, err := bolt.Open(tempfile(), 0666, nil) |
| if err != nil { |
| log.Fatal(err) |
| } |
| defer os.Remove(db.Path()) |
| |
| // Create a bucket using a read-write transaction. |
| if err := db.Update(func(tx *bolt.Tx) error { |
| _, err := tx.CreateBucket([]byte("widgets")) |
| return err |
| }); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Create several keys in a transaction. |
| tx, err := db.Begin(true) |
| if err != nil { |
| log.Fatal(err) |
| } |
| b := tx.Bucket([]byte("widgets")) |
| if err := b.Put([]byte("john"), []byte("blue")); err != nil { |
| log.Fatal(err) |
| } |
| if err := b.Put([]byte("abby"), []byte("red")); err != nil { |
| log.Fatal(err) |
| } |
| if err := b.Put([]byte("zephyr"), []byte("purple")); err != nil { |
| log.Fatal(err) |
| } |
| if err := tx.Commit(); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Iterate over the values in sorted key order. |
| tx, err = db.Begin(false) |
| if err != nil { |
| log.Fatal(err) |
| } |
| c := tx.Bucket([]byte("widgets")).Cursor() |
| for k, v := c.First(); k != nil; k, v = c.Next() { |
| fmt.Printf("%s likes %s\n", k, v) |
| } |
| |
| if err := tx.Rollback(); err != nil { |
| log.Fatal(err) |
| } |
| |
| if err := db.Close(); err != nil { |
| log.Fatal(err) |
| } |
| |
| // Output: |
| // abby likes red |
| // john likes blue |
| // zephyr likes purple |
| } |
| |
| func BenchmarkDBBatchAutomatic(b *testing.B) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| if err := db.Update(func(tx *bolt.Tx) error { |
| _, err := tx.CreateBucket([]byte("bench")) |
| return err |
| }); err != nil { |
| b.Fatal(err) |
| } |
| |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| start := make(chan struct{}) |
| var wg sync.WaitGroup |
| |
| for round := 0; round < 1000; round++ { |
| wg.Add(1) |
| |
| go func(id uint32) { |
| defer wg.Done() |
| <-start |
| |
| h := fnv.New32a() |
| buf := make([]byte, 4) |
| binary.LittleEndian.PutUint32(buf, id) |
| _, _ = h.Write(buf[:]) |
| k := h.Sum(nil) |
| insert := func(tx *bolt.Tx) error { |
| b := tx.Bucket([]byte("bench")) |
| return b.Put(k, []byte("filler")) |
| } |
| if err := db.Batch(insert); err != nil { |
| b.Error(err) |
| return |
| } |
| }(uint32(round)) |
| } |
| close(start) |
| wg.Wait() |
| } |
| |
| b.StopTimer() |
| validateBatchBench(b, db) |
| } |
| |
| func BenchmarkDBBatchSingle(b *testing.B) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| if err := db.Update(func(tx *bolt.Tx) error { |
| _, err := tx.CreateBucket([]byte("bench")) |
| return err |
| }); err != nil { |
| b.Fatal(err) |
| } |
| |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| start := make(chan struct{}) |
| var wg sync.WaitGroup |
| |
| for round := 0; round < 1000; round++ { |
| wg.Add(1) |
| go func(id uint32) { |
| defer wg.Done() |
| <-start |
| |
| h := fnv.New32a() |
| buf := make([]byte, 4) |
| binary.LittleEndian.PutUint32(buf, id) |
| _, _ = h.Write(buf[:]) |
| k := h.Sum(nil) |
| insert := func(tx *bolt.Tx) error { |
| b := tx.Bucket([]byte("bench")) |
| return b.Put(k, []byte("filler")) |
| } |
| if err := db.Update(insert); err != nil { |
| b.Error(err) |
| return |
| } |
| }(uint32(round)) |
| } |
| close(start) |
| wg.Wait() |
| } |
| |
| b.StopTimer() |
| validateBatchBench(b, db) |
| } |
| |
| func BenchmarkDBBatchManual10x100(b *testing.B) { |
| db := MustOpenDB() |
| defer db.MustClose() |
| if err := db.Update(func(tx *bolt.Tx) error { |
| _, err := tx.CreateBucket([]byte("bench")) |
| return err |
| }); err != nil { |
| b.Fatal(err) |
| } |
| |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| start := make(chan struct{}) |
| var wg sync.WaitGroup |
| |
| for major := 0; major < 10; major++ { |
| wg.Add(1) |
| go func(id uint32) { |
| defer wg.Done() |
| <-start |
| |
| insert100 := func(tx *bolt.Tx) error { |
| h := fnv.New32a() |
| buf := make([]byte, 4) |
| for minor := uint32(0); minor < 100; minor++ { |
| binary.LittleEndian.PutUint32(buf, uint32(id*100+minor)) |
| h.Reset() |
| _, _ = h.Write(buf[:]) |
| k := h.Sum(nil) |
| b := tx.Bucket([]byte("bench")) |
| if err := b.Put(k, []byte("filler")); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| if err := db.Update(insert100); err != nil { |
| b.Fatal(err) |
| } |
| }(uint32(major)) |
| } |
| close(start) |
| wg.Wait() |
| } |
| |
| b.StopTimer() |
| validateBatchBench(b, db) |
| } |
| |
| func validateBatchBench(b *testing.B, db *DB) { |
| var rollback = errors.New("sentinel error to cause rollback") |
| validate := func(tx *bolt.Tx) error { |
| bucket := tx.Bucket([]byte("bench")) |
| h := fnv.New32a() |
| buf := make([]byte, 4) |
| for id := uint32(0); id < 1000; id++ { |
| binary.LittleEndian.PutUint32(buf, id) |
| h.Reset() |
| _, _ = h.Write(buf[:]) |
| k := h.Sum(nil) |
| v := bucket.Get(k) |
| if v == nil { |
| b.Errorf("not found id=%d key=%x", id, k) |
| continue |
| } |
| if g, e := v, []byte("filler"); !bytes.Equal(g, e) { |
| b.Errorf("bad value for id=%d key=%x: %s != %q", id, k, g, e) |
| } |
| if err := bucket.Delete(k); err != nil { |
| return err |
| } |
| } |
| // should be empty now |
| c := bucket.Cursor() |
| for k, v := c.First(); k != nil; k, v = c.Next() { |
| b.Errorf("unexpected key: %x = %q", k, v) |
| } |
| return rollback |
| } |
| if err := db.Update(validate); err != nil && err != rollback { |
| b.Error(err) |
| } |
| } |
| |
| // DB is a test wrapper for bolt.DB. |
| type DB struct { |
| *bolt.DB |
| } |
| |
| // MustOpenDB returns a new, open DB at a temporary location. |
| func MustOpenDB() *DB { |
| db, err := bolt.Open(tempfile(), 0666, nil) |
| if err != nil { |
| panic(err) |
| } |
| return &DB{db} |
| } |
| |
| // Close closes the database and deletes the underlying file. |
| func (db *DB) Close() error { |
| // Log statistics. |
| if *statsFlag { |
| db.PrintStats() |
| } |
| |
| // Check database consistency after every test. |
| db.MustCheck() |
| |
| // Close database and remove file. |
| defer os.Remove(db.Path()) |
| return db.DB.Close() |
| } |
| |
| // MustClose closes the database and deletes the underlying file. Panic on error. |
| func (db *DB) MustClose() { |
| if err := db.Close(); err != nil { |
| panic(err) |
| } |
| } |
| |
| // PrintStats prints the database stats |
| func (db *DB) PrintStats() { |
| var stats = db.Stats() |
| fmt.Printf("[db] %-20s %-20s %-20s\n", |
| fmt.Sprintf("pg(%d/%d)", stats.TxStats.PageCount, stats.TxStats.PageAlloc), |
| fmt.Sprintf("cur(%d)", stats.TxStats.CursorCount), |
| fmt.Sprintf("node(%d/%d)", stats.TxStats.NodeCount, stats.TxStats.NodeDeref), |
| ) |
| fmt.Printf(" %-20s %-20s %-20s\n", |
| fmt.Sprintf("rebal(%d/%v)", stats.TxStats.Rebalance, truncDuration(stats.TxStats.RebalanceTime)), |
| fmt.Sprintf("spill(%d/%v)", stats.TxStats.Spill, truncDuration(stats.TxStats.SpillTime)), |
| fmt.Sprintf("w(%d/%v)", stats.TxStats.Write, truncDuration(stats.TxStats.WriteTime)), |
| ) |
| } |
| |
| // MustCheck runs a consistency check on the database and panics if any errors are found. |
| func (db *DB) MustCheck() { |
| if err := db.Update(func(tx *bolt.Tx) error { |
| // Collect all the errors. |
| var errors []error |
| for err := range tx.Check() { |
| errors = append(errors, err) |
| if len(errors) > 10 { |
| break |
| } |
| } |
| |
| // If errors occurred, copy the DB and print the errors. |
| if len(errors) > 0 { |
| var path = tempfile() |
| if err := tx.CopyFile(path, 0600); err != nil { |
| panic(err) |
| } |
| |
| // Print errors. |
| fmt.Print("\n\n") |
| fmt.Printf("consistency check failed (%d errors)\n", len(errors)) |
| for _, err := range errors { |
| fmt.Println(err) |
| } |
| fmt.Println("") |
| fmt.Println("db saved to:") |
| fmt.Println(path) |
| fmt.Print("\n\n") |
| os.Exit(-1) |
| } |
| |
| return nil |
| }); err != nil && err != bolt.ErrDatabaseNotOpen { |
| panic(err) |
| } |
| } |
| |
| // CopyTempFile copies a database to a temporary file. |
| func (db *DB) CopyTempFile() { |
| path := tempfile() |
| if err := db.View(func(tx *bolt.Tx) error { |
| return tx.CopyFile(path, 0600) |
| }); err != nil { |
| panic(err) |
| } |
| fmt.Println("db copied to: ", path) |
| } |
| |
| // tempfile returns a temporary file path. |
| func tempfile() string { |
| f, err := ioutil.TempFile("", "bolt-") |
| if err != nil { |
| panic(err) |
| } |
| if err := f.Close(); err != nil { |
| panic(err) |
| } |
| if err := os.Remove(f.Name()); err != nil { |
| panic(err) |
| } |
| return f.Name() |
| } |
| |
| // mustContainKeys checks that a bucket contains a given set of keys. |
| func mustContainKeys(b *bolt.Bucket, m map[string]string) { |
| found := make(map[string]string) |
| if err := b.ForEach(func(k, _ []byte) error { |
| found[string(k)] = "" |
| return nil |
| }); err != nil { |
| panic(err) |
| } |
| |
| // Check for keys found in bucket that shouldn't be there. |
| var keys []string |
| for k, _ := range found { |
| if _, ok := m[string(k)]; !ok { |
| keys = append(keys, k) |
| } |
| } |
| if len(keys) > 0 { |
| sort.Strings(keys) |
| panic(fmt.Sprintf("keys found(%d): %s", len(keys), strings.Join(keys, ","))) |
| } |
| |
| // Check for keys not found in bucket that should be there. |
| for k, _ := range m { |
| if _, ok := found[string(k)]; !ok { |
| keys = append(keys, k) |
| } |
| } |
| if len(keys) > 0 { |
| sort.Strings(keys) |
| panic(fmt.Sprintf("keys not found(%d): %s", len(keys), strings.Join(keys, ","))) |
| } |
| } |
| |
| func trunc(b []byte, length int) []byte { |
| if length < len(b) { |
| return b[:length] |
| } |
| return b |
| } |
| |
| func truncDuration(d time.Duration) string { |
| return regexp.MustCompile(`^(\d+)(\.\d+)`).ReplaceAllString(d.String(), "$1") |
| } |
| |
| func fileSize(path string) int64 { |
| fi, err := os.Stat(path) |
| if err != nil { |
| return 0 |
| } |
| return fi.Size() |
| } |
| |
| func warn(v ...interface{}) { fmt.Fprintln(os.Stderr, v...) } |
| func warnf(msg string, v ...interface{}) { fmt.Fprintf(os.Stderr, msg+"\n", v...) } |
| |
| // u64tob converts a uint64 into an 8-byte slice. |
| func u64tob(v uint64) []byte { |
| b := make([]byte, 8) |
| binary.BigEndian.PutUint64(b, v) |
| return b |
| } |
| |
| // btou64 converts an 8-byte slice into an uint64. |
| func btou64(b []byte) uint64 { return binary.BigEndian.Uint64(b) } |