├── .circleci
└── config.yml
├── .deepsource.toml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Gemfile
├── Gemfile.lock
├── LICENSE.md
├── README.md
├── benchmark.rb
├── image.png
├── lib
├── bitcask.rb
└── bitcask
│ ├── disk_store.rb
│ └── serializer.rb
└── spec
├── bitcask
├── disk_store_spec.rb
└── serializer_spec.rb
├── fixtures
├── 1672042848_bitcask.db
└── 1672070284_bitcask.db
└── spec_helper.rb
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | orbs:
3 | codecov: codecov/codecov@3
4 |
5 | jobs:
6 | build:
7 | docker:
8 | - image: cimg/ruby:3.1
9 | steps:
10 | - checkout
11 | - run:
12 | name: Install dependencies
13 | command: bundle install
14 | - run:
15 | name: Run tests and collect coverage
16 | command: bundle exec rspec
17 | - codecov/upload
18 |
19 | workflow:
20 | version: 2.1
21 | build-test:
22 | jobs:
23 | - build
--------------------------------------------------------------------------------
/.deepsource.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | test_patterns = ["spec/**"]
4 |
5 | [[analyzers]]
6 | name = "ruby"
7 | enabled = true
8 |
9 | [[transformers]]
10 | name = "rubocop"
11 | enabled = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | /.config
4 | /coverage/
5 | /InstalledFiles
6 | /pkg/
7 | /spec/reports/
8 | /spec/examples.txt
9 | /test/tmp/
10 | /test/version_tmp/
11 | /tmp/
12 |
13 | # Used by dotenv library to load environment variables.
14 | # .env
15 |
16 | # Ignore Byebug command history file.
17 | .byebug_history
18 |
19 | ## Specific to RubyMotion:
20 | .dat*
21 | .repl_history
22 | build/
23 | *.bridgesupport
24 | build-iPhoneOS/
25 | build-iPhoneSimulator/
26 |
27 | ## Specific to RubyMotion (use of CocoaPods):
28 | #
29 | # We recommend against adding the Pods directory to your .gitignore. However
30 | # you should judge for yourself, the pros and cons are mentioned at:
31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32 | #
33 | # vendor/Pods/
34 |
35 | ## Documentation cache and generated files:
36 | /.yardoc/
37 | /_yardoc/
38 | /doc/
39 | /rdoc/
40 |
41 | ## Environment normalization:
42 | /.bundle/
43 | /vendor/bundle
44 | /lib/bundler/man/
45 |
46 | # for a library or gem, you might want to ignore these files since the code is
47 | # intended to run in multiple environments; otherwise, check them in:
48 | # Gemfile.lock
49 | # .ruby-version
50 | # .ruby-gemset
51 |
52 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53 | .rvmrc
54 |
55 | # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56 | # .rubocop-https?--*
57 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | require:
2 | - rubocop-rspec
3 |
4 | AllCops:
5 | NewCops: enable
6 | Exclude:
7 | - vendor/**/*
8 | - bin/**
9 | TargetRubyVersion: 3.1.1
10 |
11 | RSpec:
12 | Enabled: true # enable rubocop-rspec cops
13 |
14 | Bundler/OrderedGems:
15 | Enabled: true
16 |
17 | Layout/ArrayAlignment:
18 | EnforcedStyle: with_first_element
19 | Enabled: true
20 |
21 | Layout/ArgumentAlignment:
22 | EnforcedStyle: with_first_argument
23 | Enabled: true
24 |
25 | Layout/ClassStructure:
26 | Enabled: true
27 | ExpectedOrder:
28 | - module_inclusion
29 | - constants
30 | - initializer
31 | - public_methods
32 | - protected_methods
33 | - private_methods
34 |
35 | Layout/MultilineMethodCallIndentation:
36 | EnforcedStyle: aligned
37 | Enabled: true
38 |
39 | Layout/ParameterAlignment:
40 | EnforcedStyle: with_first_parameter
41 | Enabled: true
42 |
43 | RSpec/ExampleLength:
44 | Max: 15
45 | CountAsOne:
46 | - array
47 | - hash
48 | - heredoc
49 |
50 | RSpec/MessageSpies:
51 | EnforcedStyle: receive
52 | Enabled: true
53 |
54 | RSpec/MultipleMemoizedHelpers:
55 | Max: 20
56 |
57 | RSpec/MultipleExpectations:
58 | Enabled: false
59 |
60 | RSpec/NamedSubject:
61 | Enabled: false
62 |
63 | RSpec/NestedGroups:
64 | Enabled: true
65 | Max: 8
66 |
67 | Style/BlockDelimiters:
68 | Enabled: true
69 | EnforcedStyle: semantic
70 | Exclude:
71 | - spec/factories/**
72 | - Gemfile
73 |
74 | Style/HashSyntax:
75 | EnforcedShorthandSyntax: never
76 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6 |
7 | gem 'zlib', '~> 3.0'
8 |
9 | group :development, :test do
10 | gem 'benchmark-ips', '~> 2.10'
11 | gem 'faker', '~> 3.1'
12 | gem 'rspec', '~> 3.12'
13 | end
14 |
15 | group :test do
16 | gem 'byebug', '~> 11.1'
17 | gem 'rubocop', '~> 1.41', require: false
18 | gem 'rubocop-rspec', '~> 2.16', require: false
19 | gem 'simplecov', require: false
20 | gem 'simplecov-cobertura', '~> 2.1', require: false
21 | end
22 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | ast (2.4.2)
5 | benchmark-ips (2.10.0)
6 | byebug (11.1.3)
7 | concurrent-ruby (1.1.10)
8 | diff-lcs (1.5.0)
9 | docile (1.4.0)
10 | faker (3.1.0)
11 | i18n (>= 1.8.11, < 2)
12 | i18n (1.12.0)
13 | concurrent-ruby (~> 1.0)
14 | json (2.6.3)
15 | parallel (1.22.1)
16 | parser (3.1.3.0)
17 | ast (~> 2.4.1)
18 | rainbow (3.1.1)
19 | regexp_parser (2.6.1)
20 | rexml (3.2.5)
21 | rspec (3.12.0)
22 | rspec-core (~> 3.12.0)
23 | rspec-expectations (~> 3.12.0)
24 | rspec-mocks (~> 3.12.0)
25 | rspec-core (3.12.0)
26 | rspec-support (~> 3.12.0)
27 | rspec-expectations (3.12.1)
28 | diff-lcs (>= 1.2.0, < 2.0)
29 | rspec-support (~> 3.12.0)
30 | rspec-mocks (3.12.1)
31 | diff-lcs (>= 1.2.0, < 2.0)
32 | rspec-support (~> 3.12.0)
33 | rspec-support (3.12.0)
34 | rubocop (1.41.1)
35 | json (~> 2.3)
36 | parallel (~> 1.10)
37 | parser (>= 3.1.2.1)
38 | rainbow (>= 2.2.2, < 4.0)
39 | regexp_parser (>= 1.8, < 3.0)
40 | rexml (>= 3.2.5, < 4.0)
41 | rubocop-ast (>= 1.23.0, < 2.0)
42 | ruby-progressbar (~> 1.7)
43 | unicode-display_width (>= 1.4.0, < 3.0)
44 | rubocop-ast (1.24.0)
45 | parser (>= 3.1.1.0)
46 | rubocop-rspec (2.16.0)
47 | rubocop (~> 1.33)
48 | ruby-progressbar (1.11.0)
49 | simplecov (0.21.2)
50 | docile (~> 1.1)
51 | simplecov-html (~> 0.11)
52 | simplecov_json_formatter (~> 0.1)
53 | simplecov-cobertura (2.1.0)
54 | rexml
55 | simplecov (~> 0.19)
56 | simplecov-html (0.12.3)
57 | simplecov_json_formatter (0.1.4)
58 | unicode-display_width (2.3.0)
59 | zlib (3.0.0)
60 |
61 | PLATFORMS
62 | ruby
63 |
64 | DEPENDENCIES
65 | benchmark-ips (~> 2.10)
66 | byebug (~> 11.1)
67 | faker (~> 3.1)
68 | rspec (~> 3.12)
69 | rubocop (~> 1.41)
70 | rubocop-rspec (~> 2.16)
71 | simplecov
72 | simplecov-cobertura (~> 2.1)
73 | zlib (~> 3.0)
74 |
75 | BUNDLED WITH
76 | 2.2.3
77 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Dinesh Gowda
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bitcask-rb: A Log-Structured Hash Table for Fast Key/Value Data
2 |
3 | [](https://www.ruby-lang.org/en/)
4 | [](https://rspec.info/)
5 | [](https://github.com/rubocop/rubocop)
6 | [](https://dl.circleci.com/status-badge/redirect/gh/dineshgowda24/bitcask-rb/tree/main)
7 | [](https://codecov.io/gh/dineshgowda24/bitcask-rb)
8 | [](https://deepsource.io/gh/dineshgowda24/bitcask-rb/?ref=repository-badge)
9 |
10 |
11 |
12 | Fast, Persistent key/value store based on [bitcask paper](https://riak.com/assets/bitcask-intro.pdf) written in Ruby.
13 | An attempt to understand and build our persistent key/value store capable of storing data enormous than the RAM. This, in any way, is not intended for production. For simplicity, implementation has ignored a few specifications from the paper.
14 |
15 | ## Prerequisites
16 |
17 | - Ruby
18 | - bundler
19 |
20 | ## Setup
21 |
22 | ```shell
23 | bundle install
24 | ```
25 |
26 | ## Usage
27 |
28 | ```ruby
29 | db_store = Bitcask::DiskStore.new('bitcask.db')
30 |
31 | # Setting values in store using put or hash style
32 | db_store.put("Anime", "One Piece")
33 | db_store["Luffy"] = "Straw Hat"
34 | db_store[2020] = "Leap Year"
35 | db_store.put("pie", 3.1415)
36 |
37 | # Getting values from store using get or hash style
38 | db_store["Anime"]
39 | db_store.get("Luffy")
40 | db_store["pie"]
41 | db_store.get(2020)
42 |
43 | # Listing keys
44 | db_store.keys
45 |
46 | # Size of the store
47 | db_store.store
48 | ```
49 |
50 | ## Tests
51 |
52 | ```shell
53 | bundle exec rspec
54 | ```
55 |
56 | ## Advantages
57 |
58 | - Simple yet powerful
59 | - Easy to use
60 | - Store data enormous than RAM
61 | - Data file is OS agnostic
62 | - Portable
63 | - Faster read/writes
64 | - Minimal dependecies
65 |
66 | ## Features
67 |
68 | | Feature | Support |
69 | |---------------------------------------|--------------------|
70 | | Persisted in disk | :white_check_mark: |
71 | | Get API | :white_check_mark: |
72 | | Put API | :white_check_mark: |
73 | | Int, Float and String for k/v | :white_check_mark: |
74 | | CRC | :white_check_mark: |
75 | | Directory Support | :x: |
76 | | Delete API | :x: |
77 | | Files Merge and LSM Trees | :x: |
78 |
79 | ## Benchmarks
80 |
81 | ### CPU Cycles
82 |
83 | ```ruby
84 | Benchmarked with value_size of 78 bytes
85 | user system total real
86 | DiskStore.put : 10k records 0.026963 0.018983 0.045946 ( 0.046078)
87 | DiskStore.get : 10k records 0.031211 0.009727 0.040938 ( 0.041154)
88 | DiskStore.put : 100k records 0.367399 0.196536 0.563935 ( 0.566659)
89 | DiskStore.get : 100k records 0.313556 0.102338 0.415894 ( 0.416280)
90 | DiskStore.put : 1M records 4.649307 2.209731 6.859038 ( 6.887667)
91 | DiskStore.get : 1M records 3.357120 1.047637 4.404757 ( 4.409732)
92 | avg_put: 0.000005 0.000002 0.000007 ( 0.000007)
93 | avg_get: 0.000003 0.000001 0.000004 ( 0.000004)
94 | ```
95 |
96 | ### Iterations Per Second
97 |
98 | ```ruby
99 | Warming up --------------------------------------
100 | DiskStore.put : 100 records with data size: 702 Bytes
101 | 149.000 i/100ms
102 | DiskStore.get : 100 records, value size: 702 Bytes
103 | 2.389k i/100ms
104 | DiskStore.put : 100 records, value size: 6 Kb
105 | 22.000 i/100ms
106 | DiskStore.get : 100 records, value size: 6 Kb
107 | 2.098k i/100ms
108 | DiskStore.put : 100 records, value size: 660 Kb
109 | 1.000 i/100ms
110 | DiskStore.get : 100 records, value size: 660 Kb
111 | 148.000 i/100ms
112 | Calculating -------------------------------------
113 | DiskStore.put : 100 records with data size: 702 Bytes
114 | 1.552k (±15.7%) i/s - 7.450k in 5.008519s
115 | DiskStore.get : 100 records, value size: 702 Bytes
116 | 24.100k (± 3.7%) i/s - 121.839k in 5.062195s
117 | DiskStore.put : 100 records, value size: 6 Kb
118 | 655.280 (±53.4%) i/s - 1.716k in 5.272456s
119 | DiskStore.get : 100 records, value size: 6 Kb
120 | 19.883k (± 7.1%) i/s - 100.704k in 5.090910s
121 | DiskStore.put : 100 records, value size: 660 Kb
122 | 4.479 (± 0.0%) i/s - 23.000 in 5.166651s
123 | DiskStore.get : 100 records, value size: 660 Kb
124 | 3.286k (±39.4%) i/s - 8.140k in 5.272424s
125 | ```
126 |
127 | ## Contributing
128 |
129 | If you wish to contribute in any way, please fork the repo and raise a PR.
130 |
131 | [](https://app.circleci.com/insights/github/dineshgowda24/bitcask-rb/workflows/workflow/overview?branch=main&reporting-window=last-30-days&insights-snapshot=true)
--------------------------------------------------------------------------------
/benchmark.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'benchmark'
4 | require 'benchmark/ips'
5 | require 'faker'
6 | require_relative 'lib/bitcask'
7 |
8 | include Benchmark
9 |
10 | disk_store = Bitcask::DiskStore.new('bitcask_benchmark.db')
11 | value = Faker::Lorem.sentence(word_count: 10)
12 |
13 | puts "Benchmarked with value_size of #{value.length} bytes"
14 |
15 | Benchmark.benchmark(CAPTION, 50, FORMAT, 'avg_put:', 'avg_get:') do |benchmark|
16 | tt_put_10k = benchmark.report('DiskStore.put : 10k records') {
17 | 10_000.times do |n_time|
18 | disk_store.put("10_000#{n_time}", value)
19 | end
20 | }
21 |
22 | tt_get_10k = benchmark.report('DiskStore.get : 10k records') {
23 | 10_000.times do
24 | disk_store.get("10_000#{rand(1..10_000)}")
25 | end
26 | }
27 |
28 | tt_put_100k = benchmark.report('DiskStore.put : 100k records') {
29 | 100_000.times do |n_time|
30 | disk_store.put("100_000#{n_time}", value)
31 | end
32 | }
33 |
34 | tt_get_100k = benchmark.report('DiskStore.get : 100k records') {
35 | 100_000.times do
36 | disk_store.get("100_000#{rand(1..100_000)}")
37 | end
38 | }
39 |
40 | tt_put_1M = benchmark.report('DiskStore.put : 1M records') {
41 | 1_000_000.times do |n_time|
42 | disk_store.put("1_000_000#{n_time}", value)
43 | end
44 | }
45 |
46 | tt_get_1M = benchmark.report('DiskStore.get : 1M records') {
47 | 1_000_000.times do
48 | disk_store.get("1_000_000#{rand(1..1_000_000)}")
49 | end
50 | }
51 |
52 | [(tt_put_10k + tt_put_100k + tt_put_1M) / (10_000 + 100_000 + 1_000_000).to_f,
53 | (tt_get_10k + tt_get_100k + tt_get_1M) / (10_000 + 100_000 + 1_000_000).to_f]
54 | end
55 |
56 | value_1 = Faker::Lorem.sentence(word_count: 100)
57 | value_2 = Faker::Lorem.sentence(word_count: 1000)
58 | value_3 = Faker::Lorem.sentence(word_count: 100_000)
59 |
60 | Benchmark.ips do |benchmark|
61 | benchmark.report("DiskStore.put : 100 records with data size: #{value_1.length} Bytes") do
62 | 100.times do |n_time|
63 | disk_store.put("10_000_#{value_1.length}#{n_time}", value_1)
64 | end
65 | end
66 |
67 | benchmark.report("DiskStore.get : 100 records, value size: #{value_1.length} Bytes") do
68 | 100.times do
69 | disk_store.get("10_000#{value_1.length}#{rand(1..10_000)}")
70 | end
71 | end
72 |
73 | benchmark.report("DiskStore.put : 100 records, value size: #{value_2.length / 1024} Kb") do
74 | 100.times do |n_time|
75 | disk_store.put("10_000#{value_2.length}#{n_time}", value_2)
76 | end
77 | end
78 |
79 | benchmark.report("DiskStore.get : 100 records, value size: #{value_2.length / 1024} Kb") do
80 | 100.times do
81 | disk_store.get("10_000#{value_2.length}#{rand(1..10_000)}")
82 | end
83 | end
84 |
85 | benchmark.report("DiskStore.put : 100 records, value size: #{value_3.length / 1024} Kb") do
86 | 100.times do |n_time|
87 | disk_store.put("10_000#{value_3.length}#{n_time}", value_3)
88 | end
89 | end
90 |
91 | benchmark.report("DiskStore.get : 100 records, value size: #{value_3.length / 1024} Kb") {
92 | 100.times do
93 | disk_store.get("10_000#{value_3.length}#{rand(1..10_000)}")
94 | end
95 | }
96 | end
97 |
98 | File.delete('bitcask_benchmark.db')
99 |
--------------------------------------------------------------------------------
/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dineshgowda24/bitcask-rb/63f1c3b701b68b679c5de8272c6dc039cb25d2f1/image.png
--------------------------------------------------------------------------------
/lib/bitcask.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'bitcask/serializer'
4 | require_relative 'bitcask/disk_store'
5 |
--------------------------------------------------------------------------------
/lib/bitcask/disk_store.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'ostruct'
4 |
5 | module Bitcask
6 | ##
7 | # This class represents a persistant store via a database file
8 | class DiskStore
9 | include Serializer
10 |
11 | ##
12 | # Creates a new store described by a database file
13 | #
14 | # If the file already exists, then contents of key_dir will be populated.
15 | # A StandardError will be raised when the file is corrupted.
16 | def initialize(db_file = 'bitcask.db')
17 | @db_fh = File.open(db_file, 'a+b')
18 | @write_pos = 0
19 | @key_dir = {}
20 |
21 | init_key_dir
22 | end
23 |
24 | def [](key)
25 | get(key)
26 | end
27 |
28 | def []=(key, value)
29 | put(key, value)
30 | end
31 |
32 | # Get the value for the given key
33 | # When the key does not exist, it returns empty string
34 | #
35 | # @param [String] key
36 | # @return [Value] value of the given key
37 | def get(key)
38 | key_struct = @key_dir[key]
39 | return '' if key_struct.nil?
40 |
41 | @db_fh.seek(key_struct[:write_pos])
42 | epoc, key, value = deserialize(@db_fh.read(key_struct[:log_size]))
43 |
44 | value
45 | end
46 |
47 | # Sets a new key value pair
48 | #
49 | # @param [String, String] key and value
50 | # @return [nil]
51 | def put(key, value)
52 | log_size, data = serialize(epoc: Time.now.to_i, key: key, value: value)
53 |
54 | @key_dir[key] = key_struct(@write_pos, log_size, key)
55 | persist(data)
56 | incr_write_pos(log_size)
57 |
58 | nil
59 | end
60 |
61 | def keys
62 | @key_dir.keys
63 | end
64 |
65 | def size
66 | @key_dir.length
67 | end
68 |
69 | def flush
70 | @db_fh.flush
71 | end
72 |
73 | def close
74 | flush
75 | @db_fh.close
76 | end
77 |
78 | private
79 |
80 | def persist(data)
81 | @db_fh.write(data)
82 | @db_fh.flush
83 | end
84 |
85 | def incr_write_pos(pos)
86 | @write_pos += pos
87 | end
88 |
89 | def key_struct(write_pos, log_size, key)
90 | { write_pos: write_pos, log_size: log_size, key: key }
91 | end
92 |
93 | def init_key_dir
94 | while (crc_and_header_bytes = @db_fh.read(crc32_header_offset))
95 |
96 | header_bytes = crc_and_header_bytes[crc32_offset..]
97 | epoc, keysz, valuesz, key_type, value_type = deserialize_header(header_bytes)
98 |
99 | key_bytes = @db_fh.read(keysz)
100 | value_bytes = @db_fh.read(valuesz)
101 |
102 | key = unpack(key_bytes, key_type)
103 | value = unpack(value_bytes, value_type)
104 |
105 | crc = crc_and_header_bytes[..crc32_offset - 1]
106 | raise StandardError, 'file corrupted' unless crc32_valid?(desearlize_crc32(crc),
107 | header_bytes + key_bytes + value_bytes)
108 |
109 | log_size = crc32_header_offset + keysz + valuesz
110 | @key_dir[key] = key_struct(@write_pos, log_size, key)
111 | incr_write_pos(log_size)
112 | end
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/bitcask/serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'zlib'
4 |
5 | module Bitcask
6 | ##
7 | # Serializer module encapsulates the complexties of serializing and deserializing arbitary data
8 | # to/from bytes stream
9 | module Serializer
10 | # Follwing are the header values stored
11 | # |epoc|keysz|valuesz|key_type|value_type|
12 | # | 4B | 4B | 4B | 2B | 2B |
13 | # A total of 16 Bytes
14 | # L< : unsiged 32 bit int with little endian byte order
15 | # S< " unsiged 12 bit int with little endian byte order
16 | # Endian order does not matter, its only used to keep consitent byte ordering to ensure that db file,
17 | # can be seemlessly interchanged in little/big endian machines
18 | HEADER_FORMAT = 'L :Integer,
33 | DATA_TYPE[:Float] => :Float,
34 | DATA_TYPE[:String] => :String
35 | }.freeze
36 |
37 | DATA_TYPE_DIRECTIVE = {
38 | # 64 bit signed long int with little endian byte order
39 | DATA_TYPE[:Integer] => 'q<',
40 | # 64 bit double with little endian byte order
41 | DATA_TYPE[:Float] => 'E'
42 | }.freeze
43 |
44 | # Serializes epoc, key, value with metadata
45 | #
46 | # @param [Integer, String, String] contents to serialize
47 | # @return [Integer, String] Size of the serialized binary string, binary seralized string
48 | def serialize(epoc:, key:, value:)
49 | key_type = type(key)
50 | value_type = type(value)
51 |
52 | key_bytes = pack(key, key_type)
53 | value_bytes = pack(value, value_type)
54 |
55 | header = serialize_header(epoc: epoc, keysz: key_bytes.length, key_type: key_type, value_type: value_type,
56 | valuesz: value_bytes.length)
57 | data = key_bytes + value_bytes
58 |
59 | [crc32_header_offset + data.length, crc32(header + data) + header + data]
60 | end
61 |
62 | # Deserializes byte string
63 | #
64 | # @param [String] byte string to deserialize
65 | # @return [Integer, String|Float|Integer, String|Float|Integer] Epoc, Key, Value
66 | def deserialize(data)
67 | return 0, '', '' unless crc32_valid?(desearlize_crc32(data[..crc32_offset - 1]), data[crc32_offset..])
68 |
69 | epoc, keysz, valuesz, key_type, value_type = deserialize_header(data[crc32_offset..crc32_header_offset - 1])
70 | key_bytes = data[crc32_header_offset..crc32_header_offset + keysz - 1]
71 | value_bytes = data[crc32_header_offset + keysz..]
72 |
73 | [epoc, unpack(key_bytes, key_type), unpack(value_bytes, value_type)]
74 | end
75 |
76 | # Serializes header
77 | #
78 | # @param [Integer, String, String, Symbol, Symbol] contents to serialize
79 | # @return [String] Byte string
80 | def serialize_header(epoc:, key_type:, keysz:, value_type:, valuesz:)
81 | [epoc, keysz, valuesz, DATA_TYPE[key_type], DATA_TYPE[value_type]].pack(HEADER_FORMAT)
82 | end
83 |
84 | # Derializes header
85 | #
86 | # @param [String] byte string to dserialize
87 | # @return [Integer, String, String, Symbol, Symbol] Epoc, keysz, valuesz, key_type, value_type
88 | def deserialize_header(header_data)
89 | header = header_data.unpack(HEADER_FORMAT)
90 |
91 | [header[0], header[1], header[2], DATA_TYPE_LOOK_UP[header[3]], DATA_TYPE_LOOK_UP[header[4]]]
92 | end
93 |
94 | def crc32_offset
95 | CRC32_SIZE
96 | end
97 |
98 | def header_offset
99 | HEADER_SIZE
100 | end
101 |
102 | def crc32_header_offset
103 | crc32_offset + header_offset
104 | end
105 |
106 | # Generates crc32 and seralizes to bytes
107 | #
108 | # @param [String] byte string
109 | # @return [String] crc32 bytes
110 | def crc32(data_bytes)
111 | [Zlib.crc32(data_bytes)].pack(CRC32_FORMAT)
112 | end
113 |
114 | # Derializes crc32 byte string
115 | #
116 | # @param [String] byte string
117 | # @return [Integer] crc32
118 | def desearlize_crc32(crc)
119 | crc.unpack1(CRC32_FORMAT)
120 | end
121 |
122 | def crc32_valid?(digest, data_bytes)
123 | digest == Zlib.crc32(data_bytes)
124 | end
125 |
126 | def pack(attribute, attribute_type)
127 | case attribute_type
128 | when :Integer, :Float
129 | [attribute].pack(directive(attribute_type))
130 | when :String
131 | attribute.encode('utf-8')
132 | else
133 | raise StandardError, 'Invalid attribute_type for pack'
134 | end
135 | end
136 |
137 | def unpack(attribute, attribute_type)
138 | case attribute_type
139 | when :Integer, :Float
140 | attribute.unpack1(directive(attribute_type))
141 | when :String
142 | attribute
143 | else
144 | raise StandardError, 'Invalid attribute_type for unpack'
145 | end
146 | end
147 |
148 | private
149 |
150 | def directive(attribute_type)
151 | DATA_TYPE_DIRECTIVE[DATA_TYPE[attribute_type]]
152 | end
153 |
154 | def type(attribute)
155 | attribute.class.to_s.to_sym
156 | end
157 | end
158 | end
159 |
--------------------------------------------------------------------------------
/spec/bitcask/disk_store_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Bitcask::DiskStore do
6 | let(:test_db_file) { 'bitcask_test.db' }
7 | let(:test_db_fixture_file) { db_fixture_file_path }
8 |
9 | describe '#put' do
10 | subject { described_class.new(test_db_file) }
11 |
12 | after do
13 | File.delete(test_db_file)
14 | end
15 |
16 | it 'puts a kv pair on the disk' do
17 | expect(subject.put(Faker::Lorem.word, Faker::Lorem.sentence)).to be_nil
18 | expect(subject.put(Faker::Lorem.word, Faker::Lorem.sentence(word_count: 10))).to be_nil
19 | expect(subject.put(Faker::Lorem.word, Faker::Lorem.sentence(word_count: 100))).to be_nil
20 | expect(subject.put(Faker::Lorem.word, Faker::Lorem.sentence(word_count: 1000))).to be_nil
21 | expect(subject.put(rand(1..10_000), Faker::Lorem.sentence(word_count: 10_000))).to be_nil
22 | expect(subject.put(rand(10.2..100.234), rand(1..10_000))).to be_nil
23 | expect(subject.put(rand(1..10_000), rand(10.2..100.234))).to be_nil
24 | end
25 | end
26 |
27 | describe '#get' do
28 | context 'when the db file is present' do
29 | subject { described_class.new(test_db_fixture_file) }
30 |
31 | context 'when the key is not present' do
32 | it 'returns empty string' do
33 | expect(subject.get('Anime')).to eq('')
34 | end
35 | end
36 |
37 | context 'when the key is present' do
38 | it 'returns value' do
39 | expect(subject.get('ut')).to eq('Voluptatum esse non vero ut vitae harum blanditiis ducimus vel nam rerum quia ipsa necessitatibus quo eaque animi ab voluptatem sed sunt non ipsam aut velit rerum perspiciatis quasi doloribus omnis eum et reprehenderit qui minima aut illo veritatis atque sequi quas eius consequatur magni saepe numquam molestias odio beatae quo maiores dignissimos illum aut sint qui porro sed in enim enim asperiores et tenetur voluptas maxime possimus quidem accusantium laudantium aliquam ipsum voluptates consequuntur et tempora cumque voluptatem dolor tempore sint nemo ex omnis repudiandae aliquid pariatur neque nostrum debitis odit qui nihil voluptatem minus temporibus voluptatem ut sit.')
40 | expect(subject.get('saepe')).to eq('Non autem magni non quaerat non eos enim amet qui molestiae pariatur quam rerum facilis nulla tempora reprehenderit ipsa rerum sunt ducimus aspernatur magni blanditiis blanditiis eveniet sed nobis quisquam iusto quia corporis in deleniti repellendus iure similique facere maxime beatae aut quidem fuga labore laborum reprehenderit suscipit eveniet molestias aspernatur vel minus sunt quo reprehenderit sint deserunt corporis velit hic recusandae et voluptatem ipsa dolores eos facere eum qui ut nam et aperiam voluptatibus minima laborum doloremque officiis optio eaque voluptatibus et sint dicta esse ab ex ut cumque temporibus alias voluptatum qui iusto laboriosam nihil qui veritatis aut pariatur et sint rerum fugiat reiciendis non quia atque blanditiis et suscipit unde magni iste ea voluptates ex ad expedita eum ut quasi et adipisci reiciendis et a earum enim excepturi autem vel accusantium qui veniam est qui odio voluptatem aut deleniti omnis quia placeat ut modi nostrum tempora est labore ipsa accusamus et sit eum delectus nobis dolores laboriosam omnis est sed voluptatem neque ut dicta et est et dolor vero quis praesentium minima ut voluptas omnis ut velit sed eum harum veniam dignissimos tempora aliquid et consectetur in ducimus cum molestiae dolor hic quisquam sed tenetur non quam eaque fugiat ea qui esse recusandae nisi officia provident dolores facere voluptatem corrupti distinctio magnam perferendis sunt soluta ut perspiciatis laudantium officia veniam quia magnam sunt libero corporis ut nisi est reprehenderit saepe ut debitis et eum ipsum illo iste animi expedita rerum totam et animi temporibus voluptatum minus amet illum inventore vel aut dolorem est necessitatibus rem fugit est omnis laudantium dolor fugit sit distinctio voluptates voluptatem aut exercitationem placeat rerum molestias corrupti earum impedit numquam minima eum voluptate in quasi illo quo veritatis perferendis et est numquam sit quo dolores dolorem animi consectetur voluptatem est expedita commodi reprehenderit inventore accusamus nostrum sapiente adipisci molestiae nam et itaque odit assumenda cupiditate deserunt vel aut cupiditate quidem quo ut dolores aut ut ad nihil aut dolores sed eaque modi molestiae debitis qui omnis eius dicta dolorem odit ut et eveniet et repellat sit omnis porro nemo qui ullam culpa explicabo accusantium omnis aut ipsum sed dignissimos odit fugit cumque harum nihil ut repudiandae nisi impedit aut dolor velit nemo a nulla facilis eum optio quaerat repellendus expedita dolores sunt perferendis ullam optio ullam consequatur iste id saepe officiis aspernatur ea rerum et nesciunt sit velit et dolorem quo quam alias in omnis et quos consequatur consequatur natus ut ut mollitia ut quae sed hic et ex quae ut libero doloremque unde rem esse sint maiores cumque maiores non ratione vitae perferendis ratione qui sapiente doloribus consequuntur consequatur dicta laudantium eligendi qui voluptas voluptate amet vel quas sunt qui similique tenetur culpa alias accusamus repellat eos sint facere qui labore facilis vitae aliquam voluptates ipsum voluptatem voluptate est dolorum autem quia illum ea aliquid molestias voluptatem ipsam atque eaque mollitia minus excepturi aliquam est iure impedit deleniti occaecati et nemo dignissimos totam illo laudantium temporibus magni suscipit tempora veritatis aut autem ut quis necessitatibus natus maiores dignissimos nulla alias adipisci recusandae quis doloribus ipsam consequatur et quis eum inventore et architecto neque at sed aliquam asperiores voluptatem vel libero asperiores natus tempore sint distinctio voluptatem est commodi sequi saepe molestiae tempora maxime dolor facilis nesciunt repudiandae corporis consequatur similique itaque eum tempore et ex ipsa deserunt possimus ullam odio aut repellat molestiae earum ea et sit doloribus provident fugit eveniet sit excepturi adipisci nostrum ab corrupti quia harum omnis quia non deleniti libero et maiores consequatur eligendi accusantium quod nihil minus ut dicta voluptatem dolor distinctio sit harum dolorum est tenetur itaque quia vel est ratione atque ex voluptas est aut molestiae est sapiente commodi saepe atque possimus rerum nihil aut neque eum repellat porro hic est debitis sequi qui optio nam tempore aut nobis assumenda blanditiis commodi occaecati reiciendis et fuga hic rerum aut aut nam ut doloribus assumenda sit assumenda et dolorem nobis error debitis maxime et quod commodi asperiores voluptas velit earum et delectus enim qui quod sit corporis est totam blanditiis mollitia dolores voluptatem consequatur sunt ut cumque a et id consequuntur molestias eos occaecati mollitia at pariatur nesciunt reiciendis corrupti voluptatibus quia laboriosam excepturi qui cum odit neque dolore accusamus cupiditate et veniam animi necessitatibus nesciunt totam dolorem qui ea dolorum doloribus et odio sit fugit eos facere culpa perspiciatis ipsa consequuntur modi velit quidem aperiam labore consequatur animi ipsam exercitationem sed ipsam aperiam labore at cum tenetur dolor provident consequatur nobis quibusdam quaerat vel alias deserunt sed voluptas et et consectetur consequatur quos deleniti in dolorum ab voluptates quo id expedita quas eius esse quo explicabo fuga tempore ut quo velit enim voluptatibus voluptas placeat voluptas id porro molestiae autem accusantium sed voluptas voluptatem maxime enim illum sint omnis qui aperiam aliquam quis aut voluptatum in aut recusandae illum quia et quae quaerat asperiores explicabo autem sunt recusandae accusamus eius praesentium qui in quis voluptas nulla aut magnam quia sapiente ea eius est error soluta autem molestias enim rerum quod sed laborum rem delectus ea adipisci rerum cumque id harum enim omnis repellat veniam sed nostrum numquam quas incidunt quibusdam esse similique nemo qui autem perspiciatis non pariatur et beatae nisi repellendus itaque officiis consequatur qui cum voluptatem et placeat non sed aspernatur amet unde aut suscipit cupiditate doloremque soluta veritatis magni nihil error et totam sint porro et nostrum aut unde officia dolorem quia unde corrupti libero rem et et natus est vero consectetur modi quibusdam voluptatem fugiat dolore incidunt reiciendis asperiores nihil veritatis amet velit quo et eligendi numquam in ea id ut iure id sequi in consequatur id facilis dolore voluptas voluptatem quia qui quibusdam qui quos exercitationem ad omnis vitae sunt voluptate qui quo aut quod possimus est quos dolorem quasi aspernatur provident beatae iusto officia nihil voluptatum et et praesentium explicabo ut itaque id laboriosam aliquid autem ipsum quos vero id inventore quis officiis ad temporibus quisquam sit deserunt fugiat incidunt repellendus cum magnam quidem enim consectetur maxime laborum rerum.')
41 | expect(subject.get('suscipit')).to eq('Quia sapiente maiores sunt.')
42 | expect(subject.get(1)).to eq(10_000)
43 | expect(subject.get(1.000)).to eq(10_000.000)
44 | end
45 | end
46 | end
47 |
48 | context 'when the db file is not present' do
49 | subject { described_class.new(test_db_file) }
50 |
51 | after do
52 | File.delete(test_db_file)
53 | end
54 |
55 | context 'when the key is not present' do
56 | it 'returns empty string' do
57 | expect(subject.get(Faker::Lorem.word)).to eq('')
58 | end
59 | end
60 |
61 | context 'when the key is present' do
62 | let(:key_1) { Faker::Lorem.word }
63 | let(:key_2) { Faker::Lorem.word }
64 | let(:key_3) { rand(1..1000) }
65 | let(:value_1) { Faker::Lorem.sentence(word_count: 10_000) }
66 | let(:value_2) { Faker::Lorem.sentence(word_count: 100_000) }
67 | let(:value_3) { rand(11.2...76.9) }
68 |
69 | before do
70 | subject.put(key_1, value_1)
71 | subject.put(key_2, value_2)
72 | subject.put(key_3, value_3)
73 | end
74 |
75 | it 'returns value' do
76 | expect(subject.get(key_1)).to eq(value_1)
77 | expect(subject.get(key_2)).to eq(value_2)
78 | expect(subject.get(key_3)).to eq(value_3)
79 | end
80 | end
81 | end
82 | end
83 |
84 | describe '#keys' do
85 | context 'when the db file is present' do
86 | subject { described_class.new(test_db_fixture_file) }
87 |
88 | context 'when the db file has data' do
89 | it 'returns an array of keys' do
90 | expect(subject.keys.length).to eq(5)
91 | expect(subject.keys).to eq([1, 1.0, 'ut', 'saepe', 'suscipit'])
92 | end
93 | end
94 | end
95 |
96 | context 'when the db file is not present' do
97 | subject { described_class.new(test_db_file) }
98 |
99 | after do
100 | File.delete(test_db_file)
101 | end
102 |
103 | context 'when the db file is empty' do
104 | it 'returns an empty array' do
105 | expect(subject.keys.length).to eq(0)
106 | expect(subject.keys).to eq([])
107 | end
108 | end
109 |
110 | context 'when the db file is not empty' do
111 | let(:key_1) { Faker::Lorem.word }
112 | let(:key_2) { Faker::Lorem.word }
113 | let(:value_1) { Faker::Lorem.sentence(word_count: 10_000) }
114 | let(:value_2) { Faker::Lorem.sentence(word_count: 100_000) }
115 |
116 | before do
117 | subject.put(key_1, value_1)
118 | subject.put(key_2, value_2)
119 | end
120 |
121 | it 'returns an empty array' do
122 | expect(subject.keys.length).to eq(2)
123 | expect(subject.keys).to eq([key_1, key_2])
124 | end
125 | end
126 | end
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/spec/bitcask/serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Bitcask::Serializer do
4 | let(:crc_size) { 4 }
5 | let(:header_size) { 16 }
6 | let(:crc_and_header_size) { header_size + crc_size }
7 | let(:subject) { Class.new { extend Bitcask::Serializer } }
8 | let(:now) { Time.now.to_i }
9 |
10 | describe '#serialize' do
11 | let(:key) { Faker::Lorem.word }
12 | let(:value) { Faker::Lorem.sentence(word_count: 10_000) }
13 |
14 | it 'returns serialized data and its size' do
15 | size, data = subject.serialize(epoc: now, key: key, value: value)
16 |
17 | expect(size).to eq(crc_and_header_size + key.length + value.length)
18 | expect(data).not_to be_empty
19 | end
20 | end
21 |
22 | describe '#serialize_header' do
23 | it 'returns serialized header' do
24 | data = subject.serialize_header(epoc: now, keysz: 10, valuesz: 20, key_type: :Integer, value_type: :Integer)
25 |
26 | expect(data.length).to eq(header_size)
27 | expect(data).not_to be_empty
28 | end
29 | end
30 |
31 | describe '#deserialize' do
32 | let(:serialized_data_1) do
33 | OpenStruct.new(raw: "<\v\x90\x8C\x01\xC1\xA9c\b\x00\x00\x00\b\x00\x00\x00\x01\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00333333\xF3?",
34 | epoc: 1_672_069_377, key: 1, value: 1.2)
35 | end
36 | let(:serialized_data_2) do
37 | OpenStruct.new(raw: "\xFE\xE9\xD2.\x01\xC1\xA9c\b\x00\x00\x00\b\x00\x00\x00\x02\x00\x01\x00333333\xF3?\x02\x00\x00\x00\x00\x00\x00\x00",
38 | epoc: 1_672_069_377, key: 1.2, value: 2)
39 | end
40 | let(:serialized_data_3) do
41 | OpenStruct.new(raw: "\xBA\xFD\x0EA\x01\xC1\xA9c\x05\x00\x00\x00\t\x00\x00\x00\x03\x00\x03\x00AnimeOne Piece",
42 | epoc: 1_672_069_377, key: 'Anime', value: 'One Piece')
43 | end
44 |
45 | context 'when a valid data is passed' do
46 | it 'returns epoc, key and value' do
47 | epoc, key, value = subject.deserialize(serialized_data_1.raw)
48 |
49 | expect(epoc).to eq(serialized_data_1.epoc)
50 | expect(key).to eq(serialized_data_1.key)
51 | expect(value).to eq(serialized_data_1.value)
52 | end
53 |
54 | it 'returns epoc, key and value' do
55 | epoc, key, value = subject.deserialize(serialized_data_2.raw)
56 |
57 | expect(epoc).to eq(serialized_data_2.epoc)
58 | expect(key).to eq(serialized_data_2.key)
59 | expect(value).to eq(serialized_data_2.value)
60 | end
61 |
62 | it 'returns epoc, key and value' do
63 | epoc, key, value = subject.deserialize(serialized_data_3.raw)
64 |
65 | expect(epoc).to eq(serialized_data_3.epoc)
66 | expect(key).to eq(serialized_data_3.key)
67 | expect(value).to eq(serialized_data_3.value)
68 | end
69 | end
70 |
71 | context 'when an empty string is passed' do
72 | it 'raises size as 0 and empty string' do
73 | epoc, key, value = subject.deserialize('')
74 |
75 | expect(epoc).to eq(0)
76 | expect(key).to eq('')
77 | expect(value).to eq('')
78 | end
79 | end
80 |
81 | context 'when crc is invalid' do
82 | it 'returns size as 0 and empty string' do
83 | epoc, key, value = subject.deserialize("\xCEi\x94\x03}\xA7\xA8c\x05\x00\x00\x00\t\x00\x00\x00animeTwo Piece")
84 |
85 | expect(epoc).to eq(0)
86 | expect(key).to eq('')
87 | expect(value).to eq('')
88 | end
89 | end
90 | end
91 |
92 | describe '#deserialize_header' do
93 | let(:key) { 'anime' }
94 | let(:value) { 'One Piece' }
95 | let(:serialized_header_data) do
96 | OpenStruct.new(raw: "\xF5\xC1\xA9c\n\x00\x00\x00\x14\x00\x00\x00\x01\x00\x01\x00", epoc: 1_672_069_621, keysz: 10,
97 | valuesz: 20, value_type: :Integer, key_type: :Integer)
98 | end
99 |
100 | it 'returns epoc, keysz and valuesz' do
101 | epoc, keysz, valuesz, key_type, value_type = subject.deserialize_header(serialized_header_data.raw)
102 |
103 | expect(epoc).to eq(serialized_header_data.epoc)
104 | expect(keysz).to eq(serialized_header_data.keysz)
105 | expect(valuesz).to eq(serialized_header_data.valuesz)
106 | expect(key_type).to eq(serialized_header_data.key_type)
107 | expect(value_type).to eq(serialized_header_data.value_type)
108 | end
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/spec/fixtures/1672042848_bitcask.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dineshgowda24/bitcask-rb/63f1c3b701b68b679c5de8272c6dc039cb25d2f1/spec/fixtures/1672042848_bitcask.db
--------------------------------------------------------------------------------
/spec/fixtures/1672070284_bitcask.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dineshgowda24/bitcask-rb/63f1c3b701b68b679c5de8272c6dc039cb25d2f1/spec/fixtures/1672070284_bitcask.db
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file was generated by the `rspec --init` command. Conventionally, all
4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5 | # The generated `.rspec` file contains `--require spec_helper` which will cause
6 | # this file to always be loaded, without a need to explicitly require it in any
7 | # files.
8 | #
9 | # Given that it is always loaded, you are encouraged to keep this file as
10 | # light-weight as possible. Requiring heavyweight dependencies from this file
11 | # will add to the boot time of your test suite on EVERY test run, even for an
12 | # individual file that may not need all of that loaded. Instead, consider making
13 | # a separate helper file that requires the additional dependencies and performs
14 | # the additional setup, and require it from the spec files that actually need
15 | # it.
16 | #
17 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
18 | require 'simplecov'
19 | require 'simplecov-cobertura'
20 |
21 | SimpleCov.start do
22 | formatter SimpleCov::Formatter::CoberturaFormatter if ENV['CI']
23 | end
24 |
25 | require 'bitcask'
26 | require 'byebug'
27 | require 'ostruct'
28 | require 'faker'
29 |
30 | RSpec.configure do |config|
31 | # rspec-expectations config goes here. You can use an alternate
32 | # assertion/expectation library such as wrong or the stdlib/minitest
33 | # assertions if you prefer.
34 | config.expect_with :rspec do |expectations|
35 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
36 | end
37 |
38 | # rspec-mocks config goes here. You can use an alternate test double
39 | # library (such as bogus or mocha) by changing the `mock_with` option here.
40 | config.mock_with :rspec do |mocks|
41 | # Prevents you from mocking or stubbing a method that does not exist on
42 | # a real object. This is generally recommended, and will default to
43 | # `true` in RSpec 4.
44 | mocks.verify_partial_doubles = true
45 | end
46 |
47 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
48 | # have no way to turn it off -- the option exists only for backwards
49 | # compatibility in RSpec 3). It causes shared context metadata to be
50 | # inherited by the metadata hash of host groups and examples, rather than
51 | # triggering implicit auto-inclusion in groups with matching metadata.
52 | config.shared_context_metadata_behavior = :apply_to_host_groups
53 | end
54 |
55 | def db_fixture_file_path
56 | 'spec/fixtures/1672070284_bitcask.db'
57 | end
58 |
--------------------------------------------------------------------------------