1
0
mirror of https://github.com/taigrr/bitcask synced 2025-01-18 04:03:17 -08:00

Improves Test Coverage by covering error cases (#95)

* Add Unit  Test for testing a corrupted config

* Add Unit Test for testing errors from .Stats()

* Refactor  Datafile into an interface and add Unit Tests for testing Merge() errors

* Refactor indexer into an interface and add Unit Tests for .Close() errors

* Add Unit Tests for .Delete() errors

* Add Unit Tests for  testing Put/Get errors

* Add Unit Test for testing Open errors (bad path for example)

* Refactor out bitcask.writeConfig

* Add more tests for config errors

* Add unit test for options that might error

* Add more test cases for close errors

* Add test case for rotating datafiles

* Fix a possible data race in .Stats()

* Add test case for checksum errors

* Add test case for Sync errors with Put and WithSync enabled

* Refactor and use testify.mock for mocks and generate mocks for all interfaces

* Refactor TestCloseErrors

* Refactored TestDeleteErrors

* Refactored TestGetErrors

* Refactored TestPutErrors

* Refactored TestMergeErrors and fixed a bug with .Fold()

* Add test case for Scan() errors

* Apparently only Scan() can return nil Node()s?
This commit is contained in:
James Mills
2019-09-09 07:18:38 +10:00
committed by GitHub
parent 13e35b7acc
commit d59d5ad8c2
11 changed files with 756 additions and 82 deletions

View File

@@ -3,7 +3,7 @@ package config
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"os"
)
// Config contains the bitcask configuration parameters
@@ -14,11 +14,11 @@ type Config struct {
Sync bool `json:"sync"`
}
// Decode decodes a serialized configuration
func Decode(path string) (*Config, error) {
// Load loads a configuration from the given path
func Load(path string) (*Config, error) {
var cfg Config
data, err := ioutil.ReadFile(filepath.Join(path, "config.json"))
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
@@ -30,7 +30,25 @@ func Decode(path string) (*Config, error) {
return &cfg, nil
}
// Encode encodes the configuration for storage
func (c *Config) Encode() ([]byte, error) {
return json.Marshal(c)
// Save saves the configuration to the provided path
func (c *Config) Save(path string) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return err
}
data, err := json.Marshal(c)
if err != nil {
return err
}
if _, err = f.Write(data); err != nil {
return err
}
if err = f.Sync(); err != nil {
return err
}
return f.Close()
}

View File

@@ -22,7 +22,18 @@ var (
mxMemPool sync.RWMutex
)
type Datafile struct {
type Datafile interface {
FileID() int
Name() string
Close() error
Sync() error
Size() int64
Read() (internal.Entry, int64, error)
ReadAt(index, size int64) (internal.Entry, error)
Write(internal.Entry) (int64, int64, error)
}
type datafile struct {
sync.RWMutex
id int
@@ -34,7 +45,7 @@ type Datafile struct {
enc *Encoder
}
func NewDatafile(path string, id int, readonly bool) (*Datafile, error) {
func NewDatafile(path string, id int, readonly bool) (Datafile, error) {
var (
r *os.File
ra *mmap.ReaderAt
@@ -70,7 +81,7 @@ func NewDatafile(path string, id int, readonly bool) (*Datafile, error) {
dec := NewDecoder(r)
enc := NewEncoder(w)
return &Datafile{
return &datafile{
id: id,
r: r,
ra: ra,
@@ -81,21 +92,21 @@ func NewDatafile(path string, id int, readonly bool) (*Datafile, error) {
}, nil
}
func (df *Datafile) FileID() int {
func (df *datafile) FileID() int {
return df.id
}
func (df *Datafile) Name() string {
func (df *datafile) Name() string {
return df.r.Name()
}
func (df *Datafile) Close() error {
func (df *datafile) Close() error {
defer func() {
df.ra.Close()
df.r.Close()
}()
// Readonly Datafile -- Nothing further to close on the write side
// Readonly datafile -- Nothing further to close on the write side
if df.w == nil {
return nil
}
@@ -107,20 +118,20 @@ func (df *Datafile) Close() error {
return df.w.Close()
}
func (df *Datafile) Sync() error {
func (df *datafile) Sync() error {
if df.w == nil {
return nil
}
return df.w.Sync()
}
func (df *Datafile) Size() int64 {
func (df *datafile) Size() int64 {
df.RLock()
defer df.RUnlock()
return df.offset
}
func (df *Datafile) Read() (e internal.Entry, n int64, err error) {
func (df *datafile) Read() (e internal.Entry, n int64, err error) {
df.Lock()
defer df.Unlock()
@@ -132,7 +143,7 @@ func (df *Datafile) Read() (e internal.Entry, n int64, err error) {
return
}
func (df *Datafile) ReadAt(index, size int64) (e internal.Entry, err error) {
func (df *datafile) ReadAt(index, size int64) (e internal.Entry, err error) {
var n int
b := make([]byte, size)
@@ -156,7 +167,7 @@ func (df *Datafile) ReadAt(index, size int64) (e internal.Entry, err error) {
return
}
func (df *Datafile) Write(e internal.Entry) (int64, int64, error) {
func (df *datafile) Write(e internal.Entry) (int64, int64, error) {
if df.w == nil {
return -1, 0, ErrReadonly
}

View File

@@ -108,8 +108,7 @@ func readIndex(r io.Reader, t art.Tree, maxKeySize int) error {
return nil
}
// WriteIndex persists a Tree into a io.Writer
func WriteIndex(t art.Tree, w io.Writer) (err error) {
func writeIndex(t art.Tree, w io.Writer) (err error) {
t.ForEach(func(node art.Node) bool {
err = writeBytes(node.Key(), w)
if err != nil {

View File

@@ -2,26 +2,55 @@ package index
import (
"os"
"path"
art "github.com/plar/go-adaptive-radix-tree"
"github.com/prologic/bitcask/internal"
)
// ReadFromFile reads an index from a persisted file
func ReadFromFile(filePath string, maxKeySize int) (art.Tree, bool, error) {
type Indexer interface {
Load(path string, maxkeySize int) (art.Tree, bool, error)
Save(t art.Tree, path string) error
}
func NewIndexer() Indexer {
return &indexer{}
}
type indexer struct{}
func (i *indexer) Load(path string, maxKeySize int) (art.Tree, bool, error) {
t := art.New()
if !internal.Exists(path.Join(filePath, "index")) {
if !internal.Exists(path) {
return t, false, nil
}
f, err := os.Open(path.Join(filePath, "index"))
f, err := os.Open(path)
if err != nil {
return t, true, err
}
defer f.Close()
if err := readIndex(f, t, maxKeySize); err != nil {
return t, true, err
}
return t, true, nil
}
func (i *indexer) Save(t art.Tree, path string) error {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer f.Close()
if err := writeIndex(t, f); err != nil {
return err
}
if err := f.Sync(); err != nil {
return err
}
return f.Close()
}

158
internal/mocks/datafile.go Normal file
View File

@@ -0,0 +1,158 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import internal "github.com/prologic/bitcask/internal"
import mock "github.com/stretchr/testify/mock"
// Datafile is an autogenerated mock type for the Datafile type
type Datafile struct {
mock.Mock
}
// Close provides a mock function with given fields:
func (_m *Datafile) Close() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// FileID provides a mock function with given fields:
func (_m *Datafile) FileID() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// Name provides a mock function with given fields:
func (_m *Datafile) Name() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Read provides a mock function with given fields:
func (_m *Datafile) Read() (internal.Entry, int64, error) {
ret := _m.Called()
var r0 internal.Entry
if rf, ok := ret.Get(0).(func() internal.Entry); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(internal.Entry)
}
var r1 int64
if rf, ok := ret.Get(1).(func() int64); ok {
r1 = rf()
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func() error); ok {
r2 = rf()
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// ReadAt provides a mock function with given fields: index, size
func (_m *Datafile) ReadAt(index int64, size int64) (internal.Entry, error) {
ret := _m.Called(index, size)
var r0 internal.Entry
if rf, ok := ret.Get(0).(func(int64, int64) internal.Entry); ok {
r0 = rf(index, size)
} else {
r0 = ret.Get(0).(internal.Entry)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(index, size)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Size provides a mock function with given fields:
func (_m *Datafile) Size() int64 {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
return r0
}
// Sync provides a mock function with given fields:
func (_m *Datafile) Sync() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Write provides a mock function with given fields: _a0
func (_m *Datafile) Write(_a0 internal.Entry) (int64, int64, error) {
ret := _m.Called(_a0)
var r0 int64
if rf, ok := ret.Get(0).(func(internal.Entry) int64); ok {
r0 = rf(_a0)
} else {
r0 = ret.Get(0).(int64)
}
var r1 int64
if rf, ok := ret.Get(1).(func(internal.Entry) int64); ok {
r1 = rf(_a0)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(internal.Entry) error); ok {
r2 = rf(_a0)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}

56
internal/mocks/indexer.go Normal file
View File

@@ -0,0 +1,56 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import art "github.com/plar/go-adaptive-radix-tree"
import mock "github.com/stretchr/testify/mock"
// Indexer is an autogenerated mock type for the Indexer type
type Indexer struct {
mock.Mock
}
// Load provides a mock function with given fields: path, maxkeySize
func (_m *Indexer) Load(path string, maxkeySize int) (art.Tree, bool, error) {
ret := _m.Called(path, maxkeySize)
var r0 art.Tree
if rf, ok := ret.Get(0).(func(string, int) art.Tree); ok {
r0 = rf(path, maxkeySize)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(art.Tree)
}
}
var r1 bool
if rf, ok := ret.Get(1).(func(string, int) bool); ok {
r1 = rf(path, maxkeySize)
} else {
r1 = ret.Get(1).(bool)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, int) error); ok {
r2 = rf(path, maxkeySize)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Save provides a mock function with given fields: t, path
func (_m *Indexer) Save(t art.Tree, path string) error {
ret := _m.Called(t, path)
var r0 error
if rf, ok := ret.Get(0).(func(art.Tree, string) error); ok {
r0 = rf(t, path)
} else {
r0 = ret.Error(0)
}
return r0
}