persistent-cache 0.0.8 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +20 -2
- data/lib/persistent-cache.rb +31 -33
- data/lib/persistent-cache/storage/storage_directory.rb +165 -0
- data/lib/persistent-cache/storage/storage_sq_lite.rb +84 -0
- data/lib/persistent-cache/version.rb +1 -1
- data/multidb +0 -0
- data/persistent-cache.gemspec +3 -0
- data/spec/persistent-cache_spec.rb +92 -87
- data/spec/spec_helper.rb +15 -0
- data/spec/storage/storage_directory_spec.rb +264 -0
- data/spec/storage/storage_sqlite_spec.rb +193 -0
- metadata +56 -2
data/README.md
CHANGED
@@ -1,11 +1,25 @@
|
|
1
1
|
# Persistent::Cache
|
2
2
|
|
3
|
-
Persistent cache behaves like a hash, with a sqlite3
|
3
|
+
Persistent cache behaves like a hash, with a pluggable back-end. Currently sqlite3 and file system directory back-ends are provided. The cache defaults to type STORAGE_SQLITE
|
4
4
|
|
5
|
-
Values in the cache have a default freshness period of 15465600 ms. This can be configured in the cache initializer. Setting fresh = nil indicates that data remains fresh for-ever. Each user of the cache may have his own independent freshness value. If stale data is requested from the cache, nil is returned.
|
5
|
+
Values in the cache have a default freshness period of 15465600 ms. This can be configured in the cache initializer. Setting fresh = nil indicates that data remains fresh for-ever. Each user of the cache may have his own independent freshness value. If stale data is requested from the cache, nil is returned. Data is marshalled before storage. If a key is not found in the cache, nil is returned. Setting the value of a key in the cache to nil deletes the entry. If required, creation time of an entry can be specified using set(key, value, timestamp)
|
6
6
|
|
7
7
|
This gem is sponsored by Hetzner (Pty) Ltd - http://hetzner.co.za
|
8
8
|
|
9
|
+
## StorageSQLite
|
10
|
+
|
11
|
+
Updates to the cache are written to the sqlite3 storage, with SQL driver timeout set to 30 seconds.
|
12
|
+
|
13
|
+
## StorageDirectory
|
14
|
+
|
15
|
+
Keys are required to be strings that are valid for use as directory names. The cache then stores from a storage root (configured in the StorageDirector constructor) with a subdirectory for each key, and a file called 'cache' for the value. The first line in the cache file is the timestamp of the entry.
|
16
|
+
|
17
|
+
When a StorageDirectory is used, it can be asked whether a key is present and what the path to a cache value is using:
|
18
|
+
|
19
|
+
get_value_path(key)
|
20
|
+
|
21
|
+
key_cached?(key)
|
22
|
+
|
9
23
|
## Installation
|
10
24
|
|
11
25
|
Add this line to your application's Gemfile:
|
@@ -60,6 +74,10 @@ Or install it yourself as:
|
|
60
74
|
|
61
75
|
cache = Persistent::Cache.new("/tmp/my-persistent-cache", nil) # for-ever fresh
|
62
76
|
|
77
|
+
cache = Persistent::Cache.new("/tmp/directory-cache", nil, STORAGE_DIRECTORY)
|
78
|
+
|
79
|
+
cache.set("mykey", "myvalue", Time.now) # explicitly set creation time
|
80
|
+
|
63
81
|
Please send feedback and comments to the authors at:
|
64
82
|
|
65
83
|
Wynand van Dyk <wynand.van.dyk@hetzner.co.za>
|
data/lib/persistent-cache.rb
CHANGED
@@ -1,19 +1,36 @@
|
|
1
1
|
require "persistent-cache/version"
|
2
2
|
require "sqlite3"
|
3
|
+
require "persistent-cache/storage/storage_sq_lite"
|
3
4
|
|
4
5
|
module Persistent
|
5
6
|
class Cache
|
7
|
+
STORAGE_SQLITE = 'sqlite' unless defined? STORAGE_SQLITE
|
8
|
+
STORAGE_DIRECTORY = 'directory' unless defined? STORAGE_DIRECTORY
|
9
|
+
|
6
10
|
# Fresh is 1 day less than the bacula default job retention time. If this is configured differently, FRESH should be updated as well.
|
7
11
|
FRESH = 15465600; FRESH.freeze
|
8
|
-
DB_TABLE = "key_value"; DB_TABLE.freeze
|
9
12
|
|
10
|
-
attr_accessor :
|
13
|
+
attr_accessor :storage_details
|
14
|
+
attr_accessor :storage
|
15
|
+
attr_accessor :fresh
|
16
|
+
|
17
|
+
def initialize(storage_details, fresh = FRESH, storage = STORAGE_SQLITE)
|
18
|
+
raise ArgumentError.new("No storage details provided") if storage_details.nil? or storage_details == ""
|
11
19
|
|
12
|
-
|
13
|
-
@
|
14
|
-
@db_handle = connect_to_database
|
15
|
-
@db_handle.busy_timeout = 30000
|
20
|
+
@storage = StorageSQLite.new(storage_details) if storage == STORAGE_SQLITE
|
21
|
+
@storage = StorageDirectory.new(storage_details) if storage == STORAGE_DIRECTORY
|
16
22
|
@fresh = fresh
|
23
|
+
@storage_details = storage_details
|
24
|
+
|
25
|
+
raise ArgumentError.new("Unsupported storage type #{storage}}") if @storage.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def set(key, value, timestamp)
|
29
|
+
if value.nil?
|
30
|
+
delete_entry(Marshal.dump(key))
|
31
|
+
else
|
32
|
+
save_key_value_pair(Marshal.dump(key), Marshal.dump(value), timestamp)
|
33
|
+
end
|
17
34
|
end
|
18
35
|
|
19
36
|
def []=(key, value)
|
@@ -35,29 +52,28 @@ module Persistent
|
|
35
52
|
end
|
36
53
|
|
37
54
|
def size
|
38
|
-
@
|
55
|
+
@storage.size
|
39
56
|
end
|
40
57
|
|
41
58
|
def keys
|
42
|
-
@
|
59
|
+
@storage.keys.collect { |key|
|
43
60
|
Marshal.load(key[0])
|
44
61
|
}
|
45
62
|
end
|
46
63
|
|
47
64
|
def clear
|
48
|
-
@
|
65
|
+
@storage.clear
|
49
66
|
end
|
50
67
|
|
51
|
-
# Methods not supported by this implementation
|
52
68
|
private
|
53
69
|
|
54
|
-
def save_key_value_pair(serialized_key, serialized_value)
|
70
|
+
def save_key_value_pair(serialized_key, serialized_value, timestamp = nil)
|
55
71
|
delete_entry(serialized_key)
|
56
|
-
@
|
72
|
+
@storage.save_key_value_pair(serialized_key, serialized_value, timestamp)
|
57
73
|
end
|
58
74
|
|
59
75
|
def lookup_key(serialized_key)
|
60
|
-
result = @
|
76
|
+
result = @storage.lookup_key(serialized_key)
|
61
77
|
return nil if nil_result?(result)
|
62
78
|
return nil if stale_entry?(serialized_key, result)
|
63
79
|
|
@@ -66,6 +82,7 @@ module Persistent
|
|
66
82
|
|
67
83
|
def stale_entry?(serialized_key, result)
|
68
84
|
return false if @fresh.nil?
|
85
|
+
|
69
86
|
timestamp = Time.parse(result[0][1])
|
70
87
|
if ((Time.now - timestamp) > FRESH)
|
71
88
|
delete_entry(serialized_key)
|
@@ -75,26 +92,7 @@ module Persistent
|
|
75
92
|
end
|
76
93
|
|
77
94
|
def delete_entry(serialized_key)
|
78
|
-
@
|
79
|
-
end
|
80
|
-
|
81
|
-
def open_database
|
82
|
-
@handle = SQLite3::Database.open(@database_details)
|
83
|
-
end
|
84
|
-
|
85
|
-
def create_database
|
86
|
-
@handle = SQLite3::Database.new(@database_details)
|
87
|
-
create_table
|
88
|
-
@handle
|
89
|
-
end
|
90
|
-
|
91
|
-
def create_table
|
92
|
-
@handle.execute("CREATE TABLE #{DB_TABLE}(key TEXT PRIMARY KEY, value TEXT, timestamp TEXT)")
|
93
|
-
@handle
|
94
|
-
end
|
95
|
-
|
96
|
-
def connect_to_database
|
97
|
-
File.exists?(@database_details) ? open_database : create_database
|
95
|
+
@storage.delete_entry(serialized_key)
|
98
96
|
end
|
99
97
|
|
100
98
|
def nil_result?(result)
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Persistent
|
4
|
+
class StorageDirectory
|
5
|
+
CACHE_FILE = "cache" unless defined? CACHE_FILE; CACHE_FILE.freeze
|
6
|
+
|
7
|
+
attr_accessor :storage_root
|
8
|
+
|
9
|
+
def initialize(storage_details)
|
10
|
+
raise ArgumentError.new("Storage details not provided") if storage_details.nil? or storage_details == ""
|
11
|
+
@storage_root = storage_details
|
12
|
+
connect_to_database
|
13
|
+
end
|
14
|
+
|
15
|
+
def connect_to_database
|
16
|
+
FileUtils.makedirs([@storage_root]) if not File.exists?(@storage_root)
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_key_value_pair(key, value, timestamp = nil)
|
20
|
+
prepare_to_store_key_value(key, value, timestamp)
|
21
|
+
store_key_value(key, value, get_time(timestamp)) if not value.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def lookup_key(key)
|
25
|
+
validate_key(key)
|
26
|
+
return [] if not File.exists? compile_value_path(key)
|
27
|
+
lookup_key_value_timestamp(key)
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete_entry(key)
|
31
|
+
validate_key(key)
|
32
|
+
FileUtils.rm_rf(compile_key_path(key))
|
33
|
+
end
|
34
|
+
|
35
|
+
def size
|
36
|
+
count = Dir::glob("#{@storage_root}/**/").size
|
37
|
+
# if the directory does not exist, count == 0, which is what we want
|
38
|
+
return 0 if count == 0
|
39
|
+
# if the directory does exist, but is empty, count == 1, namely the directory itself, and we want to return 0 (i.e. count - 1)
|
40
|
+
# if the directory does exist and includes subdirectories, the directory itself is still counted as well, and we want to return count - 1
|
41
|
+
return count - 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def keys
|
45
|
+
return [] if size == 0
|
46
|
+
list_keys_sorted
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear
|
50
|
+
keys.each do |key|
|
51
|
+
delete_entry(key[0])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_value_path(key)
|
56
|
+
validate_key(key)
|
57
|
+
|
58
|
+
return nil if not key_cached?(key)
|
59
|
+
|
60
|
+
compile_value_path(key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def key_cached?(key)
|
64
|
+
# don't read the value here, as it may be very large - rather look whether the key is present
|
65
|
+
File.exists? compile_key_path(key)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def list_keys_sorted
|
71
|
+
result = []
|
72
|
+
append_keys(result).sort
|
73
|
+
end
|
74
|
+
|
75
|
+
def append_keys(result)
|
76
|
+
get_key_directories.each do |dir|
|
77
|
+
result << extract_key_from_directory(dir)
|
78
|
+
end
|
79
|
+
result
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_key_directories
|
83
|
+
subdirectories = Dir::glob("#{@storage_root}/**/")
|
84
|
+
#exclude the storage root directory itself
|
85
|
+
subdirectories[1..-1]
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_key_from_directory(dir)
|
89
|
+
key = dir.match(/#{@storage_root}\/(\w+)\//)[1]
|
90
|
+
[key]
|
91
|
+
end
|
92
|
+
|
93
|
+
def compile_key_path(key)
|
94
|
+
"#{@storage_root}/#{key}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def compile_value_path(key)
|
98
|
+
"#{compile_key_path(key)}/#{CACHE_FILE}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_save_key_value_pair(key, value)
|
102
|
+
validate_key(key)
|
103
|
+
raise ArgumentError.new("Only string values allowed") if not value.is_a?(String)
|
104
|
+
end
|
105
|
+
|
106
|
+
def lookup_key_value_timestamp(key)
|
107
|
+
result = [[],[]]
|
108
|
+
data = File.read(compile_value_path(key))
|
109
|
+
format_value_timestamp(result, data)
|
110
|
+
end
|
111
|
+
|
112
|
+
def format_value_timestamp(result, data)
|
113
|
+
result[0][0] = data.lines.to_a[1..-1].join
|
114
|
+
result[0][1] = data.lines.to_a[0..0].join.split("\n")[0]
|
115
|
+
result
|
116
|
+
end
|
117
|
+
|
118
|
+
def validate_key(key)
|
119
|
+
raise ArgumentError.new("Only string keys allowed") if not key.is_a?(String)
|
120
|
+
end
|
121
|
+
|
122
|
+
def empty_key_value(key)
|
123
|
+
FileUtils.makedirs([compile_key_path(key)]) if not File.exists?(compile_key_path(key))
|
124
|
+
FileUtils.rm_f(compile_value_path(key))
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_time(timestamp)
|
128
|
+
timestamp.nil? ? Time.now.to_s : timestamp.to_s
|
129
|
+
end
|
130
|
+
|
131
|
+
def prepare_to_store_key_value(key, value, timestamp)
|
132
|
+
validate_save_key_value_pair(key, value)
|
133
|
+
delete_entry(key)
|
134
|
+
empty_key_value(key)
|
135
|
+
end
|
136
|
+
|
137
|
+
def store_key_value(key, value, timestamp)
|
138
|
+
tempfile = Tempfile.new("store_key_value")
|
139
|
+
store_value_timestamp_in_file(value, timestamp, tempfile)
|
140
|
+
sync_cache_value(key, tempfile)
|
141
|
+
end
|
142
|
+
|
143
|
+
def sync_cache_value(key, tempfile)
|
144
|
+
target = compile_value_path(key)
|
145
|
+
FileUtils.rm_f(target)
|
146
|
+
FileUtils.move(tempfile.path, target)
|
147
|
+
end
|
148
|
+
|
149
|
+
def store_value_timestamp_in_file(value, timestamp, tempfile)
|
150
|
+
output = File.open(tempfile, 'a')
|
151
|
+
write_value_timestamp(output, value, timestamp)
|
152
|
+
output.close
|
153
|
+
end
|
154
|
+
|
155
|
+
def write_value_timestamp(output, value, timestamp)
|
156
|
+
# Have to be explicit about writing to the file. 'puts' collapses newline at the end of the file and
|
157
|
+
# so does not guarantee exact replication of 'value' on lookup
|
158
|
+
output.write(timestamp)
|
159
|
+
output.write("\n")
|
160
|
+
output.write(value)
|
161
|
+
output.flush
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "sqlite3"
|
2
|
+
require "eh/eh"
|
3
|
+
|
4
|
+
module Persistent
|
5
|
+
class StorageSQLite
|
6
|
+
DB_TABLE = "key_value" unless defined? DB_TABLE; DB_TABLE.freeze
|
7
|
+
DB_TIMEOUT = 30000 unless defined? DB_TIMEOUT; DB_TIMEOUT.freeze
|
8
|
+
|
9
|
+
attr_accessor :storage_details
|
10
|
+
attr_accessor :storage_handler
|
11
|
+
|
12
|
+
def initialize(storage_details)
|
13
|
+
raise ArgumentError.new("Storage details not provided") if storage_details.nil? or storage_details == ""
|
14
|
+
@storage_details = storage_details
|
15
|
+
@storage_handler = connect_to_database
|
16
|
+
@storage_handler.busy_timeout = 30000
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_key_value_pair(serialized_key, serialized_value, timestamp = nil)
|
20
|
+
delete_entry(serialized_key)
|
21
|
+
time_entry = timestamp.nil? ? Time.now.to_s : timestamp.to_s
|
22
|
+
EH::retry!(:args => [serialized_key, serialized_value, time_entry]) do
|
23
|
+
@storage_handler.execute("INSERT INTO #{DB_TABLE} (key, value, timestamp) VALUES(?, ?, ?)",serialized_key, serialized_value, time_entry)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def lookup_key(serialized_key)
|
28
|
+
EH::retry!(:args => [serialized_key]) do
|
29
|
+
@storage_handler.execute("SELECT value, timestamp FROM #{DB_TABLE} WHERE key=?", serialized_key)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete_entry(serialized_key)
|
34
|
+
EH::retry!(:args => [serialized_key]) do
|
35
|
+
@storage_handler.execute("DELETE FROM #{DB_TABLE} WHERE key=?", serialized_key)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def size
|
40
|
+
EH::retry!(:args => []) do
|
41
|
+
@storage_handler.execute("SELECT value FROM #{DB_TABLE}").size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def keys
|
46
|
+
EH::retry!(:args => []) do
|
47
|
+
@storage_handler.execute("SELECT key FROM #{DB_TABLE}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def clear
|
52
|
+
EH::retry!(:args => []) do
|
53
|
+
@storage_handler.execute("DELETE FROM #{DB_TABLE}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def connect_to_database
|
60
|
+
File.exists?(@storage_details) ? open_database : create_database
|
61
|
+
end
|
62
|
+
|
63
|
+
def open_database
|
64
|
+
EH::retry!(:args => []) do
|
65
|
+
SQLite3::Database.open(@storage_details)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_database
|
70
|
+
EH::retry!(:args => []) do
|
71
|
+
@storage_handler = SQLite3::Database.new(@storage_details)
|
72
|
+
create_table
|
73
|
+
end
|
74
|
+
@storage_handler
|
75
|
+
end
|
76
|
+
|
77
|
+
def create_table
|
78
|
+
EH::retry!(:args => []) do
|
79
|
+
@storage_handler.execute("CREATE TABLE #{DB_TABLE}(key TEXT PRIMARY KEY, value TEXT, timestamp TEXT)")
|
80
|
+
end
|
81
|
+
@storage_handler
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/multidb
CHANGED
Binary file
|
data/persistent-cache.gemspec
CHANGED
@@ -15,5 +15,8 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Persistent::Cache::VERSION
|
17
17
|
gem.add_development_dependency 'rspec', '2.12.0'
|
18
|
+
gem.add_development_dependency 'simplecov'
|
19
|
+
gem.add_development_dependency 'simplecov-rcov'
|
18
20
|
gem.add_dependency 'sqlite3', '1.3.7'
|
21
|
+
gem.add_dependency 'eh'
|
19
22
|
end
|
@@ -1,74 +1,126 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'persistent-cache'
|
3
|
-
require 'tempfile'
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
path = Tempfile.new("persistent-cache-spec-testdb").path
|
8
|
-
FileUtils.rm_f(path)
|
9
|
-
path
|
10
|
-
end
|
3
|
+
require "persistent-cache"
|
4
|
+
require "persistent-cache/storage/storage_sq_lite"
|
11
5
|
|
6
|
+
describe Persistent::Cache do
|
12
7
|
before :each do
|
13
8
|
@db_name = get_database_name
|
9
|
+
@mock_storage = double(Persistent::StorageSQLite)
|
10
|
+
@test_key = "testkey"
|
11
|
+
@test_value = "testvalue"
|
12
|
+
FileUtils.rm_f(@db_name)
|
14
13
|
end
|
15
14
|
|
15
|
+
context "when constructing" do
|
16
|
+
it "should receive database connection details and create a StorageSQLite instance if specified" do
|
17
|
+
@pcache = Persistent::Cache.new(@db_name, Persistent::Cache::STORAGE_SQLITE)
|
18
|
+
@pcache.class.should == Persistent::Cache
|
19
|
+
@pcache.storage.is_a?(Persistent::StorageSQLite).should == true
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should raise an ArgumentError if storage details have not been provided" do
|
23
|
+
expect {
|
24
|
+
Persistent::Cache.new(nil)
|
25
|
+
}.to raise_error(ArgumentError)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should remember the freshness interval if provided" do
|
29
|
+
@pcache = Persistent::Cache.new(@db_name, 123)
|
30
|
+
@pcache.fresh.should == 123
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should remember the storage details provided" do
|
34
|
+
@pcache = Persistent::Cache.new(@db_name, 123)
|
35
|
+
@pcache.storage_details.should == @db_name
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should default the freshness interval to FRESH if not provided" do
|
39
|
+
@pcache = Persistent::Cache.new(@db_name)
|
40
|
+
@pcache.fresh.should == Persistent::Cache::FRESH
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should raise an ArgumentError if an unknown storage type has been provided" do
|
44
|
+
expect {
|
45
|
+
Persistent::Cache.new(@db_name, 100, "unknown")
|
46
|
+
}.to raise_error(ArgumentError)
|
47
|
+
end
|
48
|
+
end
|
16
49
|
|
17
50
|
context "When assigning a value to a key" do
|
18
|
-
it "should
|
19
|
-
|
51
|
+
it "should ask the storage handler to first delete, then save the key/value pair" do
|
52
|
+
Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
|
53
|
+
@mock_storage.should_receive(:delete_entry)
|
54
|
+
@mock_storage.should_receive(:save_key_value_pair).with(Marshal.dump(@test_key), Marshal.dump(@test_value), nil)
|
20
55
|
@pcache = Persistent::Cache.new(@db_name)
|
21
|
-
@pcache[
|
22
|
-
handle = SQLite3::Database.open(@db_name)
|
23
|
-
result = handle.execute "select value, timestamp from #{Persistent::Cache::DB_TABLE} where key=?", Marshal.dump("testkey")
|
24
|
-
result.nil?.should == false
|
25
|
-
result[0].nil?.should == false
|
26
|
-
result[0][0].should == Marshal.dump("testvalue")
|
27
|
-
test_time = Time.parse(result[0][1])
|
28
|
-
test_time.should > start_time and test_time.should < start_time + 600
|
56
|
+
@pcache[@test_key] = @test_value
|
29
57
|
end
|
30
58
|
|
31
|
-
it "should
|
59
|
+
it "should ask the storage handler to delete if the value is nil using []" do
|
60
|
+
Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
|
61
|
+
@mock_storage.should_receive(:delete_entry).with(Marshal.dump(@test_key))
|
32
62
|
@pcache = Persistent::Cache.new(@db_name)
|
33
|
-
@pcache[
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
63
|
+
@pcache[@test_key] = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should ask the storage handler to delete if the value is nil using set()" do
|
67
|
+
Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
|
68
|
+
@mock_storage.should_receive(:delete_entry).with(Marshal.dump(@test_key))
|
69
|
+
@pcache = Persistent::Cache.new(@db_name)
|
70
|
+
@pcache.set(@test_key, nil, Time.now)
|
41
71
|
end
|
42
72
|
|
43
73
|
it "should serialize the key and value for persistence" do
|
74
|
+
Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
|
75
|
+
@mock_storage.should_receive(:delete_entry)
|
76
|
+
@mock_storage.should_receive(:save_key_value_pair).with(Marshal.dump(@test_key), Marshal.dump(@test_value), nil)
|
44
77
|
@pcache = Persistent::Cache.new(@db_name)
|
45
|
-
@pcache
|
46
|
-
|
78
|
+
@pcache[@test_key] = @test_value
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should ask the storage handler to store the value, with a specific timestamp if specified" do
|
82
|
+
Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
|
83
|
+
@mock_storage.should_receive(:delete_entry)
|
84
|
+
timestamp = Time.now - 100
|
85
|
+
@mock_storage.should_receive(:save_key_value_pair).with(Marshal.dump(@test_key), Marshal.dump(@test_value), timestamp)
|
86
|
+
@pcache = Persistent::Cache.new(@db_name)
|
87
|
+
@pcache.set(@test_key, @test_value, timestamp)
|
47
88
|
end
|
48
89
|
end
|
49
90
|
|
50
91
|
context "When looking up a value given its key" do
|
51
|
-
it "should retrieve the value from
|
92
|
+
it "should retrieve the value from storage using lookup_key and deserialize the value" do
|
93
|
+
@mock_storage.should_receive(:delete_entry)
|
94
|
+
@mock_storage.should_receive(:save_key_value_pair).with(Marshal.dump(@test_key), Marshal.dump(@test_value), nil)
|
95
|
+
@mock_storage.should_receive(:lookup_key).with(Marshal.dump(@test_key)).and_return([[Marshal.dump(@test_value), Time.now.to_s]])
|
96
|
+
Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
|
52
97
|
@pcache = Persistent::Cache.new(@db_name)
|
53
|
-
@pcache[
|
54
|
-
result = @pcache[
|
55
|
-
result.should ==
|
98
|
+
@pcache[@test_key] = @test_value
|
99
|
+
result = @pcache[@test_key]
|
100
|
+
result.should == @test_value
|
56
101
|
end
|
57
102
|
|
58
103
|
it "should return nil if a value exists but it not fresh" do
|
59
|
-
|
104
|
+
@mock_storage.should_receive(:delete_entry)
|
105
|
+
@mock_storage.should_receive(:lookup_key).with(Marshal.dump(@test_key)).and_return([[Marshal.dump(@test_value), (Time.now - Persistent::Cache::FRESH).to_s]])
|
106
|
+
Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
|
107
|
+
|
108
|
+
@pcache = Persistent::Cache.new(@db_name)
|
109
|
+
@pcache[@test_key].nil?.should == true
|
60
110
|
end
|
61
111
|
|
62
112
|
it "should remove from the cache an entry it encounters that is not fresh" do
|
63
|
-
|
64
|
-
|
65
|
-
|
113
|
+
@mock_storage.should_receive(:delete_entry)
|
114
|
+
@mock_storage.should_receive(:lookup_key).with(Marshal.dump(@test_key)).and_return([[Marshal.dump(@test_value), (Time.now - Persistent::Cache::FRESH).to_s]])
|
115
|
+
Persistent::StorageSQLite.should_receive(:new).and_return @mock_storage
|
116
|
+
|
117
|
+
@pcache = Persistent::Cache.new(@db_name)
|
118
|
+
@pcache[@test_key]
|
66
119
|
end
|
67
120
|
|
68
121
|
it "should return nil if a key is not in the database" do
|
69
122
|
@pcache = Persistent::Cache.new(@db_name)
|
70
|
-
@pcache["
|
71
|
-
result = @pcache["testkey2"]
|
123
|
+
result = @pcache["thiskeydoesnotexist"]
|
72
124
|
result.nil?.should == true
|
73
125
|
end
|
74
126
|
|
@@ -78,53 +130,6 @@ describe Persistent::Cache do
|
|
78
130
|
@pcache.should_receive(:lookup_key).with(Marshal.dump("testkey"))
|
79
131
|
@pcache["testkey"]
|
80
132
|
end
|
81
|
-
|
82
|
-
def fetch_stale_entry
|
83
|
-
@pcache = Persistent::Cache.new(@db_name)
|
84
|
-
timestamp = Time.now - Persistent::Cache::FRESH
|
85
|
-
@handle = SQLite3::Database.open(@db_name)
|
86
|
-
@handle.execute("INSERT INTO #{Persistent::Cache::DB_TABLE} (key, value, timestamp) VALUES ('#{Marshal.dump("oldkey")}', '#{Marshal.dump("oldvalue")}', '#{timestamp}')")
|
87
|
-
@pcache["oldkey"].nil?.should == true
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
context "when constructing" do
|
92
|
-
it "should receive database connection details" do
|
93
|
-
@pcache = Persistent::Cache.new(@db_name)
|
94
|
-
@pcache.class.should == Persistent::Cache
|
95
|
-
@pcache.database_details.should == @db_name
|
96
|
-
end
|
97
|
-
|
98
|
-
it "should create the database if it does not exist" do
|
99
|
-
FileUtils.rm_f(@db_name)
|
100
|
-
Persistent::Cache.new(@db_name)
|
101
|
-
File.exists?(@db_name).should be_true
|
102
|
-
end
|
103
|
-
|
104
|
-
it "should create a key_value table with key (TEXT) and value (TEXT) and timestamp (TEXT) columns" do
|
105
|
-
FileUtils.rm_f(@db_name)
|
106
|
-
Persistent::Cache.new(@db_name)
|
107
|
-
handle = SQLite3::Database.open(@db_name)
|
108
|
-
result = handle.execute "PRAGMA table_info(#{Persistent::Cache::DB_TABLE})"
|
109
|
-
result[0][1].should == "key"
|
110
|
-
result[0][2].should == "TEXT"
|
111
|
-
result[1][1].should == "value"
|
112
|
-
result[1][2].should == "TEXT"
|
113
|
-
result[2][1].should == "timestamp"
|
114
|
-
result[2][2].should == "TEXT"
|
115
|
-
end
|
116
|
-
|
117
|
-
|
118
|
-
it "should use the existing database if it does exist" do
|
119
|
-
FileUtils.rm_f(@db_name)
|
120
|
-
handle = SQLite3::Database.new(@db_name)
|
121
|
-
handle.execute "create table test123 ( id int );"
|
122
|
-
handle.close
|
123
|
-
Persistent::Cache.new(@db_name)
|
124
|
-
handle = SQLite3::Database.open(@db_name)
|
125
|
-
result = handle.execute "select name from sqlite_master where type='table'"
|
126
|
-
result[0][0].should == "test123"
|
127
|
-
end
|
128
133
|
end
|
129
134
|
|
130
135
|
context "it should behave like a cache" do
|
@@ -162,7 +167,7 @@ describe Persistent::Cache do
|
|
162
167
|
100.times do |i|
|
163
168
|
threads << Thread.new do
|
164
169
|
Thread.current['pcache'] = Persistent::Cache.new("multidb")
|
165
|
-
Thread.current['pcache']["multi_test"] += 1
|
170
|
+
Thread.current['pcache']["multi_test"] += 1 if not Thread.current['pcache'].nil? and not Thread.current['pcache']["multi_test"].nil?
|
166
171
|
end
|
167
172
|
end
|
168
173
|
threads.each { |t| t.join }
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'rspec'
|
2
2
|
require 'rspec/mocks'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'simplecov'
|
5
|
+
require 'simplecov-rcov'
|
3
6
|
|
4
7
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
5
8
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
@@ -21,3 +24,15 @@ RSpec.configure do |config|
|
|
21
24
|
# --seed 1234
|
22
25
|
config.order = 'random'
|
23
26
|
end
|
27
|
+
|
28
|
+
SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
|
29
|
+
SimpleCov.start do
|
30
|
+
add_filter "/spec/"
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_database_name
|
34
|
+
path = Tempfile.new("persistent-cache-spec-testdb").path
|
35
|
+
FileUtils.rm_f(path)
|
36
|
+
path
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,264 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require "persistent-cache/storage/storage_directory"
|
3
|
+
|
4
|
+
describe Persistent::StorageDirectory do
|
5
|
+
before :each do
|
6
|
+
@test_key = "testkey"
|
7
|
+
@test_value = "testvalue"
|
8
|
+
@db_name = get_database_name
|
9
|
+
@test_dir = "#{@db_name}/#{@test_key}"
|
10
|
+
@test_file = "#{@test_dir}/#{Persistent::StorageDirectory::CACHE_FILE}"
|
11
|
+
@test_data = "some data\nmoredata\n\n"
|
12
|
+
|
13
|
+
delete_database
|
14
|
+
@iut = Persistent::StorageDirectory.new(@db_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when constructed" do
|
18
|
+
it "should create the database if it does not exist" do
|
19
|
+
File.exists?(@db_name).should be_true
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should propagate errors that are raised when failing to create a database" do
|
23
|
+
delete_database
|
24
|
+
FileUtils.should_receive(:makedirs).and_raise RuntimeError.new("testing")
|
25
|
+
expect {
|
26
|
+
@iut = Persistent::StorageDirectory.new(@db_name)
|
27
|
+
}.to raise_error RuntimeError
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should use the existing database if it does exist" do
|
31
|
+
delete_database
|
32
|
+
FileUtils.makedirs([@db_name])
|
33
|
+
test_file = "#{@db_name}/hello"
|
34
|
+
`touch #{test_file}`
|
35
|
+
Persistent::StorageDirectory.new(@db_name)
|
36
|
+
File.exist?(test_file).should == true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should have a database" do
|
40
|
+
@iut.is_a?(Persistent::StorageDirectory).should == true
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should raise an ArgumentError if storage details have not been provided" do
|
44
|
+
expect {
|
45
|
+
Persistent::StorageDirectory.new(nil)
|
46
|
+
}.to raise_error(ArgumentError)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when asked to store a key value pair" do
|
51
|
+
it "should create a directory named the same as the key, in the storage root, if that directory does not exist, and store the value in a file called CACHE_FILE" do
|
52
|
+
setup_test_entry
|
53
|
+
read_file_content(@test_file).should == @test_data
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should default to storing the current time as the first line of the catalogue" do
|
57
|
+
now = Time.now
|
58
|
+
FileUtils.rm_f(@test_dir)
|
59
|
+
@iut.save_key_value_pair(@test_key, @test_data)
|
60
|
+
read_file_timestamp(@test_file) == now.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should store a time specified as the first line of the catalogue" do
|
64
|
+
time = Time.now - 2500
|
65
|
+
FileUtils.rm_f(@test_dir)
|
66
|
+
@iut.save_key_value_pair(@test_key, @test_data, time)
|
67
|
+
read_file_timestamp(@test_file).should == time.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should overwrite the existing key/value pair if they already exist" do
|
71
|
+
FileUtils.rm_f(@test_dir)
|
72
|
+
@iut.save_key_value_pair(@test_key, "old data")
|
73
|
+
@iut.save_key_value_pair(@test_key, @test_data)
|
74
|
+
File.exists?(@test_dir).should == true
|
75
|
+
File.exists?(@test_file).should == true
|
76
|
+
read_file_content(@test_file).should == @test_data
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should raise an ArgumentError if the key is not a string" do
|
80
|
+
expect {
|
81
|
+
@iut.save_key_value_pair(1234, @test_data)
|
82
|
+
}.to raise_error ArgumentError
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should raise an ArgumentError if the value is not a string" do
|
86
|
+
expect {
|
87
|
+
@iut.save_key_value_pair(@test_key, 1234)
|
88
|
+
}.to raise_error ArgumentError
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should store the value exactly as given, regardless of newlines" do
|
92
|
+
@iut.save_key_value_pair(@test_key, "some data")
|
93
|
+
@iut.lookup_key(@test_key)[0][0].should == "some data"
|
94
|
+
|
95
|
+
@iut.save_key_value_pair(@test_key, "some data\n")
|
96
|
+
@iut.lookup_key(@test_key)[0][0].should == "some data\n"
|
97
|
+
|
98
|
+
@iut.save_key_value_pair(@test_key, "some data\n\n")
|
99
|
+
@iut.lookup_key(@test_key)[0][0].should == "some data\n\n"
|
100
|
+
|
101
|
+
@iut.save_key_value_pair(@test_key, "\nline 1\n23456\n\n\nsome data\n")
|
102
|
+
@iut.lookup_key(@test_key)[0][0].should == "\nline 1\n23456\n\n\nsome data\n"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "When looking up a value given its key" do
|
107
|
+
it "should retrieve the contents of the catalogue file from the database, excluding the timestamp" do
|
108
|
+
setup_test_entry
|
109
|
+
result = @iut.lookup_key(@test_key)
|
110
|
+
result[0][0].should == @test_data
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should retrieve the timestamp of the revision from the database" do
|
114
|
+
now = Time.now
|
115
|
+
setup_test_entry
|
116
|
+
result = @iut.lookup_key(@test_key)
|
117
|
+
result[0][1].should == now.to_s
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should return an empty array if a key is not in the database" do
|
121
|
+
setup_test_entry
|
122
|
+
result = @iut.lookup_key("thiskeyshouldnotexist")
|
123
|
+
result.should == []
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should raise an ArgumentError if the key is not a string" do
|
127
|
+
expect {
|
128
|
+
@iut.lookup_key(1234)
|
129
|
+
}.to raise_error ArgumentError
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "when asked to delete an entry" do
|
134
|
+
it "should not raise an error if the directory that results from the hash is not present" do
|
135
|
+
@iut.delete_entry("thiskeyshouldnotexist")
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should delete the directory that results from the hash if it is present" do
|
139
|
+
setup_test_entry
|
140
|
+
@iut.delete_entry(@test_key)
|
141
|
+
@iut.lookup_key(@test_key).should == []
|
142
|
+
File.exists?(@test_dir).should == false
|
143
|
+
File.exists?(@test_file).should == false
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should raise an ArgumentError if the key is not a string" do
|
147
|
+
expect {
|
148
|
+
@iut.delete_entry(1234)
|
149
|
+
}.to raise_error ArgumentError
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "when asked the size of the database" do
|
154
|
+
it "should return 0 if the database has no entries" do
|
155
|
+
@iut.size.should == 0
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should return the number of entries" do
|
159
|
+
populate_database
|
160
|
+
@iut.size.should == 3
|
161
|
+
end
|
162
|
+
|
163
|
+
it "should return 0 if the database does not exist" do
|
164
|
+
delete_database
|
165
|
+
@iut.size.should == 0
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context "when asked for the keys in the database" do
|
170
|
+
it "should return an empty array if there are no entries in the database" do
|
171
|
+
@iut.keys.should == []
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should return the keys (directories) in the database" do
|
175
|
+
populate_database
|
176
|
+
@iut.keys.should == [["one"], ["three"], ["two"]]
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should return the keys in a sorted array" do
|
180
|
+
populate_database
|
181
|
+
@iut.keys.should == [["one"], ["three"], ["two"]]
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should not return the storage root itself" do
|
185
|
+
populate_database
|
186
|
+
@iut.keys.each do |key|
|
187
|
+
(key == "").should == false
|
188
|
+
(key == "/").should == false
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should return the keys in an array, with each key in its own sub-array" do
|
193
|
+
populate_database
|
194
|
+
@iut.keys.is_a?(Array).should == true
|
195
|
+
@iut.keys[0].is_a?(Array).should == true
|
196
|
+
@iut.keys[0][0].is_a?(String).should == true
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context "when asked to clear the database" do
|
201
|
+
it "should not delete the database root directory" do
|
202
|
+
setup_test_entry
|
203
|
+
@iut.clear
|
204
|
+
File.exists?(@test_file).should == false
|
205
|
+
File.exists?(@test_dir).should == false
|
206
|
+
File.exist?(@iut.storage_root).should == true
|
207
|
+
end
|
208
|
+
|
209
|
+
it "should delete all directories in the database" do
|
210
|
+
populate_database
|
211
|
+
@iut.clear
|
212
|
+
@iut.size.should == 0
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
context "when asked about the path to a key's cache value file" do
|
217
|
+
it "should return nil if the key is not in the cache" do
|
218
|
+
@iut.get_value_path(@test_key).should == nil
|
219
|
+
end
|
220
|
+
|
221
|
+
it "should return the path to the key's cache value file if the key is in the cache" do
|
222
|
+
@iut.save_key_value_pair(@test_key, @test_data)
|
223
|
+
@iut.get_value_path(@test_key).should == @test_file
|
224
|
+
end
|
225
|
+
|
226
|
+
it "should raise an ArgumentError if the key is not a string" do
|
227
|
+
expect {
|
228
|
+
@iut.get_value_path(123)
|
229
|
+
}.to raise_error ArgumentError
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def populate_database
|
234
|
+
@iut.save_key_value_pair("one", "one")
|
235
|
+
@iut.save_key_value_pair("two", "two")
|
236
|
+
@iut.save_key_value_pair("three", "three")
|
237
|
+
end
|
238
|
+
|
239
|
+
def setup_test_entry
|
240
|
+
FileUtils.rm_f(@test_dir)
|
241
|
+
@iut.save_key_value_pair(@test_key, @test_data)
|
242
|
+
File.exists?(@test_dir).should == true
|
243
|
+
File.exists?(@test_file).should == true
|
244
|
+
end
|
245
|
+
|
246
|
+
def delete_database
|
247
|
+
FileUtils.rm_rf(@db_name)
|
248
|
+
File.exists?(@db_name).should == false
|
249
|
+
end
|
250
|
+
|
251
|
+
def read_file_data(file)
|
252
|
+
File.read(file).split("\n")
|
253
|
+
end
|
254
|
+
|
255
|
+
def read_file_timestamp(file)
|
256
|
+
result = File.read(file)
|
257
|
+
result.lines.to_a[0..0].join.split("\n")[0]
|
258
|
+
end
|
259
|
+
|
260
|
+
def read_file_content(file)
|
261
|
+
result = File.read(file)
|
262
|
+
result.lines.to_a[1..-1].join
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require "persistent-cache/storage/storage_sq_lite"
|
3
|
+
|
4
|
+
describe Persistent::StorageSQLite do
|
5
|
+
before :each do
|
6
|
+
@db_name = get_database_name
|
7
|
+
delete_database
|
8
|
+
@test_key = "testkey"
|
9
|
+
@test_value = "testvalue"
|
10
|
+
@iut = Persistent::StorageSQLite.new(@db_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
context "when constructed" do
|
14
|
+
it "should create the database if it does not exist" do
|
15
|
+
File.exists?(@db_name).should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should create a key_value table with key (TEXT) and value (TEXT) and timestamp (TEXT) columns" do
|
19
|
+
handle = SQLite3::Database.open(@db_name)
|
20
|
+
result = handle.execute "PRAGMA table_info(#{Persistent::StorageSQLite::DB_TABLE})"
|
21
|
+
result[0][1].should == "key"
|
22
|
+
result[0][2].should == "TEXT"
|
23
|
+
result[1][1].should == "value"
|
24
|
+
result[1][2].should == "TEXT"
|
25
|
+
result[2][1].should == "timestamp"
|
26
|
+
result[2][2].should == "TEXT"
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
it "should use the existing database if it does exist" do
|
31
|
+
delete_database
|
32
|
+
handle = SQLite3::Database.new(@db_name)
|
33
|
+
handle.execute "create table test123 ( id int );"
|
34
|
+
handle.close
|
35
|
+
Persistent::StorageSQLite.new(@db_name)
|
36
|
+
handle = SQLite3::Database.open(@db_name)
|
37
|
+
result = handle.execute "select name from sqlite_master where type='table'"
|
38
|
+
result[0][0].should == "test123"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should have a database handler" do
|
42
|
+
@iut.storage_handler.is_a?(SQLite3::Database).should == true
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should set the SQLite busy timeout to DB_TIMEOUT" do
|
46
|
+
delete_database
|
47
|
+
mock_database = double(SQLite3::Database)
|
48
|
+
mock_database.should_receive(:execute).at_least(0)
|
49
|
+
mock_database.should_receive(:busy_timeout=).with(Persistent::StorageSQLite::DB_TIMEOUT)
|
50
|
+
SQLite3::Database.should_receive(:new).and_return(mock_database)
|
51
|
+
Persistent::StorageSQLite.new(@db_name)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should raise an ArgumentError if storage details have not been provided" do
|
55
|
+
expect {
|
56
|
+
Persistent::StorageSQLite.new(nil)
|
57
|
+
}.to raise_error(ArgumentError)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when asked to store a key value pair" do
|
62
|
+
it "should store the key/value pair in the db, with the current time as timestamp" do
|
63
|
+
start_time = Time.now - 1
|
64
|
+
@iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
|
65
|
+
handle = SQLite3::Database.open(@db_name)
|
66
|
+
result = handle.execute "select value, timestamp from #{Persistent::StorageSQLite::DB_TABLE} where key=?", Marshal.dump(@test_key)
|
67
|
+
result.nil?.should == false
|
68
|
+
result[0].nil?.should == false
|
69
|
+
result[0][0].should == Marshal.dump(@test_value)
|
70
|
+
test_time = Time.parse(result[0][1])
|
71
|
+
test_time.should > start_time and test_time.should < start_time + 600
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should store the key/value pair in the db, with a timestamp specified" do
|
75
|
+
test_time = (Time.now - 2500)
|
76
|
+
@iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value), test_time)
|
77
|
+
handle = SQLite3::Database.open(@db_name)
|
78
|
+
result = handle.execute "select value, timestamp from #{Persistent::StorageSQLite::DB_TABLE} where key=?", Marshal.dump(@test_key)
|
79
|
+
result.nil?.should == false
|
80
|
+
result[0].nil?.should == false
|
81
|
+
result[0][0].should == Marshal.dump(@test_value)
|
82
|
+
time_retrieved = Time.parse(result[0][1])
|
83
|
+
time_retrieved.to_s.should == test_time.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should overwrite the existing key/value pair if they already exist" do
|
87
|
+
@iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
|
88
|
+
@iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump("testvalue2"))
|
89
|
+
handle = SQLite3::Database.open(@db_name)
|
90
|
+
result = handle.execute "select value from #{Persistent::StorageSQLite::DB_TABLE} where key=?", Marshal.dump(@test_key)
|
91
|
+
result.nil?.should == false
|
92
|
+
result[0].nil?.should == false
|
93
|
+
result.size.should == 1
|
94
|
+
result[0][0].should == Marshal.dump("testvalue2")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "When looking up a value given its key" do
|
99
|
+
it "should retrieve the value from the database" do
|
100
|
+
@iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
|
101
|
+
result = @iut.lookup_key(Marshal.dump(@test_key))
|
102
|
+
result[0][0].should == Marshal.dump(@test_value)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should retrieve the timestamp when the value was stored from the database" do
|
106
|
+
now = Time.now.to_s
|
107
|
+
@iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
|
108
|
+
sleep 1
|
109
|
+
result = @iut.lookup_key(Marshal.dump(@test_key))
|
110
|
+
result[0][1].should == now
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should return an empty array if a key is not in the database" do
|
114
|
+
@iut.delete_entry(Marshal.dump(@test_key))
|
115
|
+
result = @iut.lookup_key(Marshal.dump(@test_key))
|
116
|
+
result.should == []
|
117
|
+
result[0].should == nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "when asked to delete an entry" do
|
122
|
+
it "should not raise an error if the entry is not present" do
|
123
|
+
@iut.delete_entry(Marshal.dump("shouldnotbepresent"))
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should delete the entry if it is present" do
|
127
|
+
@iut.save_key_value_pair(Marshal.dump(@test_key), Marshal.dump(@test_value))
|
128
|
+
result = @iut.lookup_key(Marshal.dump(@test_key))
|
129
|
+
result[0][0].should == Marshal.dump(@test_value)
|
130
|
+
@iut.delete_entry(Marshal.dump(@test_key))
|
131
|
+
result = @iut.lookup_key(Marshal.dump(@test_key))
|
132
|
+
result.should == []
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context "when asked the size of the database" do
|
137
|
+
it "should return 0 if the database has no entries" do
|
138
|
+
@iut.size.should == 0
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should return the number of entries" do
|
142
|
+
populate_database(@iut)
|
143
|
+
@iut.size.should == 3
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "when asked for the keys in the database" do
|
148
|
+
it "should return an empty array if there are no entries in the database" do
|
149
|
+
@iut.keys.should == []
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should return the keys in the database" do
|
153
|
+
populate_database(@iut)
|
154
|
+
keys = @iut.keys.flatten
|
155
|
+
keys.include?(Marshal.dump("one")).should == true
|
156
|
+
keys.include?(Marshal.dump("two")).should == true
|
157
|
+
keys.include?(Marshal.dump("three")).should == true
|
158
|
+
@iut.size.should == 3
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should return the keys in an array, with each key in its own sub-array" do
|
162
|
+
populate_database(@iut)
|
163
|
+
found = false
|
164
|
+
test = Marshal.dump("one")
|
165
|
+
found = true if (@iut.keys[0][0] == test or @iut.keys[0][1] == test or @iut.keys[0][2] == test)
|
166
|
+
found.should == true
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "when asked to clear the database" do
|
171
|
+
it "should not delete the database file" do
|
172
|
+
populate_database(@iut)
|
173
|
+
@iut.clear
|
174
|
+
File.exists?(@db_name).should be_true
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should delete all entries in the database" do
|
178
|
+
populate_database(@iut)
|
179
|
+
@iut.clear
|
180
|
+
@iut.size.should == 0
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def populate_database(iut)
|
185
|
+
iut.save_key_value_pair(Marshal.dump("one"), Marshal.dump("one"))
|
186
|
+
iut.save_key_value_pair(Marshal.dump("two"), Marshal.dump("two"))
|
187
|
+
iut.save_key_value_pair(Marshal.dump("three"), Marshal.dump("three"))
|
188
|
+
end
|
189
|
+
|
190
|
+
def delete_database
|
191
|
+
FileUtils.rm_f(@db_name)
|
192
|
+
end
|
193
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: persistent-cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-06-12 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rspec
|
@@ -28,6 +28,38 @@ dependencies:
|
|
28
28
|
- - '='
|
29
29
|
- !ruby/object:Gem::Version
|
30
30
|
version: 2.12.0
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: simplecov
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: simplecov-rcov
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
31
63
|
- !ruby/object:Gem::Dependency
|
32
64
|
name: sqlite3
|
33
65
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,6 +76,22 @@ dependencies:
|
|
44
76
|
- - '='
|
45
77
|
- !ruby/object:Gem::Version
|
46
78
|
version: 1.3.7
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: eh
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
47
95
|
description: Persistent Cache using SQLite
|
48
96
|
email:
|
49
97
|
- wvd@hetzner.co.za
|
@@ -60,11 +108,15 @@ files:
|
|
60
108
|
- README.md
|
61
109
|
- Rakefile
|
62
110
|
- lib/persistent-cache.rb
|
111
|
+
- lib/persistent-cache/storage/storage_directory.rb
|
112
|
+
- lib/persistent-cache/storage/storage_sq_lite.rb
|
63
113
|
- lib/persistent-cache/version.rb
|
64
114
|
- multidb
|
65
115
|
- persistent-cache.gemspec
|
66
116
|
- spec/persistent-cache_spec.rb
|
67
117
|
- spec/spec_helper.rb
|
118
|
+
- spec/storage/storage_directory_spec.rb
|
119
|
+
- spec/storage/storage_sqlite_spec.rb
|
68
120
|
homepage: ''
|
69
121
|
licenses: []
|
70
122
|
post_install_message:
|
@@ -93,3 +145,5 @@ summary: Persistent Cache has a default freshness threshold of 179 days after wh
|
|
93
145
|
test_files:
|
94
146
|
- spec/persistent-cache_spec.rb
|
95
147
|
- spec/spec_helper.rb
|
148
|
+
- spec/storage/storage_directory_spec.rb
|
149
|
+
- spec/storage/storage_sqlite_spec.rb
|