persistent-cache 0.0.8 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|